├── .flake8 ├── .github └── no-response.yml ├── .gitignore ├── .python-version ├── .travis.yml ├── LICENSE ├── README.md ├── linter.py ├── messages.json └── messages ├── 1.3.0.txt ├── 1.4.0.txt ├── 1.5.0.txt └── install.txt /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | 4 | # D100 Missing docstring in public module 5 | # D101 Missing docstring in public class 6 | # D102 Missing docstring in public method 7 | # D103 Missing docstring in public function 8 | # D104 Missing docstring in public package 9 | # D105 Missing docstring in magic method 10 | # D107 Missing docstring in __init__ 11 | ignore = D100,D101,D102,D103,D105,D107 12 | 13 | -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an Issue is closed for lack of response 4 | daysUntilClose: 14 5 | # Label requiring a response 6 | responseRequiredLabel: "awaiting response" 7 | # Comment to post when closing an Issue for lack of response. Set to `false` to disable 8 | closeComment: > 9 | This issue has been automatically closed because there has been no response 10 | to our request for more information from the original author. With only the 11 | information that is currently in the issue, we don't have enough information 12 | to take action. Please reach out if you have or find the answers we need so 13 | that we can investigate further. 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | install: 5 | - pip install flake8 6 | script: 7 | - flake8 8 | -------------------------------------------------------------------------------- /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-pylint 2 | ========================= 3 | 4 | [![Build Status](https://travis-ci.org/SublimeLinter/SublimeLinter-pylint.svg?branch=master)](https://travis-ci.org/SublimeLinter/SublimeLinter-pylint) 5 | 6 | This linter plugin for [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter) provides an interface to [pylint](http://www.pylint.org/). 7 | It will be used with files that have the "python" syntax. 8 | 9 | 10 | ## Installation 11 | 12 | SublimeLinter must be installed in order to use this plugin. 13 | 14 | Please use [Package Control](https://packagecontrol.io) to install the linter plugin. 15 | 16 | Before using this plugin, ensure that `pylint` (1.0 or later) is installed on your system. 17 | To install `pylint`, do the following: 18 | 19 | 1. Install [Python](http://python.org) and [pip](http://www.pip-installer.org/en/latest/installing.html). If you plan to code in Python 3, you will need to install `pip` for Python 3 as well. 20 | 21 | 1. Install `pylint` by typing the following in a terminal, replacing ‘x’ with the minor version installed on your system: 22 | ```bash 23 | # For python 2.x 24 | [sudo] pip-2.x install pylint 25 | 26 | # For python 3.x 27 | [sudo] pip-3.x install pylint 28 | 29 | # On Windows, for python 2.x 30 | c:\Python2x\Scripts\pip.exe install pylint 31 | 32 | # On Windows, for python 3.x 33 | c:\Python3x\Scripts\pip.exe install pylint 34 | ``` 35 | 36 | Please make sure that the path to `pylint` is available to SublimeLinter. 37 | The docs cover [troubleshooting PATH configuration](http://sublimelinter.com/en/latest/troubleshooting.html#finding-a-linter-executable). 38 | 39 | 40 | ## Settings 41 | 42 | - SublimeLinter settings: http://sublimelinter.com/en/latest/settings.html 43 | - Linter settings: http://sublimelinter.com/en/latest/linter_settings.html 44 | 45 | Pylint can be configured using `.pylintrc` configuration files and inline comments, more information in [the pylint docs](https://pylint.readthedocs.io/en/latest/faq.html#message-control). 46 | -------------------------------------------------------------------------------- /linter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | from SublimeLinter.lint import PythonLinter 4 | 5 | 6 | logger = logging.getLogger('SublimeLinter.plugins.pylint') 7 | 8 | 9 | class Pylint(PythonLinter): 10 | regex = ( 11 | r'^(?P\d+):(?P\d+):' 12 | r'(?P(?:(?P[FE]\d+)|(?P[CIWR]\d+))): ' 13 | r'(?P.*?)(?:\r?$)' 14 | ) 15 | multiline = True 16 | line_col_base = (1, 0) 17 | tempfile_suffix = '-' 18 | defaults = { 19 | # paths to be added to sys.path through --init-hook 20 | 'paths': [], 21 | 'selector': 'source.python', 22 | '--rcfile=': '', 23 | '--init-hook=;': None 24 | } 25 | 26 | def on_stderr(self, stderr): 27 | stderr = re.sub( 28 | 'No config file found, using default configuration\n', '', stderr) 29 | stderr = re.sub('Using config file .+\n', '', stderr) 30 | stderr = re.sub('(?m)^.*DeprecationWarning.*\n.*\n', '', stderr) 31 | 32 | if stderr: 33 | self.notify_failure() 34 | logger.error(stderr) 35 | 36 | def cmd(self): 37 | settings = self.settings 38 | if settings['init-hook'] is None: 39 | paths = settings['paths'] 40 | if paths: 41 | commands = ['import sys'] + [ 42 | "sys.path.append('{}')".format(path) 43 | for path in paths 44 | ] 45 | settings['init-hook'] = commands 46 | 47 | return ( 48 | 'pylint', 49 | '--msg-template=\'{line}:{column}:{msg_id}: {msg} ({symbol})\'', 50 | '--module-rgx=.*', # don't check the module name 51 | '--reports=n', # remove tables 52 | '--persistent=n', # don't save the old score (no sense for temp) 53 | '${args}', 54 | '${file_on_disk}' 55 | ) 56 | 57 | ############# 58 | # Try to extract a meaningful columns. 59 | # multiple cases : 60 | # - the linter can report a meaningful column 61 | # - a 'near' info can be extracted from the message 62 | # - the error always refer to the same keyword 63 | # - the error concerns the line as a whole 64 | # - errors that are confirmed to not be in these cases 65 | 66 | # Errors of type I always report column = 0 67 | 68 | # Already reporting the proper column, use it, even if it is 0 69 | messages_meaningful_column = [ 70 | 'C0321', 71 | 'E0103', 72 | 'E0104', 73 | 'E0105', 74 | 'E0203', 75 | 'E0601', 76 | 'E0602', 77 | 'E0702', 78 | 'E0710', 79 | 'E1004', 80 | 'W0107', 81 | 'W0108', 82 | 'W0110', 83 | 'W0141', 84 | 'W0150', 85 | 'W0233', 86 | 'W0333', 87 | 'W0613', 88 | 'W0631', 89 | 'W0701', 90 | 'W0702', 91 | 'W0703', 92 | 'W0710', 93 | 'W0721', 94 | 'W1001', 95 | ] 96 | 97 | # a mapping of codes to regexp 98 | # they are expected to define the 'near' group 99 | messages_re = { 100 | 'C0102': r'Black listed name "(?P.*)"', 101 | 'C0103': r'Invalid \S+ name "(?P.*)"', 102 | 'C0202': r"Class method (?P.*) should have", 103 | 'C0203': r"Metaclass method (?P.*) should have", 104 | 'C0204': r"Metaclass class method (?P.*) should have", 105 | 'C0301': r"Line too long \(\d+/(?P\d+)\)", 106 | 'C0325': r"Unnecessary parens after '(?P.*)' keyword", 107 | 'E0001': r'unknown encoding: (?P.*)', # can also be 'invalid syntax', 'EOF in multi-line statement' 108 | 'E0011': r"Unrecognized file option '(?P.*)'", 109 | 'E0012': r"Bad option value '(?P.*)'", 110 | 'E0107': r'Use of the non-existent (?P.*) operator', 111 | 'E0108': r"Duplicate argument name (?P.*) in function definition", 112 | 'E0203': r"Access to member '(?P.*)' before its definition", 113 | 'E0603': r"Undefined variable name '(?P.*)' in", 114 | 'E0604': r"Invalid object '(?P.*)' in", 115 | 'E0611': r"No name '(?P.*)' in module", 116 | # may also be: Bad except clauses order (empty except clause should always appear last) 117 | # which is reported on the 'try' -> keep the column info ! 118 | 'E0701': r'Bad except clauses order \(.* is an ancestor class of (?P.*)\)', 119 | 'E0712': r"Catching an exception which doesn't inherit from BaseException: (?P.*)", 120 | 'E1003': r"Bad first argument '(?P.*)' given to super()", 121 | 'E1101': r"has no '(?P.*)' member", 122 | 'E1102': r"(?P.*) is not callable", 123 | 'E1103': r"has no '(?P.*)' member", 124 | 'E1123': r"Passing unexpected keyword argument '(?P.*)' in function call", 125 | 'E1124': r"Parameter '(?P.*)' passed as both positional and keyword argument", 126 | 'E1310': r"Suspicious argument in \S+\.(?P.*) call", 127 | 'F0220': r"failed to resolve interfaces implemented by \S+ \((?P.*)\)", 128 | 'F0401': r"Unable to import '(?P.*)'", 129 | 'I0010': r"Unable to consider inline option '(?P.*)'", 130 | 'I0011': r"Locally disabling (?P.*)", 131 | 'I0012': r"Locally enabling (?P.*)", 132 | 'W0102': r'Dangerous default value (?P\S*) (\(.*\) )?as argument', 133 | 'W0106': r'Expression "\((?P.*)\)" is assigned to nothing', # FIXME regex too greedy ? 134 | 'W0201': r"Attribute '(?P.*)' defined outside __init__", 135 | 'W0211': r"Static method with '(?P.*)' as first argument", 136 | 'W0212': r"Access to a protected member (?P.*) of a client class", 137 | 'W0402': r"Uses of a deprecated module '(?P.*)'", 138 | 'W0403': r"Relative import '(?P.*)', should be", 139 | 'W0404': r"Reimport '(?P.*)'", 140 | 'W0511': r"(?P.*)", 141 | 'W0512': r'Cannot decode using encoding ".*", unexpected byte at position (?P\d+)', 142 | 'W0601': r"Global variable '(?P.*)' undefined", 143 | 'W0602': r"Using global for '(?P.*)' but", 144 | 'W0611': r"Unused import (?P.*)", 145 | 'W0621': r"Redefining name '(?P.*)' from outer scope", 146 | 'W0622': r"Redefining built-in '(?P.*)'", 147 | 'W0623': r"Redefining name '(?P.*)' from object '.*' in exception handler", 148 | 'W0711': r'Exception to catch is the result of a binary "(?P.*)" operation', 149 | 'W1401': r"Anomalous backslash in string: '(?P.*)'", # does not work with \o, ... 150 | 'W1402': r"Anomalous Unicode escape in byte string: '(?P.*)'", # does not work with \u, \U 151 | 'W1501': r'"(?P.*)" is not a valid mode for open', 152 | 153 | # does not work : 'l' is too short.. 154 | # 'W0332': r'Use of "(?P.*)" as long integer identifier', 155 | } 156 | # a static map of codes to 'near' words : 157 | # some errors always relate to the same keyword. 158 | messages_near = { 159 | 'C1001': 'class', # adequately reported at column 0, converted to None 160 | 'E0100': '__init__', 161 | 'E0101': '__init__', 162 | 'E0106': 'return', 163 | 'E0235': '__exit__', 164 | 'E0711': 'NotImplemented', 165 | 'E1111': '=', 166 | 'I0014': 'disable', 167 | 'I0022': '-msg', # 'disable-msg' or 'enable-msg'. TODO find a way to highlight both 168 | 'W0122': 'exec', 169 | 'W0142': '*', 170 | 'W0231': '__init__', 171 | 'W0234': '__iter__', # or 'next'. TODO find a way to handle both 172 | 'W0301': ';', 173 | 'W0331': '<>', 174 | 'W0401': 'import *', 175 | 'W0410': '__future__', # reported at column 0, converted to None 176 | 'W0603': 'global', 177 | 'W0604': 'global', 178 | 'W0614': 'import *', 179 | 'W1111': '=', 180 | 'W1201': '%', 181 | } 182 | 183 | # Could not be generated: 184 | # C0121, C0303, C0304, 185 | # E0604, 186 | # F0002, F0003, F0004, F0010, F0202, 187 | # I0001, I0020, I0021, 188 | # W0232, W0404, W0704 189 | 190 | # Confirmed to not report a meaningful column, and to not allow for 'near' extraction 191 | # They are typically about a whole class or method. 192 | # These will be forced to None. 193 | messages_no_column = [ 194 | 'C0111', # mssing docstring for modules, classes and methods 195 | 'C0112', # empty docstring for modules, classes and methods 196 | 'C0302', 197 | 'C0303', 198 | 'C0304', 199 | # 'C0326', # special case TODO find a way to use the next 2 lines on 200 | # the report, which shows the position of the error. 201 | 'C1001', 202 | 'E0001', 203 | 'E0102', 204 | 'E0202', 205 | 'E0211', 206 | 'E0213', 207 | 'E0221', 208 | 'E0222', 209 | 'E1001', 210 | 'E1002', 211 | 'E1120', 212 | 'E1121', 213 | 'E1125', 214 | 'E1200', 215 | 'E1201', 216 | 'E1205', 217 | 'E1206', 218 | 'E1300', 219 | 'E1301', 220 | 'E1302', 221 | 'E1303', 222 | 'E1304', 223 | 'E1305', 224 | 'E1306', 225 | 'I0013', 226 | 'R0902', 227 | 'R0903', 228 | 'R0911', 229 | 'R0912', 230 | 'R0914', 231 | 'W0101', 232 | 'W0104', 233 | 'W0105', 234 | 'W0109', # on a multiline dict, it is reported on the assignment line 235 | 'W0120', 236 | 'W0121', 237 | 'W0199', 238 | 'W0221', 239 | 'W0222', 240 | 'W0223', 241 | 'W0232', 242 | 'W0311', 243 | 'W0312', 244 | 'W0406', 245 | 'W0632', 246 | 'W0633', 247 | 'W0712', 248 | 'W1300', 249 | 'W1301', 250 | ] 251 | 252 | def split_match(self, match): 253 | """ 254 | Return the components of the error message. 255 | 256 | We override this to deal with the idiosyncracies of pylint's error messages. 257 | """ 258 | match, line, col, error, warning, message, near = super().split_match(match) 259 | 260 | if match: 261 | code = match.group('code') 262 | if code in self.messages_near: 263 | near = self.messages_near[code] 264 | col = None 265 | elif code in self.messages_re: 266 | message_match = re.search(self.messages_re[code], message) 267 | if message_match: 268 | if 'near' in message_match.groupdict(): 269 | # 'near' will be more precise than 'col' 270 | near = message_match.group('near') 271 | col = None 272 | elif 'col' in message_match.groupdict(): 273 | col = int(message_match.group('col')) 274 | # else: keeping the column info anyway, useful for E0701 275 | elif code == 'C0326': 276 | if 'assignment' in message: 277 | near = '=' 278 | # there are other cases like 'comparison', but it's harder 279 | # to determine a 'near' value in that case 280 | elif code in self.messages_no_column: 281 | col = None 282 | else: 283 | # if it is an unknown error code, force it if column = 0 284 | if col == 0: 285 | col = None 286 | 287 | return match, line, col, error, warning, message, near 288 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.txt", 3 | "1.4.0": "messages/1.4.0.txt", 4 | "1.5.0": "messages/1.5.0.txt" 5 | } 6 | -------------------------------------------------------------------------------- /messages/1.3.0.txt: -------------------------------------------------------------------------------- 1 | SublimeLinter-pylint 1.3.0 2 | -------------------------- 3 | 4 | * New linting mode : the plugin will use the real file instead of a temp file 5 | whenever possible. The temp file will only be used on an unsaved file, when 6 | SublimeLinter is in background linting mode. 7 | This allows pylint to correctly detect relative imports. 8 | 9 | * Whenever possible, errors are now positioned at the correct column 10 | 11 | * New options : 12 | - show-codes : display the pylint error code next to the message in the status bar 13 | - paths : a list of paths that will be added to the sys.path for pylint to look for modules 14 | -------------------------------------------------------------------------------- /messages/1.4.0.txt: -------------------------------------------------------------------------------- 1 | SublimeLinter-pylint 1.4.0 2 | -------------------------- 3 | 4 | Removed the args pair `disable/enable`. `disable` was disfunctional with the release of SublimeLinter 4 anyways. Use the generic `args` setting instead. 5 | 6 | See: http://www.sublimelinter.com/en/stable/linter_settings.html#args -------------------------------------------------------------------------------- /messages/1.5.0.txt: -------------------------------------------------------------------------------- 1 | SublimeLinter-pylint 1.5.0 2 | -------------------------- 3 | 4 | Pylint now only runs on the actual files, i.e. on load and on save. 5 | 6 | This ensures it runs in a context where imports are available etc. Previously, 7 | it would try to switch behaviour on the fly, which would result in different 8 | errors depending on how pylint was run. 9 | -------------------------------------------------------------------------------- /messages/install.txt: -------------------------------------------------------------------------------- 1 | SublimeLinter-pylint 2 | ------------------------------- 3 | This linter plugin for SublimeLinter provides an interface to pylint. 4 | 5 | Please read the installation instructions at: 6 | 7 | https://github.com/SublimeLinter/SublimeLinter-pylint 8 | --------------------------------------------------------------------------------