From 38be1d0493cfa17303541eb2ec28db46e6617151 Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Fri, 14 Oct 2016 22:42:42 -0700 Subject: [PATCH 1/3] Add specs for the custom range formatting --- spec/fixtures/customRange.py | 24 +++++++++++++++++++++ spec/linter-flake8-spec.js | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 spec/fixtures/customRange.py diff --git a/spec/fixtures/customRange.py b/spec/fixtures/customRange.py new file mode 100644 index 0000000..7320951 --- /dev/null +++ b/spec/fixtures/customRange.py @@ -0,0 +1,24 @@ +from example import ( + f401_unused as unused_module) + + +def c901_too_complex(x): + if x > 1: + if x > 2: + if x > 3: + if x > 4: + if x > 5: + if x > 6: + if x > 7: + if x > 8: + if x > 9: + if x > 10: + if x > 11: + pass + + +def indent_unaligned(): + try: + print('H501 %(self)s' % locals()) + except: # <- H201 + pass diff --git a/spec/linter-flake8-spec.js b/spec/linter-flake8-spec.js index ea4f466..b539638 100644 --- a/spec/linter-flake8-spec.js +++ b/spec/linter-flake8-spec.js @@ -6,6 +6,7 @@ const goodPath = path.join(__dirname, 'fixtures', 'good.py'); const badPath = path.join(__dirname, 'fixtures', 'bad.py'); const errwarnPath = path.join(__dirname, 'fixtures', 'errwarn.py'); const fixturePath = path.join(__dirname, 'fixtures'); +const customRange = path.join(fixturePath, 'customRange.py'); describe('The flake8 provider for Linter', () => { const lint = require('../lib/main.coffee').provideLinter().lint; @@ -113,6 +114,47 @@ describe('The flake8 provider for Linter', () => { ); }); + it('fixes the range for certain errors', () => { + atom.config.set('linter-flake8.maxComplexity', 10); + waitsForPromise(() => + atom.workspace.open(customRange).then(editor => + lint(editor).then((messages) => { + // importedUnused() + const f401 = messages[0]; + let msgText = "F401 — 'unused_module' imported but unused"; + expect(f401.type).toBe('Warning'); + expect(f401.text).toBe(msgText); + expect(f401.filePath).toBe(customRange); + expect(f401.range).toEqual([[1, 19], [1, 32]]); + + // tooComplex() + const c901 = messages[1]; + msgText = "C901 — 'c901_too_complex' is too complex (13)"; + expect(c901.type).toBe('Warning'); + expect(c901.text).toBe(msgText); + expect(c901.filePath).toBe(customRange); + expect(c901.range).toEqual([[4, 4], [4, 20]]); + + // noLocalsString() + const h501 = messages[2]; + msgText = 'H501 — Do not use locals() for string formatting'; + expect(h501.type).toBe('Warning'); + expect(h501.text).toBe(msgText); + expect(h501.filePath).toBe(customRange); + expect(h501.range).toEqual([[21, 32], [21, 38]]); + + // H201 + const h201 = messages[3]; + msgText = "H201 — no 'except:' at least use 'except Exception:'"; + expect(h201.type).toBe('Warning'); + expect(h201.text).toBe(msgText); + expect(h201.filePath).toBe(customRange); + expect(h201.range).toEqual([[22, 4], [22, 11]]); + }) + ) + ); + }); + describe('executable path', () => { const helpers = require('atom-linter'); From 547168ac5a3e5c5a8e373e445d1ba1708a88cc21 Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Fri, 14 Oct 2016 22:45:55 -0700 Subject: [PATCH 2/3] Rewrite in ES2017 Rewrite in ES2017, including: * Rename `pep8ErrorsToWarnings` to pycodestyleErrorsToWarnings` * Move the range modification code into a separate file * Remove most of the range fixers in favor of `rangeFromLineNumber`'s "word" highlighting as it has the same effect in all removed cases. * Fix the remaining range fixing code to actually work properly * Set the minimum Atom version to v1.9.0 since we have tight version restrictions as we are using unpublic code parts * Move the configuration to a `configSchema` in the package.json * Install `hacking` in CI to test the custom range highlighting as half the code is to deal with issues it presents * Observe all settings * Use `helpers.rangeFromLineNumber` where possible * Use `async`/`await` to simplify the code where possible * Fix a race condition where the editor contents have changed since the lint was triggered * Convert `projectConfigFile` to an array. This is largely transparent as it was already being handled like one, this just means Atom handles stripping whitespace for us --- .travis.yml | 1 + coffeelint.json | 134 ------------------- lib/main.coffee | 256 ------------------------------------- lib/main.js | 227 ++++++++++++++++++++++++++++++++ lib/rangeHelpers.js | 143 +++++++++++++++++++++ package.json | 70 +++++++++- spec/linter-flake8-spec.js | 16 +-- 7 files changed, 444 insertions(+), 403 deletions(-) delete mode 100644 coffeelint.json delete mode 100644 lib/main.coffee create mode 100644 lib/main.js create mode 100644 lib/rangeHelpers.js diff --git a/.travis.yml b/.travis.yml index 1fd1c4d..176ecc0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,6 +30,7 @@ env: install: - pip install flake8 + - pip install hacking before_script: - flake8 --version diff --git a/coffeelint.json b/coffeelint.json deleted file mode 100644 index 4577a90..0000000 --- a/coffeelint.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "arrow_spacing": { - "level": "error" - }, - "braces_spacing": { - "level": "error", - "spaces": 0, - "empty_object_spaces": 0 - }, - "camel_case_classes": { - "level": "error" - }, - "coffeescript_error": { - "level": "error" - }, - "colon_assignment_spacing": { - "level": "error", - "spacing": { - "left": 0, - "right": 1 - } - }, - "cyclomatic_complexity": { - "value": 10, - "level": "ignore" - }, - "duplicate_key": { - "level": "error" - }, - "empty_constructor_needs_parens": { - "level": "ignore" - }, - "ensure_comprehensions": { - "level": "warn" - }, - "eol_last": { - "level": "ignore" - }, - "indentation": { - "value": 2, - "level": "error" - }, - "line_endings": { - "level": "ignore", - "value": "unix" - }, - "max_line_length": { - "value": 120, - "level": "warn", - "limitComments": true - }, - "missing_fat_arrows": { - "level": "ignore", - "is_strict": false - }, - "newlines_after_classes": { - "value": 3, - "level": "ignore" - }, - "no_backticks": { - "level": "error" - }, - "no_debugger": { - "level": "error" - }, - "no_empty_functions": { - "level": "ignore" - }, - "no_empty_param_list": { - "level": "error" - }, - "no_implicit_braces": { - "level": "ignore", - "strict": true - }, - "no_implicit_parens": { - "strict": true, - "level": "ignore" - }, - "no_interpolation_in_single_quotes": { - "level": "error" - }, - "no_nested_string_interpolation": { - "level": "warn" - }, - "no_plusplus": { - "level": "ignore" - }, - "no_private_function_fat_arrows": { - "level": "warn" - }, - "no_stand_alone_at": { - "level": "error" - }, - "no_tabs": { - "level": "error" - }, - "no_this": { - "level": "ignore" - }, - "no_throwing_strings": { - "level": "error" - }, - "no_trailing_semicolons": { - "level": "error" - }, - "no_trailing_whitespace": { - "level": "error", - "allowed_in_comments": false, - "allowed_in_empty_lines": true - }, - "no_unnecessary_double_quotes": { - "level": "error" - }, - "no_unnecessary_fat_arrows": { - "level": "warn" - }, - "non_empty_constructor_needs_parens": { - "level": "ignore" - }, - "prefer_english_operator": { - "level": "error", - "doubleNotLevel": "ignore" - }, - "space_operators": { - "level": "ignore" - }, - "spacing_after_comma": { - "level": "error" - }, - "transform_messes_up_line_numbers": { - "level": "warn" - } -} diff --git a/lib/main.coffee b/lib/main.coffee deleted file mode 100644 index 2b08e03..0000000 --- a/lib/main.coffee +++ /dev/null @@ -1,256 +0,0 @@ -tokenizedLineForRow = (textEditor, lineNumber) -> - # Uses non-public parts of the API, liable to break at any time! - if (textEditor.hasOwnProperty('displayBuffer')) - # Atom < 1.9.0 - # FIXME: Remove when 1.9.0 is released! - tokenBuffer = textEditor.displayBuffer.tokenizedBuffer - else - tokenBuffer = textEditor.tokenizedBuffer - tokenBuffer.tokenizedLineForRow(lineNumber) -fs = require('fs') -path = require('path') -{CompositeDisposable} = require 'atom' - -extractRange = ({code, message, lineNumber, colNumber, textEditor}) -> - switch code - when 'C901' - # C901 - 'FUNCTION' is too complex - # C901 - 'CLASS.METHOD' is too complex - symbol = /'(?:[^.]+\.)?([^']+)'/.exec(message)[1] - while true - offset = 0 - tokenizedLine = tokenizedLineForRow(textEditor, lineNumber) - if tokenizedLine is undefined - break - foundDecorator = false - for token in tokenizedLine.tokens - if 'meta.function.python' in token.scopes - if token.value is symbol - return [[lineNumber, offset], [lineNumber, offset + token.value.length]] - if 'meta.function.decorator.python' in token.scopes - foundDecorator = true - offset += token.value.length - if not foundDecorator - break - lineNumber += 1 - when 'E125', 'E127', 'E128', 'E131' - # E125 - continuation line with same indent as next logical line - # E127 - continuation line over-indented for visual indent - # E128 - continuation line under-indented for visual indent - # E131 - continuation line unaligned for hanging indent - tokenizedLine = tokenizedLineForRow(textEditor, lineNumber) - if tokenizedLine is undefined - break - offset = 0 - for token in tokenizedLine.tokens - if not token.firstNonWhitespaceIndex - return [[lineNumber, 0], [lineNumber, offset]] - if token.firstNonWhitespaceIndex isnt token.value.length - return [[lineNumber, 0], [lineNumber, offset + token.firstNonWhitespaceIndex]] - offset += token.value.length - when 'E262', 'E265' - # E262 - inline comment should start with '# ' - # E265 - block comment should start with '# ' - return [[lineNumber, colNumber - 1], [lineNumber, colNumber + 1]] - when 'F401' - # F401 - 'SYMBOL' imported but unused - symbol = /'([^']+)'/.exec(message)[1] - [prefix..., symbol] = symbol.split('.') - foundImport = false - while true - offset = 0 - tokenizedLine = tokenizedLineForRow(textEditor, lineNumber) - if tokenizedLine is undefined - break - for token in tokenizedLine.tokens - if foundImport and token.value is symbol - return [[lineNumber, offset], [lineNumber, offset + token.value.length]] - if token.value is 'import' and 'keyword.control.import.python' in token.scopes - foundImport = true - offset += token.value.length - lineNumber += 1 - when 'F821', 'F841' - # F821 - undefined name 'SYMBOL' - # F841 - local variable 'SYMBOL' is assigned but never used - symbol = /'([^']+)'/.exec(message)[1] - tokenizedLine = tokenizedLineForRow(textEditor, lineNumber) - if tokenizedLine is undefined - break - offset = 0 - for token in tokenizedLine.tokens - if token.value is symbol and offset >= colNumber - 1 - return [[lineNumber, offset], [lineNumber, offset + token.value.length]] - offset += token.value.length - when 'H101' - # H101 - use TODO(NAME) - return [[lineNumber, colNumber - 1], [lineNumber, colNumber + 3]] - when 'H201' - # H201 - no 'except:' at least use 'except Exception:' - return [[lineNumber, colNumber - 7], [lineNumber, colNumber]] - when 'H231' - # H231 - Python 3.x incompatible 'except x,y' construct - return [[lineNumber, colNumber - 1], [lineNumber, colNumber + 5]] - when 'H233' - # H233 - Python 3.x incompatible use of print operator - return [[lineNumber, colNumber - 1], [lineNumber, colNumber + 4]] - when 'H236' - # H236 - Python 3.x incompatible __metaclass__, use six.add_metaclass() - return [[lineNumber, colNumber - 1], [lineNumber, colNumber + 12]] - when 'H238' - # H238 - old style class declaration, use new style (inherit from `object`) - return [[lineNumber, colNumber - 1], [lineNumber, colNumber + 4]] - when 'H501' - # H501 - do not use locals() for string formatting - tokenizedLine = tokenizedLineForRow(textEditor, lineNumber) - if tokenizedLine is undefined - break - offset = 0 - for token in tokenizedLine.tokens - if 'meta.function-call.python' in token.scopes - if token.value is 'locals' - return [[lineNumber, offset], [lineNumber, offset + token.value.length]] - offset += token.value.length - when 'W291' - # W291 - trailing whitespace - screenLine = tokenizedLineForRow(textEditor, lineNumber) - if screenLine is undefined - break - return [[lineNumber, colNumber - 1], [lineNumber, screenLine.length]] - return [[lineNumber, colNumber - 1], [lineNumber, colNumber]] - -module.exports = - config: - executablePath: - type: 'string' - default: 'flake8' - description: 'Semicolon separated list of paths to a binary (e.g. /usr/local/bin/flake8). ' + - 'Use `$PROJECT` or `$PROJECT_NAME` substitutions for project specific paths ' + - 'e.g. `$PROJECT/.venv/bin/flake8;/usr/bin/flake8`' - disableTimeout: - type: 'boolean' - default: false - description: 'Disable the 10 second execution timeout' - projectConfigFile: - type: 'string' - default: '' - description: 'flake config file relative path from project (e.g. tox.ini or .flake8rc)' - maxLineLength: - type: 'integer' - default: 0 - ignoreErrorCodes: - type: 'array' - default: [] - items: - type: 'string' - maxComplexity: - description: 'McCabe complexity threshold (`-1` to disable)' - type: 'integer' - default: -1 - hangClosing: - type: 'boolean' - default: false - selectErrors: - description: 'input "E, W" to include all errors/warnings' - type: 'array' - default: [] - items: - type: 'string' - pep8ErrorsToWarnings: # FIXME: Rename to pycodestyle in major release - description: 'Convert pycodestyle "E" messages to linter warnings' - type: 'boolean' - default: false - flakeErrors: - description: 'Convert Flake "F" messages to linter errors' - type: 'boolean' - default: false - - activate: -> - require('atom-package-deps').install() - @subscriptions = new CompositeDisposable - @subscriptions.add atom.config.observe 'linter-flake8.disableTimeout', - (disableTimeout) => - @disableTimeout = disableTimeout - - deactivate: -> - @subscriptions.dispose() - - getProjDir: (file) -> - atom.project.relativizePath(file)[0] - - getProjName: (projDir) -> - path.basename(projDir) - - applySubstitutions: (execPath, projDir) -> - projectName = @getProjName(projDir) - execPath = execPath.replace(/\$PROJECT_NAME/i, projectName) - execPath = execPath.replace(/\$PROJECT/i, projDir) - for p in execPath.split(';') - if fs.existsSync(p) - return p - return execPath - - provideLinter: -> - helpers = require('atom-linter') - - provider = - name: 'Flake8' - grammarScopes: ['source.python', 'source.python.django'] - scope: 'file' # or 'project' - lintOnFly: true # must be false for scope: 'project' - lint: (textEditor) => - filePath = textEditor.getPath() - fileText = textEditor.getText() - parameters = ['--format=default'] - - if ( - (projectConfigFile = atom.config.get('linter-flake8.projectConfigFile')) and - (projectPath = atom.project.relativizePath(filePath)[0]) and - (configFilePath = helpers.findCached(projectPath, projectConfigFile.split(/[ ,]+/))) - ) - parameters.push('--config', configFilePath) - else - if maxLineLength = atom.config.get('linter-flake8.maxLineLength') - parameters.push('--max-line-length', maxLineLength) - if (ignoreErrorCodes = atom.config.get('linter-flake8.ignoreErrorCodes')).length - parameters.push('--ignore', ignoreErrorCodes.join(',')) - if maxComplexity = atom.config.get('linter-flake8.maxComplexity') - parameters.push('--max-complexity', maxComplexity) - if atom.config.get('linter-flake8.hangClosing') - parameters.push('--hang-closing') - if (selectErrors = atom.config.get('linter-flake8.selectErrors')).length - parameters.push('--select', selectErrors.join(',')) - - parameters.push('-') - - fs = require('fs-plus') - pycodestyleWarn = atom.config.get('linter-flake8.pep8ErrorsToWarnings') - flakeerr = atom.config.get('linter-flake8.flakeErrors') - projDir = @getProjDir(filePath) or path.dirname(filePath) - execPath = fs.normalize(@applySubstitutions(atom.config.get('linter-flake8.executablePath'), projDir)) - cwd = path.dirname(textEditor.getPath()) - options = {stdin: fileText, cwd: cwd, stream: 'both'} - if @disableTimeout - options.timeout = Infinity - return helpers.exec(execPath, parameters, options).then (result) -> - if (result.stderr and result.stderr.length and atom.inDevMode()) - console.log('flake8 stderr: ' + result.stderr) - toReturn = [] - regex = /(\d+):(\d+):\s(([A-Z])\d{2,3})\s+(.*)/g - - while (match = regex.exec(result.stdout)) isnt null - line = parseInt(match[1]) or 0 - col = parseInt(match[2]) or 0 - isErr = match[4] is 'E' and not pycodestyleWarn or match[4] is 'F' and flakeerr - toReturn.push({ - type: if isErr then 'Error' else 'Warning' - text: match[3] + ' — ' + match[5] - filePath - range: extractRange({ - code: match[3] - message: match[5] - lineNumber: line - 1 - colNumber: col - textEditor - }) - }) - return toReturn diff --git a/lib/main.js b/lib/main.js new file mode 100644 index 0000000..9c0d22b --- /dev/null +++ b/lib/main.js @@ -0,0 +1,227 @@ +'use babel'; + +// eslint-disable-next-line import/extensions, import/no-extraneous-dependencies +import { CompositeDisposable } from 'atom'; +import fs from 'fs-plus'; +import path from 'path'; +import * as helpers from 'atom-linter'; +import rangeHelpers from './rangeHelpers'; + +// Local variables +const parseRegex = /(\d+):(\d+):\s(([A-Z])\d{2,3})\s+(.*)/g; + +// Settings +let disableTimeout; +let projectConfigFile; +let maxLineLength; +let ignoreErrorCodes; +let maxComplexity; +let selectErrors; +let hangClosing; +let executablePath; +let pycodestyleErrorsToWarnings; +let flakeErrors; + +const extractRange = ({ code, message, lineNumber, colNumber, textEditor }) => { + let result; + const line = lineNumber - 1; + switch (code) { + case 'C901': + result = rangeHelpers.tooComplex(textEditor, message, line); + break; + case 'F401': + result = rangeHelpers.importedUnused(textEditor, message, line); + break; + case 'H201': + // H201 - no 'except:' at least use 'except Exception:' + + // For some reason this rule marks the ":" as the location by default + result = { + line, + col: colNumber - 7, + endCol: colNumber, + }; + break; + case 'H501': + result = rangeHelpers.noLocalsString(textEditor, line); + break; + default: + result = { + line, + col: colNumber - 1, + }; + break; + } + if (Object.hasOwnProperty.call(result, 'endCol')) { + return [ + [result.line, result.col], + [result.line, result.endCol], + ]; + } + return helpers.rangeFromLineNumber(textEditor, result.line, result.col); +}; + +const applySubstitutions = (givenExecPath, projDir) => { + let execPath = givenExecPath; + const projectName = path.basename(projDir); + execPath = execPath.replace(/\$PROJECT_NAME/i, projectName); + execPath = execPath.replace(/\$PROJECT/i, projDir); + const paths = execPath.split(';'); + for (let i = 0; i < paths.length; i += 1) { + if (fs.existsSync(paths[i])) { + return paths[i]; + } + } + return execPath; +}; + +export default { + activate() { + require('atom-package-deps').install('linter-flake8'); + + this.subscriptions = new CompositeDisposable(); + this.subscriptions.add( + atom.config.observe('linter-flake8.disableTimeout', (value) => { + disableTimeout = value; + }) + ); + this.subscriptions.add( + atom.config.observe('linter-flake8.projectConfigFile', (value) => { + projectConfigFile = value; + }) + ); + this.subscriptions.add( + atom.config.observe('linter-flake8.maxLineLength', (value) => { + maxLineLength = value; + }) + ); + this.subscriptions.add( + atom.config.observe('linter-flake8.ignoreErrorCodes', (value) => { + ignoreErrorCodes = value; + }) + ); + this.subscriptions.add( + atom.config.observe('linter-flake8.maxComplexity', (value) => { + maxComplexity = value; + }) + ); + this.subscriptions.add( + atom.config.observe('linter-flake8.selectErrors', (value) => { + selectErrors = value; + }) + ); + this.subscriptions.add( + atom.config.observe('linter-flake8.hangClosing', (value) => { + hangClosing = value; + }) + ); + this.subscriptions.add( + atom.config.observe('linter-flake8.executablePath', (value) => { + executablePath = value; + }) + ); + this.subscriptions.add( + atom.config.observe('linter-flake8.pycodestyleErrorsToWarnings', (value) => { + pycodestyleErrorsToWarnings = value; + }) + ); + this.subscriptions.add( + atom.config.observe('linter-flake8.flakeErrors', (value) => { + flakeErrors = value; + }) + ); + }, + + deactivate() { + this.subscriptions.dispose(); + }, + + provideLinter() { + return { + name: 'Flake8', + grammarScopes: ['source.python', 'source.python.django'], + scope: 'file', + lintOnFly: true, + lint: async (textEditor) => { + const filePath = textEditor.getPath(); + const fileText = textEditor.getText(); + + const parameters = ['--format=default']; + + const projectPath = atom.project.relativizePath(filePath)[0]; + const baseDir = projectPath !== null ? projectPath : path.dirname(filePath); + const configFilePath = await helpers.findCachedAsync(baseDir, projectConfigFile); + + if (projectConfigFile && baseDir !== null && configFilePath !== null) { + parameters.push('--config', configFilePath); + } else { + if (maxLineLength) { + parameters.push('--max-line-length', maxLineLength); + } + if (ignoreErrorCodes.length) { + parameters.push('--ignore', ignoreErrorCodes.join(',')); + } + if (maxComplexity) { + parameters.push('--max-complexity', maxComplexity); + } + if (hangClosing) { + parameters.push('--hang-closing'); + } + if (selectErrors.length) { + parameters.push('--select', selectErrors.join(',')); + } + } + + parameters.push('-'); + + const execPath = fs.normalize(applySubstitutions(executablePath, baseDir)); + const options = { + stdin: fileText, + cwd: path.dirname(textEditor.getPath()), + stream: 'both', + }; + if (disableTimeout) { + options.timeout = Infinity; + } + + const result = await helpers.exec(execPath, parameters, options); + + if (textEditor.getText() !== fileText) { + // Editor contents have changed, tell Linter not to update + return null; + } + + if (result.stderr && result.stderr.length && atom.inDevMode()) { + // eslint-disable-next-line no-console + console.log(`flake8 stderr: ${result.stderr}`); + } + const messages = []; + + let match = parseRegex.exec(result.stdout); + while (match !== null) { + const line = Number.parseInt(match[1], 10) || 0; + const col = Number.parseInt(match[2], 10) || 0; + const isErr = (match[4] === 'E' && !pycodestyleErrorsToWarnings) + || (match[4] === 'F' && flakeErrors); + const range = extractRange({ + code: match[3], + message: match[5], + lineNumber: line, + colNumber: col, + textEditor, + }); + + messages.push({ + type: isErr ? 'Error' : 'Warning', + text: `${match[3]} — ${match[5]}`, + filePath, + range, + }); + + match = parseRegex.exec(result.stdout); + } + return messages; + }, + }; + }, +}; diff --git a/lib/rangeHelpers.js b/lib/rangeHelpers.js new file mode 100644 index 0000000..4809c87 --- /dev/null +++ b/lib/rangeHelpers.js @@ -0,0 +1,143 @@ +'use babel'; + +const tokenizedLineForRow = (textEditor, lineNumber) => + // Uses non-public parts of the Atom API, liable to break at any time! + textEditor.tokenizedBuffer.tokenizedLineForRow(lineNumber); + +export default { + tooComplex(textEditor, message, lineNumber) { + // C901 - 'FUNCTION' is too complex + // C901 - 'CLASS.METHOD' is too complex + + // // Get the raw symbol + const symbol = /'(?:[^.]+\.)?([^']+)'/.exec(message)[1]; + + // Some variables + const lineCount = textEditor.getLineCount(); + let line; + + // Parse through the lines, starting where `flake8` says it starts + for (line = lineNumber; line < lineCount; line += 1) { + let offset = 0; + const tokenizedLine = tokenizedLineForRow(textEditor, line); + if (tokenizedLine === undefined) { + // Doesn't exist if the line is folded + break; + } + + let foundDecorator = false; + for (let i = 0; i < tokenizedLine.tokens.length; i += 1) { + const token = tokenizedLine.tokens[i]; + if (token.scopes.includes('meta.function.python')) { + if (token.value === symbol) { + return { + line, + col: offset, + endCol: offset + token.value.length, + }; + } + } + // Flag whether we have found the decorator, must be after symbol checks + if (token.scopes.includes('meta.function.decorator.python')) { + foundDecorator = true; + } + offset += token.value.length; + } + + if (!foundDecorator) { + break; + } + } + + // Fixing couldn't determine a point, let rangeFromLineNumber make up a range + return { + line, + }; + }, + + importedUnused(textEditor, message, lineNumber) { + // F401 - 'SYMBOL' imported but unused + + // Get the raw symbol and split it into the word(s) + let symbol = /'([^']+)'/.exec(message)[1]; + [symbol] = symbol.split('.').slice(-1); + const symbolParts = symbol.split(/\s/); + + // Some variables + let foundImport = false; + const lineCount = textEditor.getLineCount(); + let line; + let start; + let end; + + // Parse through the lines, starting where `flake8` says it starts + for (line = lineNumber; line < lineCount; line += 1) { + let offset = 0; + const tokenizedLine = tokenizedLineForRow(textEditor, line); + if (tokenizedLine === undefined) { + // Doesn't exist if the line is folded + break; + } + // Check each token in the line + for (let i = 0; i < tokenizedLine.tokens.length; i += 1) { + const token = tokenizedLine.tokens[i]; + // Only match on the name if we have already passed the "import" statement + if (foundImport && token.value === symbolParts[0]) { + start = { line, col: offset }; + end = { line, col: offset + token.value.length }; + } + // For multi-word symbols('foo as bar'), grab the end point as well + if (foundImport && symbolParts.length > 1 + && token.value === symbolParts[symbolParts.length - 1] + ) { + end = { line, col: offset + token.value.length }; + } + // Flag whether we have found the import, must be after symbol checks + if (token.value === 'import' && token.scopes.includes('keyword.control.import.python')) { + foundImport = true; + } + // If we didn't find what we were looking for, move on in the line + offset += token.value.length; + } + } + if (start !== undefined && end !== undefined) { + // We found a valid range + return { + line: start.line, + col: start.col, + endCol: end.col, + }; + } + // Fixing couldn't determine a point, let rangeFromLineNumber make up a range + return { + line, + }; + }, + + noLocalsString(textEditor, lineNumber) { + // H501 - do not use locals() for string formatting + const tokenizedLine = tokenizedLineForRow(textEditor, lineNumber); + if (tokenizedLine === undefined) { + return { + line: lineNumber, + }; + } + let offset = 0; + for (let i = 0; i < tokenizedLine.tokens.length; i += 1) { + const token = tokenizedLine.tokens[i]; + if (token.scopes.includes('meta.function-call.python')) { + if (token.value === 'locals') { + return { + line: lineNumber, + col: offset, + endCol: offset + token.value.length, + }; + } + } + offset += token.value.length; + } + return { + line: lineNumber, + }; + }, +}; diff --git a/package.json b/package.json index 1c36ae2..67961db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "linter-flake8", - "main": "./lib/main", + "main": "./lib/main.js", "version": "1.13.4", "description": "Atom linter plugin for Python, using flake8", "license": "MIT", @@ -8,13 +8,73 @@ "type": "git", "url": "https://github.com/AtomLinter/linter-flake8" }, + "engines": { + "atom": ">=1.9.0 <2.0.0" + }, + "configSchema": { + "executablePath": { + "type": "string", + "default": "flake8", + "description": "Semicolon separated list of paths to a binary (e.g. /usr/local/bin/flake8). Use `$PROJECT` or `$PROJECT_NAME` substitutions for project specific paths e.g. `$PROJECT/.venv/bin/flake8;/usr/bin/flake8`" + }, + "disableTimeout": { + "type": "boolean", + "default": false, + "description": "Disable the 10 second execution timeout" + }, + "projectConfigFile": { + "type": "array", + "default": [], + "description": "flake config file relative path from project (e.g. tox.ini or .flake8rc)", + "items": { + "type": "string" + } + }, + "maxLineLength": { + "type": "integer", + "default": 0 + }, + "ignoreErrorCodes": { + "type": "array", + "default": [], + "items": { + "type": "string" + } + }, + "maxComplexity": { + "description": "McCabe complexity threshold (`-1` to disable)", + "type": "integer", + "default": -1 + }, + "hangClosing": { + "type": "boolean", + "default": false + }, + "selectErrors": { + "description": "input \"E, W\" to include all errors/warnings", + "type": "array", + "default": [], + "items": { + "type": "string" + } + }, + "pycodestyleErrorsToWarnings": { + "description": "Convert pycodestyle \"E\" messages to linter warnings", + "type": "boolean", + "default": false + }, + "flakeErrors": { + "description": "Convert Flake \"F\" messages to linter errors", + "type": "boolean", + "default": false + } + }, "dependencies": { "atom-linter": "^8.0.0", "atom-package-deps": "^4.0.1", "fs-plus": "^2.8.1" }, "devDependencies": { - "coffeelint": "^1.15.0", "eslint": "^3.6.0", "eslint-config-airbnb-base": "^8.0.0", "eslint-plugin-import": "^1.16.0" @@ -24,13 +84,14 @@ ], "scripts": { "test": "apm test", - "lint": "coffeelint lib && eslint spec" + "lint": "eslint ." }, "eslintConfig": { + "extends": "airbnb-base", "rules": { "global-require": "off", "import/no-unresolved": [ - "off", + "error", { "ignore": [ "atom" @@ -38,7 +99,6 @@ } ] }, - "extends": "airbnb-base", "globals": { "atom": true }, diff --git a/spec/linter-flake8-spec.js b/spec/linter-flake8-spec.js index b539638..b76419a 100644 --- a/spec/linter-flake8-spec.js +++ b/spec/linter-flake8-spec.js @@ -2,14 +2,14 @@ import * as path from 'path'; -const goodPath = path.join(__dirname, 'fixtures', 'good.py'); -const badPath = path.join(__dirname, 'fixtures', 'bad.py'); -const errwarnPath = path.join(__dirname, 'fixtures', 'errwarn.py'); const fixturePath = path.join(__dirname, 'fixtures'); +const goodPath = path.join(fixturePath, 'good.py'); +const badPath = path.join(fixturePath, 'bad.py'); +const errwarnPath = path.join(fixturePath, 'errwarn.py'); const customRange = path.join(fixturePath, 'customRange.py'); describe('The flake8 provider for Linter', () => { - const lint = require('../lib/main.coffee').provideLinter().lint; + const lint = require('../lib/main.js').provideLinter().lint; beforeEach(() => { waitsForPromise(() => @@ -85,8 +85,8 @@ describe('The flake8 provider for Linter', () => { ) ); - it('finds the message is a warning if pep8ErrorsToWarnings is set', () => { - atom.config.set('linter-flake8.pep8ErrorsToWarnings', true); + it('finds the message is a warning if pycodestyleErrorsToWarnings is set', () => { + atom.config.set('linter-flake8.pycodestyleErrorsToWarnings', true); waitsForPromise(() => lint(editor).then(messages => expect(messages[0].type).toBe('Warning') @@ -94,8 +94,8 @@ describe('The flake8 provider for Linter', () => { ); }); - it('finds the message is an error if pep8ErrorsToWarnings is set', () => { - atom.config.set('linter-flake8.pep8ErrorsToWarnings', false); + it("finds the message is an error if pycodestyleErrorsToWarnings isn't set", () => { + atom.config.set('linter-flake8.pycodestyleErrorsToWarnings', false); waitsForPromise(() => lint(editor).then(messages => expect(messages[0].type).toBe('Error') From ca8fbf02e911a54974518215df43161398a5669a Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Fri, 14 Oct 2016 23:24:03 -0700 Subject: [PATCH 3/3] Move settings to properties of the linter --- lib/main.js | 62 +++++++++++++++++++++-------------------------------- 1 file changed, 25 insertions(+), 37 deletions(-) diff --git a/lib/main.js b/lib/main.js index 9c0d22b..92e4204 100644 --- a/lib/main.js +++ b/lib/main.js @@ -10,18 +10,6 @@ import rangeHelpers from './rangeHelpers'; // Local variables const parseRegex = /(\d+):(\d+):\s(([A-Z])\d{2,3})\s+(.*)/g; -// Settings -let disableTimeout; -let projectConfigFile; -let maxLineLength; -let ignoreErrorCodes; -let maxComplexity; -let selectErrors; -let hangClosing; -let executablePath; -let pycodestyleErrorsToWarnings; -let flakeErrors; - const extractRange = ({ code, message, lineNumber, colNumber, textEditor }) => { let result; const line = lineNumber - 1; @@ -82,52 +70,52 @@ export default { this.subscriptions = new CompositeDisposable(); this.subscriptions.add( atom.config.observe('linter-flake8.disableTimeout', (value) => { - disableTimeout = value; + this.disableTimeout = value; }) ); this.subscriptions.add( atom.config.observe('linter-flake8.projectConfigFile', (value) => { - projectConfigFile = value; + this.projectConfigFile = value; }) ); this.subscriptions.add( atom.config.observe('linter-flake8.maxLineLength', (value) => { - maxLineLength = value; + this.maxLineLength = value; }) ); this.subscriptions.add( atom.config.observe('linter-flake8.ignoreErrorCodes', (value) => { - ignoreErrorCodes = value; + this.ignoreErrorCodes = value; }) ); this.subscriptions.add( atom.config.observe('linter-flake8.maxComplexity', (value) => { - maxComplexity = value; + this.maxComplexity = value; }) ); this.subscriptions.add( atom.config.observe('linter-flake8.selectErrors', (value) => { - selectErrors = value; + this.selectErrors = value; }) ); this.subscriptions.add( atom.config.observe('linter-flake8.hangClosing', (value) => { - hangClosing = value; + this.hangClosing = value; }) ); this.subscriptions.add( atom.config.observe('linter-flake8.executablePath', (value) => { - executablePath = value; + this.executablePath = value; }) ); this.subscriptions.add( atom.config.observe('linter-flake8.pycodestyleErrorsToWarnings', (value) => { - pycodestyleErrorsToWarnings = value; + this.pycodestyleErrorsToWarnings = value; }) ); this.subscriptions.add( atom.config.observe('linter-flake8.flakeErrors', (value) => { - flakeErrors = value; + this.flakeErrors = value; }) ); }, @@ -150,37 +138,37 @@ export default { const projectPath = atom.project.relativizePath(filePath)[0]; const baseDir = projectPath !== null ? projectPath : path.dirname(filePath); - const configFilePath = await helpers.findCachedAsync(baseDir, projectConfigFile); + const configFilePath = await helpers.findCachedAsync(baseDir, this.projectConfigFile); - if (projectConfigFile && baseDir !== null && configFilePath !== null) { + if (this.projectConfigFile && baseDir !== null && configFilePath !== null) { parameters.push('--config', configFilePath); } else { - if (maxLineLength) { - parameters.push('--max-line-length', maxLineLength); + if (this.maxLineLength) { + parameters.push('--max-line-length', this.maxLineLength); } - if (ignoreErrorCodes.length) { - parameters.push('--ignore', ignoreErrorCodes.join(',')); + if (this.ignoreErrorCodes.length) { + parameters.push('--ignore', this.ignoreErrorCodes.join(',')); } - if (maxComplexity) { - parameters.push('--max-complexity', maxComplexity); + if (this.maxComplexity) { + parameters.push('--max-complexity', this.maxComplexity); } - if (hangClosing) { + if (this.hangClosing) { parameters.push('--hang-closing'); } - if (selectErrors.length) { - parameters.push('--select', selectErrors.join(',')); + if (this.selectErrors.length) { + parameters.push('--select', this.selectErrors.join(',')); } } parameters.push('-'); - const execPath = fs.normalize(applySubstitutions(executablePath, baseDir)); + const execPath = fs.normalize(applySubstitutions(this.executablePath, baseDir)); const options = { stdin: fileText, cwd: path.dirname(textEditor.getPath()), stream: 'both', }; - if (disableTimeout) { + if (this.disableTimeout) { options.timeout = Infinity; } @@ -201,8 +189,8 @@ export default { while (match !== null) { const line = Number.parseInt(match[1], 10) || 0; const col = Number.parseInt(match[2], 10) || 0; - const isErr = (match[4] === 'E' && !pycodestyleErrorsToWarnings) - || (match[4] === 'F' && flakeErrors); + const isErr = (match[4] === 'E' && !this.pycodestyleErrorsToWarnings) + || (match[4] === 'F' && this.flakeErrors); const range = extractRange({ code: match[3], message: match[5],