├── test ├── fixtures │ ├── scopedLinter │ │ ├── index.js │ │ └── package.json │ ├── noStandardEngine │ │ ├── index.js │ │ └── package.json │ ├── skip │ │ ├── submodule │ │ │ ├── index.js │ │ │ └── package.json │ │ └── package.json │ ├── standardEngineKey │ │ ├── index.js │ │ └── package.json │ ├── simpleSemiStandard │ │ ├── index.js │ │ ├── node_modules │ │ │ └── semistandard │ │ │ │ ├── package.json │ │ │ │ └── index.js │ │ └── package.json │ ├── stubForWorker │ │ ├── crashing.js │ │ ├── crashOnLint.js │ │ ├── lintText-sync.js │ │ ├── errOnLint.js │ │ ├── lintText-async.js │ │ ├── errOnLint-sync.js │ │ └── index.js │ └── localLinter │ │ └── node_modules │ │ └── my-linter │ │ └── index.js ├── mocha.opts ├── benchmark.js ├── util │ ├── textEditorFactory.js │ └── atomHelper.js └── lib │ ├── cache.spec.js │ ├── fix.spec.js │ ├── commands.spec.js │ ├── reportError.spec.js │ ├── workerManagement.spec.js │ ├── findOptions.spec.js │ ├── register.spec.js │ ├── optInManager.spec.js │ ├── worker.spec.js │ └── linting.spec.js ├── .gitignore ├── lib ├── globals.js ├── fix.js ├── caches.js ├── commands.js ├── reportError.js ├── worker.js ├── register.js ├── workerManagement.js ├── findOptions.js ├── optInManager.js └── linting.js ├── .editorconfig ├── .travis.yml ├── appveyor.yml ├── LICENSE ├── package.json └── README.md /test/fixtures/scopedLinter/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 'foo!' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | /node_modules 3 | coverage 4 | .nvmrc 5 | -------------------------------------------------------------------------------- /test/fixtures/noStandardEngine/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 'foo' 2 | -------------------------------------------------------------------------------- /test/fixtures/skip/submodule/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 'foo!' 2 | -------------------------------------------------------------------------------- /test/fixtures/standardEngineKey/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 'foo!' 2 | -------------------------------------------------------------------------------- /lib/globals.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | atom, 3 | console 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/simpleSemiStandard/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 'foo!' 2 | -------------------------------------------------------------------------------- /test/fixtures/stubForWorker/crashing.js: -------------------------------------------------------------------------------- 1 | throw new Error('crash in linter loading') 2 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | --reporter spec 3 | --require test/util/atomHelper 4 | --timeout 5000 5 | -------------------------------------------------------------------------------- /test/fixtures/noStandardEngine/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "module-with-no-standard-engine-linting" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/stubForWorker/crashOnLint.js: -------------------------------------------------------------------------------- 1 | exports.lintText = () => { 2 | throw new Error('threw when linting') 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/stubForWorker/lintText-sync.js: -------------------------------------------------------------------------------- 1 | exports.lintTextSync = (source, opts) => { 2 | return { results: [] } 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/stubForWorker/errOnLint.js: -------------------------------------------------------------------------------- 1 | exports.lintText = (source, opts, cb) => { 2 | cb(new Error('err when linting')) 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/stubForWorker/lintText-async.js: -------------------------------------------------------------------------------- 1 | exports.lintText = (source, opts, cb) => { 2 | cb(null, { results: [] }) 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/simpleSemiStandard/node_modules/semistandard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "semistandard", 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/stubForWorker/errOnLint-sync.js: -------------------------------------------------------------------------------- 1 | exports.lintTextSync = (source, opts) => { 2 | throw new Error('err when linting') 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/simpleSemiStandard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "some-module", 3 | "devDependencies": { 4 | "semistandard": "1.0.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/simpleSemiStandard/node_modules/semistandard/index.js: -------------------------------------------------------------------------------- 1 | exports.lintText = (source, opts, cb) => { 2 | cb(null, { results: [{ messages: [] }] }) 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/skip/submodule/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "this-is-a-submodule-with-package-json", 3 | "standard-engine": { 4 | "skip": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/localLinter/node_modules/my-linter/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | exports.lintText = (source, opts, cb) => { 3 | cb(null, { results: [{ hello: 'world' }] }) 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/standardEngineKey/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "some-module", 3 | "devDependencies": { 4 | "standard": "*" 5 | }, 6 | "standard-engine": "my-linter" 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/scopedLinter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "some-module", 3 | "standard-engine": "@my-scope/my-linter", 4 | "my-linter": { 5 | "ignore": ["world"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/skip/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "some-module", 3 | "devDependencies": { 4 | "standard": "*" 5 | }, 6 | "standard-engine": { 7 | "name": "my-linter" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | 9 | [*.js] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [{package.json,*.yml,*.cjson}] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /lib/fix.js: -------------------------------------------------------------------------------- 1 | const { fix: getOutput } = require('./linting') 2 | 3 | function fix (textEditor, reportError) { 4 | return getOutput(textEditor, reportError) 5 | .then(output => { 6 | if (output) textEditor.getBuffer().setTextViaDiff(output) 7 | }) 8 | } 9 | 10 | module.exports = fix 11 | -------------------------------------------------------------------------------- /lib/caches.js: -------------------------------------------------------------------------------- 1 | const caches = new Set() 2 | 3 | function add (cache) { 4 | caches.add(cache) 5 | return cache 6 | } 7 | exports.add = add 8 | 9 | function clearAll () { 10 | for (const cache of caches) { 11 | if (cache.reset) cache.reset() 12 | if (cache.clear) cache.clear() 13 | } 14 | } 15 | exports.clearAll = clearAll 16 | -------------------------------------------------------------------------------- /test/fixtures/stubForWorker/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Ensure this stub is not cached. The worker should take care of caching it. 3 | delete require.cache[module.id] 4 | // If the module is reloaded then the second time the worker tries to lint the 5 | // text it'll receive a conut of 1, not 2. 6 | let count = 0 7 | 8 | exports.lintText = (source, opts, cb) => { 9 | count++ 10 | cb(null, { results: [{ count }] }) 11 | } 12 | -------------------------------------------------------------------------------- /lib/commands.js: -------------------------------------------------------------------------------- 1 | const caches = require('./caches') 2 | const reportError = require('./reportError') 3 | 4 | let fix = (...args) => { 5 | const loaded = require('./fix') 6 | fix = loaded 7 | return loaded(...args) 8 | } 9 | 10 | module.exports = { 11 | 'Standard Engine:Fix File' () { 12 | const editor = atom.workspace.getActiveTextEditor() 13 | if (editor) fix(editor, reportError) 14 | }, 15 | 16 | 'Standard Engine:Restart' () { 17 | caches.clearAll() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7.4.0" # Ships with Atom 1.22.0 4 | env: 5 | - FRESH_DEPS=false 6 | - FRESH_DEPS=true 7 | cache: 8 | directories: 9 | - $HOME/.npm 10 | before_install: 11 | - npm install --global npm@5.5.1 12 | - npm --version 13 | install: 14 | - if [[ ${FRESH_DEPS} == "true" ]]; then npm install --no-shrinkwrap --prefer-online; else npm install --prefer-offline; fi 15 | script: npm run coverage 16 | after_script: 17 | - "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls" 18 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: off 2 | cache: 3 | - '%APPDATA%\npm-cache' 4 | clone_depth: 1 5 | skip_branch_with_pr: true 6 | skip_commits: 7 | files: 8 | - '**/*.md' 9 | configuration: 10 | - FreshDeps 11 | - LockedDeps 12 | environment: 13 | nodejs_version: "7.4.0" 14 | install: 15 | - ps: Install-Product node $env:nodejs_version 16 | - npm install --global npm@5.5.1 17 | - npm --version 18 | - if %configuration% == FreshDeps (npm install --no-shrinkwrap --prefer-online) 19 | - if %configuration% == LockedDeps (npm install --prefer-offline) 20 | test_script: 21 | - npm run coverage 22 | -------------------------------------------------------------------------------- /lib/reportError.js: -------------------------------------------------------------------------------- 1 | const caches = require('./caches') 2 | const { atom, console } = require('./globals') 3 | 4 | const displayed = caches.add(new Set()) 5 | 6 | function reportError (err) { 7 | console.error(err) 8 | 9 | const key = err.message 10 | if (!displayed.has(key)) { 11 | displayed.add(key) 12 | const notification = atom.notifications.addError('Standard Engine: An error occurred', { 13 | description: err.message, 14 | dismissable: true 15 | }) 16 | 17 | notification.onDidDismiss(() => displayed.delete(key)) 18 | } 19 | } 20 | 21 | module.exports = reportError 22 | -------------------------------------------------------------------------------- /test/benchmark.js: -------------------------------------------------------------------------------- 1 | const textEditorFactory = require('./util/textEditorFactory') 2 | const linter = require('../lib/register').provideLinter() 3 | const debugFactory = require('debug') 4 | 5 | const firstRun = debugFactory('first run') 6 | const secondRun = debugFactory('second run') 7 | 8 | const startTime = Date.now() 9 | 10 | firstRun('started') 11 | 12 | linter.lint(textEditorFactory('var foo = "bar"')).then(data => { 13 | firstRun('done') 14 | 15 | secondRun('started') 16 | linter.lint(textEditorFactory('var foo = "bar"')).then(data => { 17 | secondRun('done') 18 | 19 | console.log('\nTotal time spent:', Date.now() - startTime, 'ms') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2015 Gustav Nikolaj Olsen 4 | 5 | Permission to use, copy, modify, and/or distribute this software for 6 | any purpose with or without fee is hereby granted, provided that the 7 | above copyright notice and this permission notice appear in all 8 | copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 11 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 12 | WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 13 | AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 14 | DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR 15 | PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 16 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 17 | PERFORMANCE OF THIS SOFTWARE. 18 | -------------------------------------------------------------------------------- /test/util/textEditorFactory.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = function textEditorFactory (input = {}) { 4 | if (typeof input === 'string') { 5 | input = { 6 | source: input 7 | } 8 | } 9 | return { 10 | getText () { 11 | return input.source || '' 12 | }, 13 | getBuffer () { 14 | return { 15 | positionForCharacterIndex (x) { 16 | // This method is supposed to return the position as an array 17 | // [line, column] as zero indexed numbers. For now it is just stubbed 18 | // out. 19 | return [0, 0] 20 | } 21 | } 22 | }, 23 | getPath () { 24 | return input.path || path.resolve(__dirname, 'foo.js') // standard devDep will be found 25 | }, 26 | getGrammar () { 27 | return { 28 | scopeName: input.scopeName || 'source.js' 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/lib/cache.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* global describe, it */ 3 | const expect = require('unexpected').clone() 4 | 5 | const caches = require('../../lib/caches') 6 | 7 | describe('lib/caches', () => { 8 | describe('add()', () => { 9 | it('should return the added cache', () => { 10 | const obj = {} 11 | expect(caches.add(obj), 'to be', obj) 12 | }) 13 | }) 14 | 15 | describe('clearAll()', () => { 16 | it('should reset added caches', () => { 17 | let reset1 = false 18 | let cleared2 = false 19 | const cache1 = { 20 | reset () { 21 | reset1 = true 22 | } 23 | } 24 | const cache2 = { 25 | clear () { 26 | cleared2 = true 27 | } 28 | } 29 | 30 | expect(reset1, 'to be', false) 31 | expect(cleared2, 'to be', false) 32 | caches.add(cache1) 33 | caches.add(cache2) 34 | caches.clearAll() 35 | expect(reset1, 'to be', true) 36 | expect(cleared2, 'to be', true) 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /test/lib/fix.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* global describe, it */ 3 | const expect = require('unexpected').clone() 4 | const proxyquire = require('proxyquire').noPreserveCache() 5 | 6 | const textEditorFactory = require('../util/textEditorFactory') 7 | 8 | describe('lib/fix', () => { 9 | it('should get output from the linter', () => { 10 | let args 11 | const fix = proxyquire('../../lib/fix', { 12 | './linting': { 13 | fix (...actual) { 14 | args = actual 15 | return Promise.resolve(null) 16 | } 17 | } 18 | }) 19 | 20 | const editor = textEditorFactory() 21 | const reportError = () => {} 22 | 23 | fix(editor, reportError) 24 | expect(args[0], 'to be', editor) 25 | expect(args[1], 'to be', reportError) 26 | }) 27 | 28 | it('should update the text buffer if there is output', () => { 29 | const expected = 'output' 30 | const fix = proxyquire('../../lib/fix', { 31 | './linting': { 32 | fix () { 33 | return Promise.resolve(expected) 34 | } 35 | } 36 | }) 37 | 38 | const editor = textEditorFactory() 39 | let diff 40 | editor.getBuffer = () => { 41 | return { 42 | setTextViaDiff (output) { 43 | diff = output 44 | } 45 | } 46 | } 47 | 48 | return fix(editor, () => {}).then(() => expect(diff, 'to equal', expected)) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /lib/worker.js: -------------------------------------------------------------------------------- 1 | const cleanYamlObject = require('clean-yaml-object') 2 | const resolveCwd = require('resolve-cwd') 3 | 4 | const linterName = process.argv[2] 5 | let linter, loadFailure 6 | { 7 | const linterPath = resolveCwd(linterName) 8 | if (linterPath) { 9 | try { 10 | linter = require(linterPath) 11 | } catch (err) { 12 | loadFailure = { 13 | error: cleanYamlObject(err) 14 | } 15 | } 16 | } else { 17 | loadFailure = { 18 | results: [ 19 | { 20 | messages: [ 21 | { 22 | message: `Could not load linter "${linterName}"`, 23 | fatal: true 24 | } 25 | ] 26 | } 27 | ] 28 | } 29 | } 30 | } 31 | 32 | process.on('message', ({ filename, fix, id, text }) => { 33 | if (loadFailure) { 34 | process.send(Object.assign({ id }, loadFailure)) 35 | return 36 | } 37 | 38 | try { 39 | if (linter.lintTextSync) { 40 | const { results } = linter.lintTextSync(text, { filename, fix }) 41 | process.send({ id, results }) 42 | } else { 43 | linter.lintText(text, { filename, fix }, (err, { results } = {}) => { 44 | if (err) { 45 | process.send({ 46 | error: cleanYamlObject(err), 47 | id 48 | }) 49 | } else { 50 | process.send({ id, results }) 51 | } 52 | }) 53 | } 54 | } catch (err) { 55 | process.send({ 56 | error: cleanYamlObject(err), 57 | id 58 | }) 59 | } 60 | }) 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linter-js-standard-engine", 3 | "version": "2.3.0", 4 | "description": "Linter plugin for Standard Engine based linters.", 5 | "main": "lib/register.js", 6 | "repository": "https://github.com/gustavnikolaj/linter-js-standard-engine.git", 7 | "author": "Gustav Nikolaj ", 8 | "license": "ISC", 9 | "bugs": { 10 | "url": "https://github.com/gustavnikolaj/linter-js-standard-engine/issues" 11 | }, 12 | "engines": { 13 | "atom": ">=1.22.0" 14 | }, 15 | "files": [ 16 | "lib" 17 | ], 18 | "scripts": { 19 | "lint": "standard", 20 | "test": "mocha test/**/*.spec.js", 21 | "posttest": "standard", 22 | "coverage": "nyc npm test" 23 | }, 24 | "homepage": "https://github.com/gustavnikolaj/linter-js-standard-engine#readme", 25 | "devDependencies": { 26 | "coveralls": "^2.11.14", 27 | "debug": "2.2.0", 28 | "mocha": "^3.1.2", 29 | "nyc": "^8.3.2", 30 | "proxyquire": "^1.7.10", 31 | "standard": "^8.5.0", 32 | "unexpected": "^10.18.1", 33 | "unique-temp-dir": "^1.0.0" 34 | }, 35 | "standard": { 36 | "globals": [ 37 | "atom" 38 | ], 39 | "ignore": [ 40 | "test/fixtures/faked/*.js" 41 | ] 42 | }, 43 | "providedServices": { 44 | "linter": { 45 | "versions": { 46 | "2.0.0": "provideLinter" 47 | } 48 | } 49 | }, 50 | "dependencies": { 51 | "clean-yaml-object": "^0.1.0", 52 | "es6-error": "^4.0.1", 53 | "find-up": "^2.1.0", 54 | "lru-cache": "^4.0.1", 55 | "minimatch": "^3.0.3", 56 | "read-pkg": "^2.0.0", 57 | "resolve-cwd": "^1.0.0" 58 | }, 59 | "nyc": { 60 | "cache": true, 61 | "reporter": [ 62 | "html", 63 | "lcov", 64 | "text" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/register.js: -------------------------------------------------------------------------------- 1 | const caches = require('./caches') 2 | const commands = require('./commands') 3 | const optInManager = require('./optInManager') 4 | const reportError = require('./reportError') 5 | 6 | const commandDisposable = atom.commands.add('atom-workspace', commands) 7 | 8 | let lint = (...args) => { 9 | const { lint: loaded } = require('./linting') 10 | lint = loaded 11 | return loaded(...args) 12 | } 13 | 14 | exports.config = { 15 | enabledProjects: { 16 | title: 'Enable', 17 | description: 'Control whether linting should be enabled manually, for each project, or is enabled for all projects.', 18 | type: 'number', 19 | default: optInManager.SOME, 20 | enum: [ 21 | {value: optInManager.NONE, description: 'Reset existing permissions'}, 22 | {value: optInManager.SOME, description: 'Decide for each project'}, 23 | {value: optInManager.ALL, description: 'Enable for all projects'} 24 | ] 25 | }, 26 | grammarScopes: { 27 | title: 'Scopes', 28 | description: 'Grammar scopes for which linting is enabled. Reload window for changes to take effect.', 29 | type: 'array', 30 | default: ['javascript', 'source.js', 'source.js.jsx'], 31 | items: { 32 | type: 'string' 33 | } 34 | } 35 | } 36 | 37 | exports.activate = state => { 38 | optInManager.activate(state && state.optIn) 39 | } 40 | 41 | exports.deactivate = () => { 42 | caches.clearAll() 43 | commandDisposable.dispose() 44 | optInManager.deactivate() 45 | } 46 | 47 | exports.provideLinter = () => ({ 48 | name: 'standard-engine', 49 | grammarScopes: atom.config.get('linter-js-standard-engine.grammarScopes'), 50 | scope: 'file', 51 | lintsOnChange: true, 52 | lint (textEditor) { 53 | return lint(textEditor, reportError) 54 | } 55 | }) 56 | 57 | exports.serialize = () => ({ optIn: optInManager.serialize() }) 58 | -------------------------------------------------------------------------------- /test/lib/commands.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* global describe, it */ 3 | const expect = require('unexpected').clone() 4 | const proxyquire = require('proxyquire').noPreserveCache() 5 | 6 | const textEditorFactory = require('../util/textEditorFactory') 7 | 8 | describe('lib/commands', () => { 9 | describe('fix', () => { 10 | it('should fix the current editor', () => { 11 | atom.workspace._activeTextEditor = textEditorFactory() 12 | 13 | let fixed = false 14 | let args 15 | const commands = proxyquire('../../lib/commands', { 16 | './fix' (...actual) { 17 | fixed = true 18 | args = actual 19 | } 20 | }) 21 | const reportError = require('../../lib/reportError') 22 | 23 | expect(fixed, 'to be false') 24 | commands['Standard Engine:Fix File']() 25 | expect(fixed, 'to be true') 26 | expect(args[0], 'to be', atom.workspace._activeTextEditor) 27 | expect(args[1], 'to be', reportError) 28 | }) 29 | it('should do nothing if there is no current editor', () => { 30 | atom.workspace._activeTextEditor = null 31 | 32 | let fixed = false 33 | const commands = proxyquire('../../lib/commands', { 34 | './fix' () { 35 | fixed = true 36 | } 37 | }) 38 | 39 | expect(fixed, 'to be false') 40 | commands['Standard Engine:Fix File']() 41 | expect(fixed, 'to be false') 42 | }) 43 | }) 44 | 45 | describe('restart', () => { 46 | it('should clear all caches', () => { 47 | let cleared = false 48 | const commands = proxyquire('../../lib/commands', { 49 | './caches': { 50 | clearAll () { 51 | cleared = true 52 | } 53 | } 54 | }) 55 | 56 | expect(cleared, 'to be false') 57 | commands['Standard Engine:Restart']() 58 | expect(cleared, 'to be true') 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /lib/workerManagement.js: -------------------------------------------------------------------------------- 1 | const { fork } = require('child_process') 2 | const lruCache = require('lru-cache') 3 | 4 | const caches = require('./caches') 5 | const workerPath = require.resolve('./worker') 6 | 7 | const workers = caches.add(lruCache({ 8 | max: 2, 9 | dispose (_, worker) { 10 | worker.dispose() 11 | } 12 | })) 13 | 14 | function takeResolvers (pending, id) { 15 | const resolvers = pending.get(id) 16 | pending.delete(id) 17 | return resolvers 18 | } 19 | 20 | function createWorker (linterName, cwd) { 21 | const worker = fork(workerPath, [linterName], { cwd }) 22 | 23 | let sequenceNumber = 0 24 | const pendingResolvers = new Map() 25 | 26 | worker.on('message', ({ id, error, results }) => { 27 | const resolvers = takeResolvers(pendingResolvers, id) 28 | if (!resolvers) return 29 | 30 | if (error) { 31 | Object.setPrototypeOf(error, Error.prototype) 32 | resolvers.reject(error) 33 | } else { 34 | resolvers.resolve(results) 35 | } 36 | }) 37 | 38 | return { 39 | lint (filename, text, fix) { 40 | const id = ++sequenceNumber 41 | return new Promise((resolve, reject) => { 42 | pendingResolvers.set(id, { resolve, reject }) 43 | worker.send({ filename, fix, id, text }) 44 | }) 45 | }, 46 | 47 | dispose () { 48 | // Ignore errors thrown, since this method may be called when the worker 49 | // is removed from the cache even if it has already exited. 50 | try { 51 | worker.disconnect() 52 | } catch (err) {} 53 | }, 54 | 55 | disposed: new Promise(resolve => worker.once('exit', resolve)) 56 | } 57 | } 58 | 59 | function getWorker (linterName, projectRoot) { 60 | const cacheKey = linterName + '\n' + projectRoot 61 | if (workers.has(cacheKey)) return workers.get(cacheKey) 62 | 63 | const worker = createWorker(linterName, projectRoot) 64 | workers.set(cacheKey, worker) 65 | worker.disposed.then(() => { 66 | // A new worker may have been created in the meantime, make sure not to 67 | // delete that one. 68 | if (workers.peek(cacheKey) === worker) { 69 | workers.del(cacheKey) 70 | } 71 | }) 72 | 73 | return worker 74 | } 75 | 76 | exports.getWorker = getWorker 77 | -------------------------------------------------------------------------------- /test/lib/reportError.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* global describe, it */ 3 | const expect = require('unexpected').clone() 4 | const proxyquire = require('proxyquire').noPreserveCache() 5 | 6 | const caches = require('../../lib/caches') 7 | 8 | let lastError 9 | const reportError = proxyquire('../../lib/reportError', { 10 | './globals': { 11 | atom, 12 | console: { 13 | error (err) { 14 | lastError = err 15 | } 16 | } 17 | } 18 | }) 19 | 20 | describe('lib/reportError', () => { 21 | it('should add a notification', () => { 22 | atom.notifications._errors = [] 23 | reportError(new Error('This is the message')) 24 | 25 | expect(atom.notifications._errors, 'to have length', 1) 26 | 27 | const [ notification ] = atom.notifications._errors 28 | expect(notification, 'to satisfy', { 29 | message: 'Standard Engine: An error occurred', 30 | options: { 31 | description: 'This is the message', 32 | dismissable: true 33 | } 34 | }) 35 | }) 36 | it('should write to the console', () => { 37 | const expected = new Error('This is the message') 38 | reportError(expected) 39 | expect(lastError, 'to be', expected) 40 | 41 | lastError = null 42 | reportError(expected) 43 | expect(lastError, 'to be', expected) 44 | }) 45 | it('should not add duplicate notifications, based on message', () => { 46 | atom.notifications._errors = [] 47 | caches.clearAll() 48 | reportError(new Error('This is the message')) 49 | 50 | expect(atom.notifications._errors, 'to have length', 1) 51 | }) 52 | it('should add duplicate notifications once the previous one has been dismissed', () => { 53 | atom.notifications._errors = [] 54 | caches.clearAll() 55 | reportError(new Error('This is the message')) 56 | 57 | expect(atom.notifications._errors, 'to have length', 1) 58 | atom.notifications._errors[0]._dismiss() 59 | 60 | reportError(new Error('This is the message')) 61 | expect(atom.notifications._errors, 'to have length', 2) 62 | }) 63 | it('should add duplicate notifications after caches have been cleared', () => { 64 | atom.notifications._errors = [] 65 | caches.clearAll() 66 | reportError(new Error('This is the message')) 67 | 68 | expect(atom.notifications._errors, 'to have length', 1) 69 | caches.clearAll() 70 | 71 | reportError(new Error('This is the message')) 72 | expect(atom.notifications._errors, 'to have length', 2) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /lib/findOptions.js: -------------------------------------------------------------------------------- 1 | const {dirname, join} = require('path') 2 | 3 | const ExtendableError = require('es6-error') 4 | const findUp = require('find-up') 5 | const readPkg = require('read-pkg') 6 | 7 | const caches = require('./caches') 8 | 9 | const KNOWN_LINTERS = new Set([ 10 | 'happiness', 11 | 'onelint', 12 | 'semistandard', 13 | 'standard', 14 | 'uber-standard' 15 | ]) 16 | 17 | class MissingLinterError extends ExtendableError { 18 | constructor (message = 'No supported linter found') { 19 | super(message) 20 | } 21 | } 22 | 23 | class MissingPackageError extends ExtendableError { 24 | constructor (message = 'No package.json found') { 25 | super(message) 26 | } 27 | } 28 | 29 | const resolvedPackages = caches.add(new Map()) 30 | const computedOptions = caches.add(new Map()) 31 | 32 | function resolvePackage (dir) { 33 | if (resolvedPackages.has(dir)) return resolvedPackages.get(dir) 34 | 35 | const promise = findUp('package.json', {cwd: dir}).then(packagePath => { 36 | if (!packagePath) return null 37 | 38 | return readPkg(packagePath).then(pkg => { 39 | const stanza = pkg['standard-engine'] 40 | if (stanza && stanza.skip === true) { 41 | // Resolve a parent `package.json` file instead. 42 | return resolvePackage(join(dirname(packagePath), '..')) 43 | } 44 | 45 | return {packagePath, pkg} 46 | }) 47 | }) 48 | resolvedPackages.set(dir, promise) 49 | promise.then(packagePath => { 50 | if (!packagePath && resolvedPackages.get(dir) === promise) { 51 | resolvedPackages.delete(dir) 52 | } 53 | }) 54 | return promise 55 | } 56 | 57 | function getOptions ({packagePath, pkg}) { 58 | if (computedOptions.has(packagePath)) return computedOptions.get(packagePath) 59 | 60 | const stanza = pkg['standard-engine'] 61 | let linterName = !stanza || typeof stanza === 'string' 62 | ? stanza 63 | : stanza.name 64 | 65 | // Try to find default linterName 66 | if (!linterName && pkg.devDependencies) { 67 | for (const dep of Object.keys(pkg.devDependencies)) { 68 | if (KNOWN_LINTERS.has(dep)) { 69 | linterName = dep 70 | break 71 | } 72 | } 73 | } 74 | 75 | if (!linterName) throw new MissingLinterError() 76 | 77 | // Support scoped packages. Assume their `cmd` (and thus options key) is 78 | // configured as the package name *without* the scope prefix. 79 | let optionsKey = linterName 80 | if (optionsKey.includes('/')) { 81 | optionsKey = optionsKey.split('/')[1] 82 | } 83 | 84 | const options = pkg[optionsKey] || {} 85 | const {ignore: ignoreGlobs = []} = options 86 | 87 | const projectRoot = dirname(packagePath) 88 | const retval = {ignoreGlobs, linterName, projectRoot} 89 | computedOptions.set(packagePath, retval) 90 | return retval 91 | } 92 | 93 | function findOptions (file) { 94 | return resolvePackage(dirname(file)) 95 | .then(resolved => { 96 | if (!resolved) throw new MissingPackageError() 97 | 98 | return getOptions(resolved) 99 | }) 100 | } 101 | 102 | exports = module.exports = findOptions 103 | 104 | exports.MissingLinterError = MissingLinterError 105 | exports.MissingPackageError = MissingPackageError 106 | -------------------------------------------------------------------------------- /lib/optInManager.js: -------------------------------------------------------------------------------- 1 | const { createHash, randomBytes } = require('crypto') 2 | const caches = require('./caches') 3 | 4 | const NONE = 1 5 | exports.NONE = NONE 6 | 7 | const SOME = 2 8 | exports.SOME = SOME 9 | 10 | const ALL = 3 11 | exports.ALL = ALL 12 | 13 | class State { 14 | constructor (allowAll, { seed, allowed = [] } = {}) { 15 | this.allowAll = allowAll && Promise.resolve(true) 16 | this.seed = seed ? Buffer.from(seed, 'base64') : randomBytes(16) 17 | this.allowed = new Set(allowed) 18 | 19 | this.cache = caches.add(new Map()) 20 | } 21 | 22 | dispose () { 23 | this.cache.clear() 24 | } 25 | } 26 | 27 | let state 28 | let subscription 29 | 30 | function activate (serialized) { 31 | state = new State(atom.config.get('linter-js-standard-engine.enabledProjects') === ALL, serialized) 32 | 33 | subscription = atom.config.onDidChange('linter-js-standard-engine.enabledProjects', ({newValue}) => { 34 | state.dispose() 35 | 36 | state = new State(newValue === ALL) 37 | if (newValue === NONE) { 38 | atom.config.set('linter-js-standard-engine.enabledProjects', SOME) 39 | } 40 | }) 41 | } 42 | exports.activate = activate 43 | 44 | function deactivate () { 45 | subscription.dispose() 46 | } 47 | exports.deactivate = deactivate 48 | 49 | function serialize () { 50 | return { 51 | seed: state.seed.toString('base64'), 52 | allowed: Array.from(state.allowed) 53 | } 54 | } 55 | exports.serialize = serialize 56 | 57 | function checkPermission (linterName, projectRoot) { 58 | if (state.allowAll) return state.allowAll 59 | 60 | const {cache} = state 61 | const cacheKey = `${linterName}\n${projectRoot}` 62 | if (cache.has(cacheKey)) return cache.get(cacheKey) 63 | 64 | // Hash the cache key, no need to store project paths and linter names in the serialized state. 65 | const stateKey = createHash('sha256') 66 | .update(state.seed) 67 | .update(cacheKey) 68 | .digest('base64') 69 | 70 | if (state.allowed.has(stateKey)) { 71 | const promise = Promise.resolve(true) 72 | cache.set(cacheKey, promise) 73 | return promise 74 | } 75 | 76 | const promise = new Promise(resolve => { 77 | let dismissedViaButton = false 78 | 79 | const notification = atom.notifications.addInfo('Enable linter?', { 80 | buttons: [ 81 | { 82 | text: 'This project only', 83 | onDidClick () { 84 | state.allowed.add(stateKey) 85 | resolve(true) 86 | dismissedViaButton = true 87 | notification.dismiss() 88 | } 89 | }, 90 | { 91 | text: 'Run any linter for this and all future projects', 92 | onDidClick () { 93 | atom.config.set('linter-js-standard-engine.enabledProjects', ALL) 94 | resolve(true) 95 | dismissedViaButton = true 96 | notification.dismiss() 97 | } 98 | } 99 | ], 100 | description: `Do you want to run \`${linterName}\`, as provided by \`${projectRoot}\`?`, 101 | dismissable: true 102 | }) 103 | 104 | notification.onDidDismiss(() => { 105 | if (dismissedViaButton) return 106 | 107 | cache.delete(cacheKey) 108 | resolve(false) 109 | }) 110 | }) 111 | cache.set(cacheKey, promise) 112 | return promise 113 | } 114 | exports.checkPermission = checkPermission 115 | -------------------------------------------------------------------------------- /test/util/atomHelper.js: -------------------------------------------------------------------------------- 1 | if (typeof global.atom === 'undefined') { 2 | global.atom = { 3 | commands: { 4 | _commands: {}, 5 | 6 | add (target, ...args) { 7 | if (args.length === 1) { 8 | const [commands] = args 9 | const disposables = [] 10 | for (const name in commands) { 11 | disposables.push(this.add(target, name, commands[name])) 12 | } 13 | 14 | return { 15 | dispose () { 16 | for (const disposable of disposables) { 17 | disposable.dispose() 18 | } 19 | } 20 | } 21 | } 22 | 23 | const [name, callback] = args 24 | const obj = { name, callback, disposed: false } 25 | ;(this._commands[target] || (this._commands[target] = [])).push(obj) 26 | return { 27 | dispose () { 28 | obj.disposed = true 29 | } 30 | } 31 | } 32 | }, 33 | 34 | config: { 35 | _callbacks: new Map(), 36 | _map: new Map(), 37 | 38 | get (key) { 39 | if (key === 'linter-js-standard-engine.grammarScopes') return ['javascript', 'source.js', 'source.js.jsx'] 40 | return this._map.get(key) 41 | }, 42 | 43 | set (key, value) { 44 | this._map.set(key, value) 45 | }, 46 | 47 | onDidChange (key, callback) { 48 | if (!this._callbacks.has(key)) { 49 | this._callbacks.set(key, new Set()) 50 | } 51 | const set = this._callbacks.get(key) 52 | set.add(callback) 53 | return { 54 | dispose () { 55 | set.delete(callback) 56 | } 57 | } 58 | } 59 | }, 60 | 61 | notifications: { 62 | _errors: [], 63 | _enableLinters: null, 64 | 65 | addError (message, options) { 66 | const obj = { 67 | message, 68 | options, 69 | _callbacks: new Set(), 70 | _dismiss () { 71 | for (const cb of this._callbacks) { 72 | cb() 73 | } 74 | } 75 | } 76 | this._errors.push(obj) 77 | 78 | return { 79 | onDidDismiss (callback) { 80 | obj._callbacks.add(callback) 81 | } 82 | } 83 | }, 84 | 85 | addInfo (message, options) { 86 | let obj 87 | 88 | if (message === 'Enable linter?') { 89 | if (this._enableLinters) { 90 | obj = { 91 | options, 92 | _callbacks: new Set(), 93 | _dismiss () { 94 | for (const cb of this._callbacks) { 95 | cb() 96 | } 97 | } 98 | } 99 | 100 | this._enableLinters.push(obj) 101 | } else { 102 | setImmediate(() => { 103 | (options.buttons[0].onDidClick)() 104 | }) 105 | } 106 | } 107 | 108 | return { 109 | dismiss () { 110 | if (obj) { 111 | obj._dismiss() 112 | } 113 | }, 114 | onDidDismiss (callback) { 115 | if (obj) { 116 | obj._callbacks.add(callback) 117 | } 118 | } 119 | } 120 | } 121 | }, 122 | 123 | workspace: { 124 | _activeTextEditor: null, 125 | 126 | getActiveTextEditor () { 127 | return this._activeTextEditor 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/linting.js: -------------------------------------------------------------------------------- 1 | const { relative } = require('path') 2 | 3 | const ExtendableError = require('es6-error') 4 | const minimatch = require('minimatch') 5 | 6 | const findOptions = require('./findOptions') 7 | const { checkPermission } = require('./optInManager') 8 | const { getWorker } = require('./workerManagement') 9 | 10 | const GRAMMAR_SCOPES = atom.config.get('linter-js-standard-engine.grammarScopes') 11 | 12 | function getRange (line = 1, column = 1, source = '') { 13 | const line0 = line - 1 14 | const column0 = column - 1 15 | 16 | const end = [line0, column0] 17 | const start = [line0, column0 - source.slice(0, column0).trim().length] 18 | return [start, end] 19 | } 20 | 21 | function suppressError (err) { 22 | return err.name === 'MissingLinterError' || err.name === 'MissingPackageError' 23 | } 24 | 25 | class InvalidReportError extends ExtendableError { 26 | constructor (message = 'Invalid lint report') { 27 | super(message) 28 | } 29 | } 30 | 31 | function getReport (textEditor, fix) { 32 | const { scopeName } = textEditor.getGrammar() 33 | if (!GRAMMAR_SCOPES.includes(scopeName)) return Promise.resolve(null) 34 | 35 | // Get text at the time the linter was invoked. 36 | const fileContent = textEditor.getText() 37 | const filePath = textEditor.getPath() 38 | 39 | return findOptions(filePath) 40 | .then(({ linterName, projectRoot, ignoreGlobs }) => { 41 | const relativePath = relative(projectRoot, filePath) 42 | const fileIsIgnored = ignoreGlobs.some(pattern => minimatch(relativePath, pattern)) 43 | if (fileIsIgnored) return null 44 | 45 | return checkPermission(linterName, projectRoot) 46 | .then(allowed => { 47 | if (!allowed) return null 48 | 49 | const worker = getWorker(linterName, projectRoot) 50 | return worker.lint(filePath, fileContent, fix) 51 | .then(([{ messages, output } = {}]) => { 52 | if (!Array.isArray(messages)) throw new InvalidReportError() 53 | return { filePath, messages, output } 54 | }) 55 | }) 56 | }) 57 | } 58 | 59 | function fix (textEditor, reportError) { 60 | return getReport(textEditor, true) 61 | .then(report => report && report.output) 62 | .catch(err => { 63 | if (!suppressError(err)) reportError(err) 64 | 65 | return null 66 | }) 67 | } 68 | 69 | function lint (textEditor, reportError) { 70 | return getReport(textEditor, false) 71 | .then(report => { 72 | if (!report) return [] 73 | 74 | const { filePath } = report 75 | return report.messages.map(({ message: excerpt, line, column, severity, source, fix }) => { 76 | const result = { 77 | severity: severity === 2 ? 'error' : 'warning', 78 | excerpt, 79 | location: { 80 | file: filePath, 81 | position: getRange(line, column, source) 82 | } 83 | } 84 | 85 | if (fix) { 86 | result.solutions = [ 87 | { 88 | position: [ 89 | textEditor.getBuffer().positionForCharacterIndex(fix.range[0]), 90 | textEditor.getBuffer().positionForCharacterIndex(fix.range[1]) 91 | ], 92 | replaceWith: fix.text 93 | } 94 | ] 95 | } 96 | 97 | return result 98 | }) 99 | }) 100 | .catch(err => { 101 | if (!suppressError(err)) reportError(err) 102 | 103 | return [] 104 | }) 105 | } 106 | 107 | exports.fix = fix 108 | exports.lint = lint 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # linter-js-standard-engine 2 | 3 | [![Build Status](https://travis-ci.org/gustavnikolaj/linter-js-standard-engine.svg?branch=master)](https://travis-ci.org/gustavnikolaj/linter-js-standard-engine) 4 | [![Build status](https://ci.appveyor.com/api/projects/status/ce33sbafvyhp9ovn?svg=true)](https://ci.appveyor.com/project/gustavnikolaj/linter-js-standard-engine) 5 | [![Coverage Status](https://coveralls.io/repos/github/gustavnikolaj/linter-js-standard-engine/badge.svg?branch=master)](https://coveralls.io/github/gustavnikolaj/linter-js-standard-engine?branch=master) 6 | 7 | A linter for GitHub's [Atom Editor](https://atom.io) using the [Linter 8 | Plugin](https://github.com/atom-community/linter), for use with the 9 | [`standard`](https://github.com/feross/standard) linter, and other linters based 10 | on [`standard-engine`](https://github.com/flet/standard-engine). 11 | 12 | ## Installation 13 | 14 | ```console 15 | $ apm install linter-js-standard-engine 16 | ``` 17 | 18 | Or, Settings → Install → Search for `linter-js-standard-engine`. 19 | 20 | ## Usage 21 | 22 | `linter-js-standard-engine` recognizes the following linters if they're present 23 | in the `devDependencies` of your `package.json` file: 24 | 25 | * `happiness` 26 | * `onelint` 27 | * `semistandard` 28 | * `standard` 29 | * `uber-standard` 30 | 31 | For example: 32 | 33 | ```json 34 | { 35 | "devDependencies": { 36 | "standard": "*" 37 | } 38 | } 39 | ``` 40 | 41 | Additionally you can specify what linter to use using by setting 42 | `standard-engine` in your `package.json` file: 43 | 44 | ```json 45 | { 46 | "standard-engine": "@novemberborn/as-i-preach" 47 | } 48 | ``` 49 | 50 | The value must be a reference to a Node.js module that implements 51 | `standard-engine`. The above example is for 52 | [`@novemberborn/as-i-preach`](https://github.com/novemberborn/as-i-preach). 53 | 54 | When set, the `standard-engine` value takes precedence over any other linters 55 | discovered in the `devDependencies`. 56 | 57 | The `package.json` file is discovered by walking up the file system, starting at 58 | the file being linted. The first `package.json` file found is the one that's 59 | used. The linter is invoked with its working directory set to the directory the 60 | `package.json` file is in. The `package.json` file's location and contents are 61 | cached. 62 | 63 | If you have a project setup with multiple `package.json` files you can tell 64 | `linter-js-standard-engine` to ignore nested `package.json` files and keep 65 | walking up the file system: 66 | 67 | ```json 68 | { 69 | "standard-engine": { 70 | "skip": true 71 | } 72 | } 73 | ``` 74 | 75 | This object format can also be used to specify the linter: 76 | 77 | ```json 78 | { 79 | "standard-engine": { 80 | "name": "@novemberborn/as-i-preach" 81 | } 82 | } 83 | ``` 84 | 85 | ### Commands 86 | 87 | #### Fix File 88 | 89 | Run `Standard Engine: Fix File` to automatically fix linting issues. 90 | 91 | #### Restart 92 | 93 | Run `Standard Engine: Restart` to clear internal caches and restart linters. 94 | 95 | ## What about `linter-js-standard`? 96 | 97 | If you're using `standard`, `semistandard` or `happiness` then you could use 98 | another package called 99 | [`linter-js-standard`](https://github.com/ricardofbarros/linter-js-standard). 100 | 101 | The advantage of using `linter-js-standard-engine` however is that it utilizes 102 | the linter that is installed in your project, rather than bundling a particular 103 | linter version. We also support more linters, including any custom linter that's 104 | based on `standard-engine`. 105 | 106 | ## License 107 | 108 | This module is made public under the ISC License. 109 | 110 | See the 111 | [LICENSE](https://github.com/gustavnikolaj/linter-js-standard-engine/blob/master/LICENSE) 112 | file for additional details. 113 | -------------------------------------------------------------------------------- /test/lib/workerManagement.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* global describe, it */ 3 | 4 | const expect = require('unexpected') 5 | const proxyquire = require('proxyquire').noPreserveCache() 6 | const EventEmitter = require('events') 7 | const caches = require('../../lib/caches') 8 | 9 | const forks = {} 10 | 11 | class Child extends EventEmitter { 12 | constructor () { 13 | super() 14 | this.wasDisconnected = false 15 | } 16 | 17 | send (cmd) { 18 | this.emit('send', cmd) 19 | } 20 | 21 | disconnect () { 22 | this.wasDisconnected = true 23 | } 24 | } 25 | 26 | const workerManagement = proxyquire('../../lib/workerManagement', { 27 | 'child_process': { 28 | fork (workerPath, args, opts) { 29 | const name = args[0] 30 | const child = new Child() 31 | forks[name] = child 32 | return child 33 | } 34 | } 35 | }) 36 | 37 | describe('lib/workerManagement', () => { 38 | it('should shut down least recently used workers', () => { 39 | workerManagement.getWorker('first', '/') 40 | workerManagement.getWorker('second', '/') 41 | expect(forks, 'to satisfy', { 42 | first: { 43 | wasDisconnected: false 44 | }, 45 | second: { 46 | wasDisconnected: false 47 | } 48 | }) 49 | 50 | workerManagement.getWorker('third', '/') 51 | expect(forks, 'to have key', 'third') 52 | expect(forks, 'to satisfy', { 53 | first: { 54 | wasDisconnected: true 55 | }, 56 | second: { 57 | wasDisconnected: false 58 | }, 59 | third: { 60 | wasDisconnected: false 61 | } 62 | }) 63 | }) 64 | it('should forward errors to the lint() promise', () => { 65 | const worker = workerManagement.getWorker('linter', '/') 66 | const expected = 'error' 67 | forks.linter.on('send', cmd => { 68 | forks.linter.emit('message', { id: cmd.id, error: { message: expected } }) 69 | }) 70 | 71 | return expect(worker.lint('', {}), 'to be rejected').then(actual => { 72 | expect(actual, 'to be a', Error) 73 | expect(actual, 'to have message', expected) 74 | }) 75 | }) 76 | it('should ignore unexpected messages from the worker', () => { 77 | workerManagement.getWorker('linter', '/') 78 | forks.linter.emit('message', { id: 10 }) 79 | }) 80 | it('should clean up linters that exit', () => { 81 | const foo = workerManagement.getWorker('first', '/') 82 | expect(workerManagement.getWorker('first', '/'), 'to be', foo) 83 | forks.first.emit('exit') 84 | return new Promise(resolve => setTimeout(resolve, 10)) 85 | .then(() => { 86 | const bar = workerManagement.getWorker('first', '/') 87 | expect(bar, 'not to be', foo) 88 | 89 | // Ensure first is purged from the cache 90 | workerManagement.getWorker('second', '/') 91 | workerManagement.getWorker('third', '/') 92 | 93 | const child = forks.first 94 | expect(child, 'to have property', 'wasDisconnected', true) 95 | const baz = workerManagement.getWorker('first', '/') 96 | 97 | child.emit('exit') 98 | return new Promise(resolve => setTimeout(resolve, 10)) 99 | .then(() => { 100 | expect(workerManagement.getWorker('first', '/'), 'to be', baz) 101 | }) 102 | }) 103 | }) 104 | it('should ignore disconnect errors', () => { 105 | const worker = workerManagement.getWorker('first', '/') 106 | forks.first.disconnect = () => { throw new Error('ignore me') } 107 | expect(() => worker.dispose(), 'not to throw') 108 | }) 109 | 110 | describe('clearing all caches', () => { 111 | it('should shut down all workers', () => { 112 | // Reset state 113 | caches.clearAll() 114 | 115 | workerManagement.getWorker('first', '/') 116 | workerManagement.getWorker('second', '/') 117 | expect(forks, 'to satisfy', { 118 | first: { 119 | wasDisconnected: false 120 | }, 121 | second: { 122 | wasDisconnected: false 123 | } 124 | }) 125 | 126 | caches.clearAll() 127 | expect(forks, 'to satisfy', { 128 | first: { 129 | wasDisconnected: true 130 | }, 131 | second: { 132 | wasDisconnected: true 133 | } 134 | }) 135 | }) 136 | }) 137 | }) 138 | -------------------------------------------------------------------------------- /test/lib/findOptions.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | const path = require('path') 4 | 5 | const findUp = require('find-up') 6 | const proxyquire = require('proxyquire').noPreserveCache() 7 | const readPkg = require('read-pkg') 8 | const expect = require('unexpected') 9 | const uniqueTempDir = require('unique-temp-dir') 10 | 11 | const caches = require('../../lib/caches') 12 | const findOptions = require('../../lib/findOptions') 13 | 14 | function fixturesPath (relativePath) { 15 | return path.resolve(__dirname, '../fixtures', relativePath) 16 | } 17 | 18 | describe('lib/findOptions', () => { 19 | it('should be able to find options about this module', () => expect(findOptions(__filename), 'to be fulfilled').then(options => expect(options, 'to satisfy', { 20 | linterName: 'standard', 21 | ignoreGlobs: ['test/fixtures/faked/*.js'] 22 | }))) 23 | it('should be able to find semistandard listed as devDependency', () => { 24 | const file = fixturesPath('simpleSemiStandard/index.js') 25 | return expect(findOptions(file), 'to be fulfilled').then(options => expect(options, 'to equal', { 26 | projectRoot: fixturesPath('simpleSemiStandard'), 27 | linterName: 'semistandard', 28 | ignoreGlobs: [] 29 | })) 30 | }) 31 | it('should be able to find a linter from the standard-engine package.json key', () => { 32 | const file = fixturesPath('standardEngineKey/index.js') 33 | return expect(findOptions(file), 'to be fulfilled').then(options => expect(options, 'to equal', { 34 | projectRoot: fixturesPath('standardEngineKey'), 35 | linterName: 'my-linter', 36 | ignoreGlobs: [] 37 | })) 38 | }) 39 | it('should read config for scoped linters', () => { 40 | const file = fixturesPath('scopedLinter/index.js') 41 | return expect(findOptions(file), 'to be fulfilled').then(options => expect(options, 'to equal', { 42 | projectRoot: fixturesPath('scopedLinter'), 43 | linterName: '@my-scope/my-linter', 44 | ignoreGlobs: ['world'] 45 | })) 46 | }) 47 | it('should not select a linter for a project with no linter', () => { 48 | const file = fixturesPath('noStandardEngine/index.js') 49 | return expect(findOptions(file), 'to be rejected').then(msg => expect(msg, 'to satisfy', 'No supported linter found')) 50 | }) 51 | it('should fail when package.json cannot be found', () => { 52 | // Outside of this directory, presumably without a package.json from there to the filesystem root. 53 | const dir = uniqueTempDir({ create: true }) 54 | const file = path.join(dir, 'file.js') 55 | return expect(findOptions(file), 'to be rejected').then(msg => { 56 | return expect(msg, 'to satisfy', 'No package.json found') 57 | }) 58 | }) 59 | it('should walk up the tree if a package.json is found stating it\'s not the projects root', () => { 60 | const file = fixturesPath('skip/submodule/index.js') 61 | return expect(findOptions(file), 'to be fulfilled').then(options => expect(options, 'to equal', { 62 | projectRoot: fixturesPath('skip'), 63 | linterName: 'my-linter', 64 | ignoreGlobs: [] 65 | })) 66 | }) 67 | 68 | describe('caching', () => { 69 | const findCwds = [] 70 | const readPkgs = [] 71 | const findOptions = proxyquire('../../lib/findOptions', { 72 | 'find-up' (name, { cwd }) { 73 | findCwds.push(cwd) 74 | return findUp(name, { cwd }) 75 | }, 76 | 'read-pkg' (path) { 77 | readPkgs.push(path) 78 | return readPkg(path) 79 | } 80 | }) 81 | 82 | it('should cache results for the same file', () => { 83 | return findOptions(__filename).then(options => { 84 | return expect(findOptions(__filename), 'to be fulfilled').then(cached => expect(cached, 'to be', options)) 85 | }) 86 | }) 87 | it('should cache results for files in the same directory', () => { 88 | return findOptions(path.join(__dirname, 'lint.spec.js')).then(options => { 89 | return expect(findOptions(__filename), 'to be fulfilled').then(cached => expect(cached, 'to be', options)) 90 | }) 91 | }) 92 | 93 | describe('clearing all caches', () => { 94 | it('should lead to fresh options', () => { 95 | return findOptions(__filename).then(options => { 96 | caches.clearAll() 97 | return expect(findOptions(__filename), 'to be fulfilled').then(fresh => expect(fresh, 'not to be', options)) 98 | }) 99 | }) 100 | }) 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /test/lib/register.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* global describe, it */ 3 | 4 | const fs = require('fs') 5 | const path = require('path') 6 | 7 | const expect = require('unexpected').clone() 8 | const proxyquire = require('proxyquire').noPreserveCache() 9 | 10 | const plugin = proxyquire('../../lib/register', {}) 11 | 12 | const textEditorFactory = require('../util/textEditorFactory') 13 | 14 | const linter = plugin.provideLinter() 15 | const lint = linter.lint.bind(linter) 16 | 17 | expect.addAssertion('to be a valid lint report', (expect, subject) => expect(subject, 'to have items satisfying', { 18 | severity: expect.it('to be a string').and('not to be empty').and('to match', /^[a-z]+$/), 19 | excerpt: expect.it('to be a string').and('not to be empty'), 20 | location: expect.it('to exhaustively satisfy', { 21 | file: expect.it('to be a string').and('to match', /\.js$/), 22 | position: expect.it('to have items satisfying', 'to have items satisfying', 'to be a number') 23 | }) 24 | })) 25 | 26 | describe('linter-js-standard-engine', () => { 27 | plugin.activate() 28 | 29 | it('should be able to lint a test file', () => { 30 | const textEditor = textEditorFactory('var foo = "bar"') 31 | return expect(lint(textEditor), 'to be fulfilled').then(data => expect(data, 'to be a valid lint report')) 32 | }) 33 | it('should be able to lint a test file', () => { 34 | const textEditor = textEditorFactory({ 35 | source: 'var foo = "bar"', 36 | path: path.resolve(__dirname, '..', 'fixtures', 'faked', 'foo.js') 37 | }) 38 | return expect(lint(textEditor), 'to be fulfilled').then(data => expect(data, 'to be empty')) 39 | }) 40 | it('should skip files with the wrong scope', () => { 41 | const textEditor = textEditorFactory({ source: '# markdown', scopeName: 'source.gfm' }) 42 | return expect(lint(textEditor), 'to be fulfilled').then(data => { 43 | expect(data, 'to be empty') 44 | }) 45 | }) 46 | it('should not skip any files if the ignore option is not set', () => { 47 | const filePath = path.resolve(__dirname, '..', 'fixtures', 'simpleSemiStandard', 'index.js') 48 | const textEditor = textEditorFactory({ 49 | source: fs.readFileSync(filePath), 50 | path: filePath 51 | }) 52 | return expect(lint(textEditor), 'to be fulfilled').then(data => expect(data, 'to be empty')) 53 | }) 54 | it('should clear all caches when deactivated', () => { 55 | let cleared = false 56 | const plugin = proxyquire('../../lib/register', { 57 | './caches': { 58 | clearAll () { 59 | cleared = true 60 | } 61 | } 62 | }) 63 | plugin.activate() 64 | 65 | expect(cleared, 'to be false') 66 | plugin.deactivate() 67 | expect(cleared, 'to be true') 68 | }) 69 | it('should register a restart command', () => { 70 | atom.commands._commands = [] 71 | proxyquire('../../lib/register', {}) 72 | const commands = require('../../lib/commands') 73 | 74 | expect(atom.commands._commands, 'to have key', 'atom-workspace') 75 | expect(atom.commands._commands['atom-workspace'], 'to contain', { 76 | name: 'Standard Engine:Restart', 77 | callback: commands['Standard Engine:Restart'], 78 | disposed: false 79 | }) 80 | }) 81 | it('should register a fix command', () => { 82 | atom.commands._commands = [] 83 | proxyquire('../../lib/register', {}) 84 | const commands = require('../../lib/commands') 85 | 86 | expect(atom.commands._commands, 'to have key', 'atom-workspace') 87 | expect(atom.commands._commands['atom-workspace'], 'to contain', { 88 | name: 'Standard Engine:Fix File', 89 | callback: commands['Standard Engine:Fix File'], 90 | disposed: false 91 | }) 92 | }) 93 | it('should dispose commands when deactivated', () => { 94 | atom.commands._commands = [] 95 | const plugin = proxyquire('../../lib/register', {}) 96 | 97 | let states = [] 98 | for (const target in atom.commands._commands) { 99 | for (const { disposed } of atom.commands._commands[target]) { 100 | states.push(disposed) 101 | } 102 | } 103 | expect(states, 'to equal', [false, false]) 104 | 105 | plugin.deactivate() 106 | states = [] 107 | for (const target in atom.commands._commands) { 108 | for (const { disposed } of atom.commands._commands[target]) { 109 | states.push(disposed) 110 | } 111 | } 112 | expect(states, 'to equal', [true, true]) 113 | }) 114 | it('should provide an error reporter when linting', () => { 115 | let actual 116 | const { lint } = proxyquire('../../lib/register', { 117 | './linting': { 118 | lint (textEditor, reportError) { 119 | actual = reportError 120 | return new Promise(() => {}) 121 | } 122 | } 123 | }).provideLinter() 124 | 125 | lint(textEditorFactory('')) 126 | expect(actual, 'to be', require('../../lib/reportError')) 127 | }) 128 | 129 | it('should serialize the opt-in manager', () => { 130 | const expected = { foo: 'bar' } 131 | const { serialize } = proxyquire('../../lib/register', { 132 | './optInManager': { 133 | serialize: () => expected 134 | } 135 | }) 136 | 137 | expect(serialize(), 'to equal', { optIn: expected }) 138 | }) 139 | 140 | it('should activate the opt-in manager with serialized state', () => { 141 | const expected = { foo: 'bar' } 142 | let actual 143 | const { activate } = proxyquire('../../lib/register', { 144 | './optInManager': { 145 | activate (state) { 146 | actual = state 147 | } 148 | } 149 | }) 150 | 151 | activate({ optIn: expected }) 152 | expect(actual, 'to be', expected) 153 | }) 154 | }) 155 | -------------------------------------------------------------------------------- /test/lib/optInManager.spec.js: -------------------------------------------------------------------------------- 1 | /* global beforeEach, after, describe, it */ 2 | const {createHash} = require('crypto') 3 | const expect = require('unexpected').clone() 4 | 5 | const optInManager = require('../../lib/optInManager') 6 | 7 | describe('optInManager', () => { 8 | beforeEach(() => { 9 | atom.config._callbacks = new Map() 10 | atom.config._map = new Map([['linter-js-standard-engine.enabledProjects', optInManager.SOME]]) 11 | atom.notifications._enableLinters = [] 12 | optInManager.activate() 13 | }) 14 | 15 | after(() => { 16 | atom.notifications._enableLinters = null 17 | atom.config._map = new Map() 18 | atom.config._callbacks = new Map() 19 | }) 20 | 21 | describe('checkPermission()', () => { 22 | it('shows a notification', () => { 23 | optInManager.checkPermission() 24 | expect(atom.notifications._enableLinters, 'not to be empty') 25 | }) 26 | 27 | it('returns false if notification is dismissed', () => { 28 | const promise = optInManager.checkPermission() 29 | atom.notifications._enableLinters[0]._dismiss() 30 | return expect(promise, 'to be fulfilled with', false) 31 | }) 32 | 33 | it('returns true if button to allow project is clicked', () => { 34 | const promise = optInManager.checkPermission() 35 | atom.notifications._enableLinters[0].options.buttons[0].onDidClick() 36 | return expect(promise, 'to be fulfilled with', true) 37 | }) 38 | 39 | it('returns true if button to allow all projects is clicked', () => { 40 | const promise = optInManager.checkPermission() 41 | atom.notifications._enableLinters[0].options.buttons[1].onDidClick() 42 | return expect(promise, 'to be fulfilled with', true) 43 | }) 44 | 45 | it('returns true if all projects are allowed', () => { 46 | atom.config.set('linter-js-standard-engine.enabledProjects', optInManager.ALL) 47 | optInManager.activate() 48 | return expect(optInManager.checkPermission(), 'to be fulfilled with', true) 49 | }) 50 | 51 | it('updates config if all projects are allowed', () => { 52 | optInManager.checkPermission() 53 | atom.notifications._enableLinters[0].options.buttons[1].onDidClick() 54 | expect(atom.config._map.get('linter-js-standard-engine.enabledProjects'), 'to be', optInManager.ALL) 55 | }) 56 | 57 | it('dismisses notification if the first button is clicked', () => { 58 | optInManager.checkPermission() 59 | 60 | let dismissed = false 61 | atom.notifications._enableLinters[0]._callbacks.add(() => { 62 | dismissed = true 63 | }) 64 | atom.notifications._enableLinters[0].options.buttons[0].onDidClick() 65 | expect(dismissed, 'to be', true) 66 | }) 67 | 68 | it('dismisses notification if the second button is clicked', () => { 69 | optInManager.checkPermission() 70 | 71 | let dismissed = false 72 | atom.notifications._enableLinters[0]._callbacks.add(() => { 73 | dismissed = true 74 | }) 75 | atom.notifications._enableLinters[0].options.buttons[1].onDidClick() 76 | expect(dismissed, 'to be', true) 77 | }) 78 | 79 | it('caches results', () => { 80 | const promise = optInManager.checkPermission('foo', 'bar') 81 | expect(promise, 'to be', optInManager.checkPermission('foo', 'bar')) 82 | expect(promise, 'not to be', optInManager.checkPermission('foo', 'BAZ')) 83 | }) 84 | 85 | it('tracks allowed linters and projects', () => { 86 | optInManager.checkPermission('foo', 'bar') 87 | atom.notifications._enableLinters[0].options.buttons[0].onDidClick() 88 | const {allowed} = optInManager.serialize() 89 | expect(allowed, 'not to be empty') 90 | }) 91 | }) 92 | 93 | describe('activate()', () => { 94 | it('subscribes to config changes', () => { 95 | expect(atom.config._callbacks.get('linter-js-standard-engine.enabledProjects').size, 'to be', 1) 96 | }) 97 | 98 | describe('after config changes to NONE', () => { 99 | it('resets to SOME', () => { 100 | atom.config._map.delete('linter-js-standard-engine.enabledProjects') 101 | for (const cb of atom.config._callbacks.get('linter-js-standard-engine.enabledProjects')) { 102 | cb({ newValue: optInManager.NONE }) 103 | } 104 | expect(atom.config._map.get('linter-js-standard-engine.enabledProjects'), 'to be', optInManager.SOME) 105 | }) 106 | }) 107 | 108 | describe('after config changes to ALL', () => { 109 | describe('checkPermission()', () => { 110 | it('always returns true, without prompting', () => { 111 | for (const cb of atom.config._callbacks.get('linter-js-standard-engine.enabledProjects')) { 112 | cb({ newValue: optInManager.ALL }) 113 | } 114 | 115 | expect(optInManager.checkPermission(), 'to be fulfilled with', true) 116 | expect(atom.notifications._enableLinters, 'to be empty') 117 | }) 118 | }) 119 | }) 120 | 121 | describe('deserializes state, which is used by checkPermission()', () => { 122 | it('always true for an allowed linter & project, without prompting', () => { 123 | const seed = Buffer.from('decafbad', 'hex') 124 | const serialized = { 125 | seed: seed.toString('base64'), 126 | allowed: [createHash('sha256').update(seed).update('foo\nbar').digest('base64')] 127 | } 128 | optInManager.activate(serialized) 129 | 130 | expect(optInManager.checkPermission('foo', 'bar'), 'to be fulfilled with', true) 131 | expect(atom.notifications._enableLinters, 'to be empty') 132 | }) 133 | }) 134 | }) 135 | 136 | describe('deactivate()', () => { 137 | it('disposes the config change subscription', () => { 138 | optInManager.deactivate() 139 | expect(atom.config._callbacks.get('linter-js-standard-engine.enabledProjects').size, 'to be', 0) 140 | }) 141 | }) 142 | 143 | describe('serialize()', () => { 144 | const seed = Buffer.from('decafbad', 'hex') 145 | const serialized = { 146 | seed: seed.toString('base64'), 147 | allowed: [createHash('sha256').update(seed).update('foo\nbar').digest('base64')] 148 | } 149 | optInManager.activate(serialized) 150 | 151 | expect(optInManager.serialize(), 'to equal', serialized) 152 | }) 153 | }) 154 | -------------------------------------------------------------------------------- /test/lib/worker.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | const expect = require('unexpected') 4 | const childProcess = require('child_process') 5 | const path = require('path') 6 | 7 | const workerPath = require.resolve('../../lib/worker') 8 | 9 | describe('lib/worker', () => { 10 | it('should only load linter once', () => { 11 | const child = childProcess.fork(workerPath, [require.resolve('../fixtures/stubForWorker')]) 12 | const messages = [] 13 | const promise = new Promise(resolve => { 14 | child.on('message', m => { 15 | messages.push(m) 16 | if (messages.length === 2) { 17 | resolve() 18 | } 19 | }) 20 | }) 21 | 22 | child.send({ id: 1, source: '' }) 23 | child.send({ id: 2, source: '' }) 24 | 25 | return promise.then(() => { 26 | expect(messages[0].results[0], 'to have property', 'count', 1) 27 | expect(messages[1].results[0], 'to have property', 'count', 2) 28 | }) 29 | }) 30 | it('should report crashes in linter loading', () => { 31 | const child = childProcess.fork(workerPath, [require.resolve('../fixtures/stubForWorker/crashing')]) 32 | const promise = new Promise(resolve => { 33 | child.on('message', m => resolve(m)) 34 | }) 35 | 36 | child.send({ id: 1, source: '' }) 37 | 38 | return promise.then((message) => { 39 | expect(message.error, 'to have property', 'message', 'crash in linter loading') 40 | }) 41 | }) 42 | it('should report missing linters as a linter message', () => { 43 | const child = childProcess.fork(workerPath, ['non-existent-for-sure-or-so-we-hope']) 44 | const promise = new Promise(resolve => { 45 | child.on('message', m => resolve(m)) 46 | }) 47 | 48 | child.send({ id: 1, source: '' }) 49 | 50 | return promise.then((message) => { 51 | expect(message, 'to satisfy', { 52 | results: [ 53 | { 54 | messages: [ 55 | { 56 | message: 'Could not load linter "non-existent-for-sure-or-so-we-hope"', 57 | fatal: true 58 | } 59 | ] 60 | } 61 | ] 62 | }) 63 | }) 64 | }) 65 | it('should resolve linter from working directory', () => { 66 | const cwd = path.resolve(__dirname, '..', 'fixtures', 'localLinter') 67 | const child = childProcess.fork(workerPath, ['my-linter'], { cwd }) 68 | const promise = new Promise(resolve => { 69 | child.on('message', m => resolve(m)) 70 | }) 71 | 72 | child.send({ id: 1, source: '' }) 73 | 74 | return promise.then((message) => { 75 | expect(message.results[0], 'to have property', 'hello', 'world') 76 | }) 77 | }) 78 | it('should report crashes when linting', () => { 79 | const child = childProcess.fork(workerPath, [require.resolve('../fixtures/stubForWorker/crashOnLint')]) 80 | const promise = new Promise(resolve => { 81 | child.on('message', m => resolve(m)) 82 | }) 83 | 84 | child.send({ id: 1, source: '' }) 85 | 86 | return promise.then((message) => { 87 | expect(message.error, 'to have property', 'message', 'threw when linting') 88 | }) 89 | }) 90 | it('should report errors when linting', () => { 91 | const child = childProcess.fork(workerPath, [require.resolve('../fixtures/stubForWorker/errOnLint')]) 92 | const promise = new Promise(resolve => { 93 | child.on('message', m => resolve(m)) 94 | }) 95 | 96 | child.send({ id: 1, source: '' }) 97 | 98 | return promise.then((message) => { 99 | expect(message.error, 'to have property', 'message', 'err when linting') 100 | }) 101 | }) 102 | it('should serialize errors', () => { 103 | const child = childProcess.fork(workerPath, [require.resolve('../fixtures/stubForWorker/errOnLint')]) 104 | const promise = new Promise(resolve => { 105 | child.on('message', m => resolve(m)) 106 | }) 107 | 108 | child.send({ id: 1, source: '' }) 109 | 110 | return promise.then((message) => { 111 | expect(message.error, 'to have property', 'stack') 112 | expect(message.error.stack, 'to contain', `${require.resolve('../fixtures/stubForWorker/errOnLint')}:2`) 113 | }) 114 | }) 115 | }) 116 | 117 | describe('handle both async and sync lintText', () => { 118 | describe('sync', () => { 119 | it('should get a lint result', () => { 120 | const child = childProcess.fork(workerPath, [require.resolve('../fixtures/stubForWorker/lintText-sync')]) 121 | const promise = new Promise(resolve => { 122 | child.on('message', m => resolve(m)) 123 | }) 124 | 125 | child.send({ id: 1, source: '' }) 126 | 127 | return expect(promise, 'when fulfilled', 'to satisfy', { 128 | id: 1, 129 | results: [] 130 | }) 131 | }) 132 | it('should get a lint error', () => { 133 | const child = childProcess.fork(workerPath, [require.resolve('../fixtures/stubForWorker/errOnLint-sync')]) 134 | const promise = new Promise(resolve => { 135 | child.on('message', m => resolve(m)) 136 | }) 137 | 138 | child.send({ id: 1, source: '' }) 139 | 140 | return expect(promise, 'when fulfilled', 'to satisfy', { 141 | id: 1, 142 | error: { 143 | name: 'Error', 144 | message: 'err when linting' 145 | } 146 | }) 147 | }) 148 | }) 149 | describe('async', () => { 150 | it('should get a lint result', () => { 151 | const child = childProcess.fork(workerPath, [require.resolve('../fixtures/stubForWorker/lintText-async')]) 152 | const promise = new Promise(resolve => { 153 | child.on('message', m => resolve(m)) 154 | }) 155 | 156 | child.send({ id: 1, source: '' }) 157 | 158 | return expect(promise, 'when fulfilled', 'to satisfy', { 159 | id: 1, 160 | results: [] 161 | }) 162 | }) 163 | it('should get a lint error', () => { 164 | const child = childProcess.fork(workerPath, [require.resolve('../fixtures/stubForWorker/errOnLint')]) 165 | const promise = new Promise(resolve => { 166 | child.on('message', m => resolve(m)) 167 | }) 168 | 169 | child.send({ id: 1, source: '' }) 170 | 171 | return expect(promise, 'when fulfilled', 'to satisfy', { 172 | id: 1, 173 | error: { 174 | name: 'Error', 175 | message: 'err when linting' 176 | } 177 | }) 178 | }) 179 | }) 180 | }) 181 | -------------------------------------------------------------------------------- /test/lib/linting.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | 3 | const path = require('path') 4 | const expect = require('unexpected').clone() 5 | const proxyquire = require('proxyquire').noPreserveCache() 6 | const textEditorFactory = require('../util/textEditorFactory') 7 | const { MissingLinterError, MissingPackageError } = require('../../lib/findOptions') 8 | 9 | describe('lib/linting', () => { 10 | const optInManager = proxyquire('../../lib/optInManager', {}) 11 | optInManager.activate() 12 | let hasPermission = true 13 | optInManager.checkPermission = () => Promise.resolve(hasPermission) 14 | 15 | let stub 16 | const linting = proxyquire('../../lib/linting', { 17 | './optInManager': optInManager, 18 | './workerManagement': { 19 | getWorker () { 20 | return { 21 | fix (...args) { 22 | return stub(...args) 23 | }, 24 | lint (...args) { 25 | return stub(...args) 26 | } 27 | } 28 | } 29 | } 30 | }) 31 | 32 | beforeEach(() => { 33 | hasPermission = true 34 | stub = undefined 35 | }) 36 | 37 | describe('lint()', () => { 38 | it('should convert an eslint report to an atom report', () => { 39 | stub = () => Promise.resolve([ 40 | { 41 | filePath: '', 42 | messages: [ 43 | { 44 | ruleId: 'eol-last', 45 | severity: 2, 46 | message: 'Newline required at end of file but not found.', 47 | line: 1, 48 | column: 2, 49 | nodeType: 'Program', 50 | source: 'var foo = "bar"', 51 | fix: { range: [15, 15], text: '\n' } 52 | }, 53 | { 54 | ruleId: 'no-unused-vars', 55 | severity: 1, 56 | message: '"foo" is defined but never used', 57 | line: 1, 58 | column: 5, 59 | nodeType: 'Identifier', 60 | source: 'var foo = "bar"' 61 | }, 62 | { 63 | ruleId: 'quotes', 64 | severity: 2, 65 | message: 'Strings must use singlequote.', 66 | line: 1, 67 | column: 11, 68 | nodeType: 'Literal', 69 | source: 'var foo = "bar"', 70 | fix: { range: [10, 15], text: "'bar'" } 71 | }, 72 | { 73 | severity: 2, 74 | message: 'Made up message to test fallback code paths' 75 | } 76 | ], 77 | errorCount: 3, 78 | warningCount: 0 79 | } 80 | ]) 81 | 82 | const filePath = path.resolve(__dirname, '..', 'fixtures', 'file.js') 83 | const textEditor = textEditorFactory({ 84 | source: 'var foo = "bar"', 85 | path: filePath 86 | }) 87 | return expect(linting.lint(textEditor), 'to be fulfilled').then(report => expect(report, 'to equal', [ 88 | { 89 | severity: 'error', 90 | excerpt: 'Newline required at end of file but not found.', 91 | location: { 92 | file: filePath, 93 | position: [ [ 0, 0 ], [ 0, 1 ] ] 94 | }, 95 | solutions: [ 96 | { 97 | position: [ [ 0, 0 ], [ 0, 0 ] ], // Mocked out position calculation... 98 | replaceWith: '\n' 99 | } 100 | ] 101 | }, 102 | { 103 | severity: 'warning', 104 | excerpt: '"foo" is defined but never used', 105 | location: { 106 | file: filePath, 107 | position: [ [ 0, 1 ], [ 0, 4 ] ] 108 | } 109 | }, 110 | { 111 | severity: 'error', 112 | excerpt: 'Strings must use singlequote.', 113 | location: { 114 | file: filePath, 115 | position: [ [ 0, 1 ], [ 0, 10 ] ] 116 | }, 117 | solutions: [ 118 | { 119 | position: [ [ 0, 0 ], [ 0, 0 ] ], // Mocked out position calculation... 120 | replaceWith: '\'bar\'' 121 | } 122 | ] 123 | }, 124 | { 125 | severity: 'error', 126 | excerpt: 'Made up message to test fallback code paths', 127 | location: { 128 | file: filePath, 129 | position: [ [ 0, 0 ], [ 0, 0 ] ] 130 | } 131 | } 132 | ])) 133 | }) 134 | }) 135 | 136 | describe('fix()', () => { 137 | it('should return the output from an eslint report', () => { 138 | stub = () => Promise.resolve([ 139 | { 140 | filePath: '', 141 | messages: [], 142 | output: 'fixed' 143 | } 144 | ]) 145 | 146 | const filePath = path.resolve(__dirname, '..', 'fixtures', 'file.js') 147 | const textEditor = textEditorFactory({ 148 | source: 'var foo = "bar"', 149 | path: filePath 150 | }) 151 | return expect(linting.fix(textEditor), 'to be fulfilled') 152 | .then(output => expect(output, 'to equal', 'fixed')) 153 | }) 154 | }) 155 | 156 | for (const { method, emptiness, returnDescription } of [ 157 | { method: 'fix', emptiness: 'null', returnDescription: 'null' }, 158 | { method: 'lint', emptiness: 'empty', returnDescription: 'an empty array' } 159 | ]) { 160 | describe(`${method}()`, () => { 161 | it(`should return ${returnDescription} if the file is ignored`, () => { 162 | stub = () => Promise.reject(new Error('Should never be called')) 163 | const filePath = path.resolve(__dirname, '..', 'fixtures', 'scopedLinter', 'world') 164 | const textEditor = textEditorFactory({ 165 | source: 'var foo = "bar"', 166 | path: filePath 167 | }) 168 | return expect(linting[method](textEditor), 'to be fulfilled') 169 | .then(output => expect(output, `to be ${emptiness}`)) 170 | }) 171 | 172 | it(`should return ${returnDescription} if there is no permission for the linter to run`, () => { 173 | const filePath = path.resolve(__dirname, '..', 'fixtures', 'file.js') 174 | const textEditor = textEditorFactory({ 175 | source: 'var foo = "bar"', 176 | path: filePath 177 | }) 178 | hasPermission = false 179 | stub = () => Promise.resolve([ 180 | { 181 | filePath, 182 | messages: [ 183 | { 184 | ruleId: 'eol-last', 185 | severity: 2, 186 | message: 'Newline required at end of file but not found.', 187 | line: 1, 188 | column: 2, 189 | nodeType: 'Program', 190 | source: 'var foo = "bar"', 191 | fix: { range: [15, 15], text: '\n' } 192 | } 193 | ], 194 | output: 'fixed' 195 | } 196 | ]) 197 | return expect(linting[method](textEditor), 'to be fulfilled') 198 | .then(output => expect(output, `to be ${emptiness}`)) 199 | }) 200 | 201 | describe('error handling', () => { 202 | let currentError 203 | const stubbedOptions = proxyquire('../../lib/linting', { 204 | './findOptions' () { 205 | return Promise.reject(currentError) 206 | } 207 | }) 208 | 209 | let reportedError 210 | const reportError = err => { reportedError = err } 211 | 212 | for (const ErrorClass of [MissingLinterError, MissingPackageError]) { 213 | it(`should suppress "${ErrorClass.name}" errors`, () => { 214 | currentError = new ErrorClass() 215 | return expect(stubbedOptions[method](textEditorFactory('')), 'to be fulfilled') 216 | .then(data => expect(data, `to be ${emptiness}`)) 217 | }) 218 | } 219 | 220 | it('should report errors that are not suppressed', () => { 221 | currentError = new Error('do not suppress me') 222 | stub = () => Promise.reject(currentError) 223 | return expect(linting[method](textEditorFactory(''), reportError), 'to be fulfilled').then(data => { 224 | expect(data, `to be ${emptiness}`) 225 | expect(reportedError, 'to be', currentError) 226 | }) 227 | }) 228 | it('should add errors that are not suppressed with a default description', () => { 229 | currentError = new Error('') 230 | stub = () => Promise.reject(currentError) 231 | return expect(linting[method](textEditorFactory(''), reportError), 'to be fulfilled').then(data => { 232 | expect(data, `to be ${emptiness}`) 233 | expect(reportedError, 'to be', currentError) 234 | }) 235 | }) 236 | }) 237 | 238 | it('should add an error upon receiving an invalid report from the linters lintText() method', () => { 239 | stub = () => Promise.resolve([]) 240 | let reportedError 241 | const reportError = err => { reportedError = err } 242 | return expect(linting[method](textEditorFactory(''), reportError), 'to be fulfilled').then(data => { 243 | expect(data, `to be ${emptiness}`) 244 | expect(reportedError, 'to have message', 'Invalid lint report') 245 | }) 246 | }) 247 | }) 248 | } 249 | }) 250 | --------------------------------------------------------------------------------