diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a58099..34741b0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ If you would like to contribute enhancements or fixes, please do the following: Please note that modications should follow these coding guidelines: - Indent is 2 spaces. -- Code should pass coffeelint linter. +- Code should pass ESLint linter. - Vertical whitespace helps readability, don’t be afraid to use it. Thank you for helping out! diff --git a/coffeelint.json b/coffeelint.json deleted file mode 100644 index 49595dd..0000000 --- a/coffeelint.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "max_line_length": { - "level": "ignore" - } -} diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..4bf9b35 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,145 @@ +'use babel'; + +// eslint-disable-next-line import/no-extraneous-dependencies, import/extensions +import { CompositeDisposable } from 'atom'; + +// Dependencies +let helpers; +let path; +let fs; + +const applySubstitutions = (givenExecPath, projDir) => { + let execPath = givenExecPath; + const projectName = path.basename(projDir); + execPath = execPath.replace(/\$PROJECT_NAME/ig, projectName); + execPath = execPath.replace(/\$PROJECT/ig, projDir); + const paths = execPath.split(';'); + const foundPath = paths.find(testPath => fs.existsSync(testPath)); + if (foundPath) { + return foundPath; + } + return execPath; +}; + +const loadDeps = () => { + if (!helpers) { + helpers = require('atom-linter'); + } + if (!path) { + path = require('path'); + } + if (!fs) { + fs = require('fs-plus'); + } +}; + +module.exports = { + activate() { + this.idleCallbacks = new Set(); + let depsCallbackID; + const installLinterJSHintDeps = () => { + this.idleCallbacks.delete(depsCallbackID); + if (!atom.inSpecMode()) { + require('atom-package-deps').install('linter-pycodestyle'); + } + loadDeps(); + }; + depsCallbackID = window.requestIdleCallback(installLinterJSHintDeps); + this.idleCallbacks.add(depsCallbackID); + + this.subscriptions = new CompositeDisposable(); + this.subscriptions.add( + atom.config.observe('linter-pycodestyle.maxLineLength', (value) => { + this.maxLineLength = value; + }), + atom.config.observe('linter-pycodestyle.ignoreErrorCodes', (value) => { + this.ignoreCodes = value; + }), + atom.config.observe('linter-pycodestyle.convertAllErrorsToWarnings', (value) => { + this.convertAllErrorsToWarnings = value; + }), + atom.config.observe('linter-pycodestyle.executablePath', (value) => { + this.executablePath = value; + }), + atom.config.observe('linter-pycodestyle.forcedConfig', (value) => { + this.forcedConfig = value; + }), + ); + }, + + deactivate() { + this.idleCallbacks.forEach(callbackID => window.cancelIdleCallback(callbackID)); + this.idleCallbacks.clear(); + this.subscriptions.dispose(); + }, + + provideLinter() { + return { + name: 'pycodestyle', + grammarScopes: ['source.python', 'source.python.django'], + scope: 'file', + lintsOnChange: true, + lint: async (textEditor) => { + const filePath = textEditor.getPath(); + const fileContents = textEditor.getText(); + + let projectPath = atom.project.relativizePath(filePath)[0]; + if (projectPath === null) { + // Default project directory to file directory if path cannot be determined + projectPath = path.dirname(filePath); + } + + const parameters = []; + if (this.maxLineLength) { + parameters.push(`--max-line-length=${this.maxLineLength}`); + } + if (this.ignoreCodes) { + parameters.push(`--ignore=${this.ignoreCodes.join(',')}`); + } + if (this.forcedConfig) { + const forcedConfigPath = fs.normalize(applySubstitutions(this.forcedConfig, projectPath)); + parameters.push(`--config=${forcedConfigPath}`); + } + parameters.push('-'); + + loadDeps(); + + const execOpts = { + cwd: projectPath, + env: process.env, + stdin: fileContents, + ignoreExitCode: true, + }; + + const execPath = fs.normalize(applySubstitutions(this.executablePath, projectPath)); + + const results = await helpers.exec(execPath, parameters, execOpts); + + if (textEditor.getText() !== fileContents) { + // File has changed since the lint was triggered, tell Linter not to update + return null; + } + + const toReturn = []; + const regex = /stdin:(\d+):(\d+):(.*)/g; + const severity = this.convertAllErrorsToWarnings ? 'warning' : 'error'; + + let match = regex.exec(results); + while (match !== null) { + const line = Number.parseInt(match[1], 10) - 1 || 0; + const col = Number.parseInt(match[2], 10) - 1 || 0; + toReturn.push({ + severity, + excerpt: match[3].trim(), + location: { + file: filePath, + position: helpers.generateRange(textEditor, line, col), + }, + }); + match = regex.exec(results); + } + return toReturn; + }, + }; + }, +}; diff --git a/lib/main.coffee b/lib/main.coffee deleted file mode 100644 index e5805f4..0000000 --- a/lib/main.coffee +++ /dev/null @@ -1,60 +0,0 @@ -helpers = null -path = null - -# This function is from: https://atom.io/packages/linter-pylint -getProjectDir = (filePath) -> - path ?= require('path') - atomProject = atom.project.relativizePath(filePath)[0] - if atomProject == null - # Default project dirextory to file directory if path cannot be determined - return path.dirname(filePath) - return atomProject - -module.exports = - config: - executablePath: - type: 'string' - default: 'pycodestyle' - maxLineLength: - type: 'integer' - default: 0 - ignoreErrorCodes: - type: 'array' - default: [] - description: 'For a list of code visit http://pycodestyle.readthedocs.org/en/latest/intro.html#error-codes' - convertAllErrorsToWarnings: - type: 'boolean' - default: true - - activate: -> - require('atom-package-deps').install('linter-pycodestyle') - - provideLinter: -> - provider = - name: 'pycodestyle' - grammarScopes: ['source.python', 'source.python.django'] - scope: 'file' # or 'project' - lintOnFly: true # must be false for scope: 'project' - lint: (textEditor)-> - helpers ?= require('atom-linter') - filePath = textEditor.getPath() - parameters = [] - if maxLineLength = atom.config.get('linter-pycodestyle.maxLineLength') - parameters.push("--max-line-length=#{maxLineLength}") - if ignoreCodes = atom.config.get('linter-pycodestyle.ignoreErrorCodes') - parameters.push("--ignore=#{ignoreCodes.join(',')}") - parameters.push('-') - msgtype = if atom.config.get('linter-pycodestyle.convertAllErrorsToWarnings') then 'Warning' else 'Error' - return helpers.exec(atom.config.get('linter-pycodestyle.executablePath'), parameters, {cwd: getProjectDir(filePath), env: process.env, stdin: textEditor.getText(), ignoreExitCode: true}).then (result) -> - toReturn = [] - regex = /stdin:(\d+):(\d+):(.*)/g - while (match = regex.exec(result)) isnt null - line = parseInt(match[1]) or 0 - col = parseInt(match[2]) or 0 - toReturn.push({ - type: msgtype - text: match[3] - filePath - range: [[line - 1, col - 1], [line - 1, col]] - }) - return toReturn diff --git a/package.json b/package.json index db68087..937947a 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,61 @@ { "name": "linter-pycodestyle", - "main": "./lib/main", + "main": "./lib/index", "version": "2.0.2", "description": "Linter plugin for pycodestyle", "repository": "https://github.com/AtomLinter/linter-pycodestyle", "license": "MIT", + "configSchema": { + "executablePath": { + "type": "string", + "default": "pycodestyle", + "description": "Semicolon separated list of paths to a binary (e.g. `/usr/local/bin/pycodestyle`). Use `$PROJECT` or `$PROJECT_NAME` substitutions for project specific paths e.g. `$PROJECT/.venv/bin/pycodestyle;/usr/bin/pycodestyle`" + }, + "maxLineLength": { + "type": "integer", + "default": 0 + }, + "ignoreErrorCodes": { + "title": "Ignored Error Codes", + "type": "array", + "default": [], + "description": "For a list of code visit http://pycodestyle.readthedocs.org/en/latest/intro.html#error-codes" + }, + "convertAllErrorsToWarnings": { + "type": "boolean", + "default": true + }, + "forcedConfig": { + "type": "string", + "default": "", + "description": "Forces `pycodestyle` to use this configuration at all times. Supports substituion of `$PROJECT` and `$PROJECT_NAME`." + } + }, "scripts": { "test": "apm test", - "lint": "coffeelint ." + "lint": "eslint ." }, "engines": { - "atom": ">0.50.0" + "atom": ">=1.7.0 <2.0.0" }, "dependencies": { - "atom-linter": "^8.0.0", - "atom-package-deps": "^4.0.1" + "atom-linter": "^10.0.0", + "atom-package-deps": "^4.6.0", + "fs-plus": "^3.0.1" }, "devDependencies": { - "jasmine-fix": "^1.0.1", - "coffeelint": "^1.9.7" + "eslint": "^4.4.1", + "eslint-config-airbnb-base": "^11.3.1", + "eslint-plugin-import": "^2.7.0", + "jasmine-fix": "^1.0.1" }, "package-deps": [ - "linter" + "linter:2.0.0" ], "providedServices": { "linter": { "versions": { - "1.0.0": "provideLinter" + "2.0.0": "provideLinter" } } }, @@ -36,5 +65,26 @@ "linter", "pep8", "pycodestyle" - ] + ], + "eslintConfig": { + "extends": "airbnb-base", + "rules": { + "global-require": "off", + "import/no-unresolved": [ + "error", + { + "ignore": [ + "atom" + ] + } + ] + }, + "globals": { + "atom": true + }, + "env": { + "node": true, + "browser": true + } + } } diff --git a/spec/.eslintrc.js b/spec/.eslintrc.js new file mode 100644 index 0000000..c0a9cca --- /dev/null +++ b/spec/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + env: { + jasmine: true, + atomtest: true, + } +}; diff --git a/spec/fixtures/pycodestyle b/spec/fixtures/pycodestyle new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/pycodestyle_backup b/spec/fixtures/pycodestyle_backup new file mode 100644 index 0000000..e69de29 diff --git a/spec/linter-pycodestyle-spec.js b/spec/linter-pycodestyle-spec.js index 53f3fc9..8c2b81d 100644 --- a/spec/linter-pycodestyle-spec.js +++ b/spec/linter-pycodestyle-spec.js @@ -7,10 +7,9 @@ import { it, fit, wait, beforeEach, afterEach } from 'jasmine-fix'; const fixturePath = join(__dirname, 'fixtures'); const goodPath = join(fixturePath, 'good.py'); const badPath = join(fixturePath, 'bad.py'); -const emptyPath = join(fixturePath, 'empty.py'); describe('The pycodestyle provider for Linter', () => { - const lint = require('../lib/main.coffee').provideLinter().lint; + const lint = require('../lib/').provideLinter().lint; beforeEach(async () => { // Info about this beforeEach() implementation: @@ -39,11 +38,10 @@ describe('The pycodestyle provider for Linter', () => { it('verifies that message', async () => { const messages = await lint(editor); - expect(messages[0].type).toBe('Warning'); - expect(messages[0].html).not.toBeDefined(); - expect(messages[0].text).toBe(' E401 multiple imports on one line'); - expect(messages[0].filePath).toBe(badPath); - expect(messages[0].range).toEqual([[0, 9], [0, 10]]); + expect(messages[0].severity).toBe('warning'); + expect(messages[0].excerpt).toBe('E401 multiple imports on one line'); + expect(messages[0].location.file).toBe(badPath); + expect(messages[0].location.position).toEqual([[0, 9], [0, 15]]); }); }); @@ -53,4 +51,77 @@ describe('The pycodestyle provider for Linter', () => { expect(messages.length).toBe(0); }); + describe('executable path', () => { + const helpers = require('atom-linter'); + + let editor = null; + + beforeEach(async () => { + atom.project.addPath(fixturePath); + + spyOn(helpers, 'exec'); + + editor = await atom.workspace.open(badPath); + }); + + it('finds executable relative to project', async () => { + atom.config.set('linter-pycodestyle.executablePath', join('$PROJECT', 'pycodestyle')); + await lint(editor); + expect(helpers.exec.mostRecentCall.args[0]).toBe(join(fixturePath, 'pycodestyle')); + }); + + it('finds executable relative to projects', async () => { + const paths = [ + join('$project', 'null'), + join('$pRoJeCt', 'pycodestyle1'), + join('$PrOjEcT', 'pycodestyle2'), + join('$PROJECT', 'pycodestyle'), + ].join(';'); + atom.config.set('linter-pycodestyle.executablePath', paths); + await lint(editor); + expect(helpers.exec.mostRecentCall.args[0]).toBe(join(fixturePath, 'pycodestyle')); + }); + + it('finds executable using project name', async () => { + atom.config.set('linter-pycodestyle.executablePath', join('$PROJECT_NAME', 'pycodestyle')); + await lint(editor); + expect(helpers.exec.mostRecentCall.args[0]).toBe(join('fixtures', 'pycodestyle')); + }); + + it('finds executable using project names', async () => { + const paths = [ + join('$project_name', 'null'), + join('$pRoJeCt_NaMe', 'flake1'), + join('$PrOjEcT_nAmE', 'flake2'), + join('$PROJECT_NAME', 'pycodestyle'), + ].join(';'); + const correct = [ + join('fixtures', 'null'), + join('fixtures', 'flake1'), + join('fixtures', 'flake2'), + join('fixtures', 'pycodestyle'), + ].join(';'); + atom.config.set('linter-pycodestyle.executablePath', paths); + await lint(editor); + expect(helpers.exec.mostRecentCall.args[0]).toBe(correct); + }); + + it('normalizes executable path', async () => { + atom.config.set('linter-pycodestyle.executablePath', + join(fixturePath, '..', 'fixtures', 'pycodestyle'), + ); + await lint(editor); + expect(helpers.exec.mostRecentCall.args[0]).toBe(join(fixturePath, 'pycodestyle')); + }); + + it('finds backup executable', async () => { + const pycodestyleNotFound = join('$PROJECT', 'pycodestyle_notfound'); + const pycodestyleBackup = join(fixturePath, 'pycodestyle_backup'); + atom.config.set('linter-pycodestyle.executablePath', + `${pycodestyleNotFound};${pycodestyleBackup}`, + ); + await lint(editor); + expect(helpers.exec.mostRecentCall.args[0]).toBe(join(fixturePath, 'pycodestyle_backup')); + }); + }); });