├── .github └── workflows │ └── ci.yml ├── .python-version ├── LICENSE ├── README.md ├── linter.py ├── messages.json ├── messages ├── 4.2.0.txt └── install.txt └── mypy.ini /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | check-messages: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: kaste/upgrade-messages-test-action@v1 15 | 16 | lint: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: TrueBrain/actions-flake8@v2 21 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 17 | THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SublimeLinter-flake8 2 | ==================== 3 | 4 | [![Build Status](https://travis-ci.org/SublimeLinter/SublimeLinter-flake8.svg?branch=master)](https://travis-ci.org/SublimeLinter/SublimeLinter-flake8) 5 | 6 | This linter plugin for [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter) provides an interface to [flake8](http://flake8.readthedocs.org/en/latest/). 7 | 8 | 9 | ## Installation 10 | 11 | SublimeLinter must be installed in order to use this plugin. 12 | 13 | Install via [Package Control](https://packagecontrol.io) or `git clone` as usual. 14 | 15 | Ensure that a `flake8` is actually installed somewhere on your system. Typically, `pip install flake8` on the command line will do that. 16 | 17 | If you want to use a globally installed flake, make sure that it is available on the PATH. Before going any further, please read and follow the steps in ["Finding a linter executable"](http://sublimelinter.com/en/latest/troubleshooting.html#finding-a-linter-executable) through "Validating your PATH" in the documentation. 18 | 19 | Otherwise configure ["executable"](http://www.sublimelinter.com/en/latest/linter_settings.html#executable) or the ["python"](http://www.sublimelinter.com/en/latest/linter_settings.html#python) setting. 20 | 21 | If you use pipenv, and you're working on a project with a Pipfile, everything should be automatic. 22 | 23 | 24 | ## Settings 25 | 26 | - SublimeLinter settings: http://sublimelinter.com/en/latest/settings.html 27 | - Linter settings: http://sublimelinter.com/en/latest/linter_settings.html 28 | 29 | Additional settings: 30 | 31 | - `ignore_fixables` (default: `True`): filter warnings that Sublime can fix automatically (e.g. trailing white-space) on save. 32 | 33 | SublimeLinter-flake8 works with common flake8 [configuration files](http://flake8.pycqa.org/en/latest/user/configuration.html#configuration-locations) and inline overrides. Note that by default the [working dir](http://www.sublimelinter.com/en/latest/linter_settings.html#working-dir) is set to an open folder attached to the current window of Sublime. Edit this setting if your config files are located in a subfolder, for example. 34 | 35 | Use ["args"](http://www.sublimelinter.com/en/latest/linter_settings.html#args) if you want to pass additional command line arguments to `flake8`. 36 | 37 | ## Compatibility with `--per-file-ignores` and other flake8 plugins 38 | 39 | SublimeLinter-flake8 is compatible with most flake8 plugins out of the box. However, plugins that rely on selecting or ignoring certain files based on filename may appear to be "broken" due to the way SublimeLinter runs during linting. This includes flake8's own `--per-file-ignores` [option](http://flake8.pycqa.org/en/latest/user/options.html#cmdoption-flake8-per-file-ignores) introduced in 3.7.0, as well as plugins such as [flake8-aaa](https://github.com/jamescooke/flake8-aaa) and [flake8-pyi](https://github.com/ambv/flake8-pyi). 40 | 41 | To make the source filename available to the `flake8` executable again, pass the `--stdin-display-name` option using SublimeLinter's ["args"](http://www.sublimelinter.com/en/latest/linter_settings.html#args) setting: 42 | 43 | ``` 44 | "args": ["--stdin-display-name", "${file:stdin}"] 45 | ``` 46 | 47 | Including the `:stdin` fallback ensures that files that have yet to be saved are still linted. 48 | -------------------------------------------------------------------------------- /linter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from SublimeLinter.lint import PythonLinter 3 | import re 4 | 5 | 6 | logger = logging.getLogger('SublimeLinter.plugins.flake8') 7 | 8 | 9 | CAPTURE_WS = re.compile(r'(\s+)') 10 | CAPTURE_IMPORT_ID = re.compile(r'^\'(?:.*\.)?(.+)\'') 11 | CAPTURE_F403_HINT = re.compile(r"'(.*)?'") 12 | 13 | 14 | # Following codes are marked as errors. All other codes are marked as warnings. 15 | error_codes = { 16 | line.split(' ', 1)[0] 17 | for line in ( 18 | # Pyflake Errors: 19 | 'F402 import module from line N shadowed by loop variable', 20 | 'F404 future import(s) name after other statements', 21 | 'F812 list comprehension redefines name from line N', 22 | 'F823 local variable name ... referenced before assignment', 23 | 'F831 duplicate argument name in function definition', 24 | 'F821 undefined name name', 25 | 'F822 undefined name name in __all__', 26 | 27 | # Pep8 Errors: 28 | 'E112 expected an indented block', 29 | 'E113 unexpected indentation', 30 | 'E901 SyntaxError or IndentationError', 31 | 'E902 IOError', 32 | 'E999 SyntaxError', 33 | ) 34 | } 35 | 36 | 37 | class Flake8(PythonLinter): 38 | 39 | cmd = ('flake8', '--format', 'default', '${args}', '-') 40 | defaults = { 41 | 'selector': 'source.python', 42 | 43 | # Ignore codes Sublime can auto-fix 44 | 'ignore_fixables': True 45 | } 46 | 47 | regex = ( 48 | r'^.+?:(?P\d+):(?P\d+): ' 49 | r'(?P\w+\d+):? ' 50 | r'(?P.*)' 51 | ) 52 | multiline = True 53 | default_type = 'warning' 54 | 55 | def on_stderr(self, stderr): 56 | # For python 3.7 we actually have the case that flake yields 57 | # FutureWarnings. We just eat those as they're irrelevant here. Note 58 | # that we try to eat the subsequent line as well which usually contains 59 | # the culprit source line. 60 | stderr = re.sub(r'^.+FutureWarning.+\n(.*\n?)?', '', stderr, re.M) 61 | stderr = re.sub(r'^.+DeprecationWarning.+\n(.*\n?)?', '', stderr, re.M) 62 | 63 | if stderr: 64 | self.notify_failure() 65 | logger.error(stderr) 66 | 67 | def split_match(self, match): 68 | error = super().split_match(match) 69 | if error['code'] in error_codes: 70 | error['error_type'] = 'error' 71 | return error 72 | 73 | def parse_output(self, proc, virtual_view): 74 | errors = super().parse_output(proc, virtual_view) 75 | 76 | if not self.settings.get('ignore_fixables', True): 77 | return errors 78 | 79 | trims_ws = self.view.settings().get('trim_trailing_white_space_on_save') 80 | ensures_newline = self.view.settings().get('ensure_newline_at_eof_on_save') 81 | 82 | if not (trims_ws or ensures_newline): 83 | return errors 84 | 85 | filtered_errors = [] 86 | for error in errors: 87 | code = error['code'] 88 | 89 | if ensures_newline and code == 'W292': 90 | continue 91 | 92 | if trims_ws and code in ('W291', 'W293'): 93 | continue 94 | 95 | if trims_ws and code == 'W391': 96 | # Fixable if one WS line at EOF, or the view only has one line. 97 | lines = len(virtual_view._newlines) - 1 98 | if ( 99 | virtual_view.select_line(lines - 1).strip() == '' 100 | and ( 101 | lines < 2 102 | or virtual_view.select_line(lines - 2).strip() != '' 103 | ) 104 | ): 105 | continue 106 | 107 | filtered_errors.append(error) 108 | 109 | return filtered_errors 110 | 111 | def reposition_match(self, line, col, m, virtual_view): 112 | """Reposition white-space errors.""" 113 | code = m.code 114 | 115 | if code in ('W291', 'W293', 'E501'): 116 | txt = virtual_view.select_line(line).rstrip('\n') 117 | return (line, col, len(txt)) 118 | 119 | if code.startswith('E1'): 120 | return (line, 0, col) 121 | 122 | if code in ('E262', 'E265'): 123 | txt = virtual_view.select_line(line).rstrip('\n') 124 | match = CAPTURE_WS.match(txt[col + 1:]) 125 | if match is not None: 126 | length = len(match.group(1)) 127 | return (line, col, col + length + 1) 128 | 129 | if code.startswith('E266'): 130 | txt = virtual_view.select_line(line).rstrip('\n') 131 | tail_text = txt[col:] 132 | count_comment_sign = len(tail_text) - len(tail_text.lstrip("#")) 133 | return (line, col, col + count_comment_sign) 134 | 135 | if code.startswith('E2'): 136 | txt = virtual_view.select_line(line).rstrip('\n') 137 | match = CAPTURE_WS.match(txt[col:]) 138 | if match is not None: 139 | length = len(match.group(1)) 140 | return (line, col, col + length) 141 | 142 | if code in ('E302', 'E305'): 143 | return line - 1, 0, 1 144 | 145 | if code == 'E303': 146 | match = re.match(r'too many blank lines \((\d+)', m.message.strip()) 147 | if match is not None: 148 | count = int(match.group(1)) 149 | starting_line = line - count 150 | return ( 151 | starting_line, 152 | 0, 153 | sum( 154 | len(virtual_view.select_line(_line)) 155 | for _line in range(starting_line, line) 156 | ) 157 | ) 158 | 159 | if code == 'E999': 160 | txt = virtual_view.select_line(line).rstrip('\n') 161 | last_col = len(txt) 162 | if col + 1 == last_col: 163 | return line, last_col, last_col 164 | 165 | if code == 'F401': 166 | # Typical message from flake is "'x.y.z' imported but unused" 167 | # The import_id will be 'z' in that case. 168 | # Since, it is usual to spread imports on multiple lines, we 169 | # search MAX_LINES for `import_id` starting with the reported line. 170 | MAX_LINES = 30 171 | match = CAPTURE_IMPORT_ID.search(m.message) 172 | if match: 173 | import_id = match.group(1) 174 | 175 | if import_id == '*': 176 | pattern = re.compile(r'(\*)') 177 | else: 178 | pattern = re.compile(r'\b({})\b'.format(re.escape(import_id))) 179 | 180 | last_line = len(virtual_view._newlines) - 1 181 | for _line in range(line, min(line + MAX_LINES, last_line)): 182 | txt = virtual_view.select_line(_line) 183 | 184 | # Take the right most match, to count for 185 | # 'from util import util' 186 | matches = list(pattern.finditer(txt)) 187 | if matches: 188 | match = matches[-1] 189 | return _line, match.start(1), match.end(1) 190 | 191 | # Fallback, and mark the line. 192 | col = None 193 | 194 | if code == 'F403': 195 | txt = virtual_view.select_line(line).rstrip('\n') 196 | match = CAPTURE_F403_HINT.search(m.message) 197 | if match: 198 | hint = match.group(1) 199 | start = txt.find(hint) 200 | if start >= 0: 201 | return line, start + len(hint) - 1, start + len(hint) 202 | 203 | return super().reposition_match(line, col, m, virtual_view) 204 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.txt", 3 | "4.2.0": "messages/4.2.0.txt" 4 | } 5 | -------------------------------------------------------------------------------- /messages/4.2.0.txt: -------------------------------------------------------------------------------- 1 | SublimeLinter-flake8 4.2.0 2 | -------------------------- 3 | 4 | Less noise, hide problems that can be fixed automatically! 5 | 6 | Sublime has a basic code formatter built in. E.g. it will remove trailing white-space on save. (The related Sublime settings here are "trim_trailing_white_space_on_save" and "ensure_newline_at_eof_on_save".) 7 | 8 | Starting with this release, we will filter (hide) these errors automatically. We introduce a new setting "ignore_fixables" (True by default) which controls this behavior. 9 | 10 | Maybe you already know black, the new, zero-config code formatter for python. And in case you use it, you may have noticed that flake8 now produces a lot of noise which black can fix for you. There is an addon https://packagecontrol.io/packages/SublimeLinter-addon-black-for-flake which will automatically filter all warnings black can fix. Try it! 11 | 12 | 13 | Notable enhancement: Mark unused imported identifiers correct. E.g. in the following code, 14 | 15 | from x import ( 16 | x, 17 | y, 18 | z 19 | ) 20 | 21 | we now correctly highlight x, y, and z. 22 | 23 | Fix: Tighten compatibility with python 3.7 24 | 25 | -------------------------------------------------------------------------------- /messages/install.txt: -------------------------------------------------------------------------------- 1 | SublimeLinter-flake8 2 | ------------------------------- 3 | This linter plugin for SublimeLinter provides an interface to flake8. 4 | 5 | Please read the installation instructions at: 6 | 7 | https://github.com/SublimeLinter/SublimeLinter-flake8 8 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | check_untyped_defs = True 3 | warn_redundant_casts = True 4 | warn_unused_ignores = True 5 | mypy_path = 6 | :$MYPY_CONFIG_FILE_DIR/../ 7 | :$MYPY_CONFIG_FILE_DIR/../SublimeLinter/stubs 8 | sqlite_cache = True 9 | 10 | [mypy-Default] 11 | ignore_missing_imports = True 12 | 13 | [mypy-unittesting] 14 | ignore_missing_imports = True 15 | 16 | [mypy-package_control] 17 | ignore_missing_imports = True 18 | --------------------------------------------------------------------------------