-
Notifications
You must be signed in to change notification settings - Fork 28
/
linter.py
203 lines (162 loc) · 6.84 KB
/
linter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
import logging
from SublimeLinter.lint import PythonLinter
import re
logger = logging.getLogger('SublimeLinter.plugins.flake8')
CAPTURE_WS = re.compile(r'(\s+)')
CAPTURE_IMPORT_ID = re.compile(r'^\'(?:.*\.)?(.+)\'')
CAPTURE_F403_HINT = re.compile(r"'(.*)?'")
# Following codes are marked as errors. All other codes are marked as warnings.
error_codes = {
line.split(' ', 1)[0]
for line in (
# Pyflake Errors:
'F402 import module from line N shadowed by loop variable',
'F404 future import(s) name after other statements',
'F812 list comprehension redefines name from line N',
'F823 local variable name ... referenced before assignment',
'F831 duplicate argument name in function definition',
'F821 undefined name name',
'F822 undefined name name in __all__',
# Pep8 Errors:
'E112 expected an indented block',
'E113 unexpected indentation',
'E901 SyntaxError or IndentationError',
'E902 IOError',
'E999 SyntaxError',
)
}
class Flake8(PythonLinter):
cmd = ('flake8', '--format', 'default', '${args}', '-')
defaults = {
'selector': 'source.python',
# Ignore codes Sublime can auto-fix
'ignore_fixables': True
}
regex = (
r'^.+?:(?P<line>\d+):(?P<col>\d+): '
r'(?P<code>\w+\d+):? '
r'(?P<message>.*)'
)
multiline = True
default_type = 'warning'
def on_stderr(self, stderr):
# For python 3.7 we actually have the case that flake yields
# FutureWarnings. We just eat those as they're irrelevant here. Note
# that we try to eat the subsequent line as well which usually contains
# the culprit source line.
stderr = re.sub(r'^.+FutureWarning.+\n(.*\n?)?', '', stderr, re.M)
stderr = re.sub(r'^.+DeprecationWarning.+\n(.*\n?)?', '', stderr, re.M)
if stderr:
self.notify_failure()
logger.error(stderr)
def split_match(self, match):
error = super().split_match(match)
if error['code'] in error_codes:
error['error_type'] = 'error'
return error
def parse_output(self, proc, virtual_view):
errors = super().parse_output(proc, virtual_view)
if not self.settings.get('ignore_fixables', True):
return errors
trims_ws = self.view.settings().get('trim_trailing_white_space_on_save')
ensures_newline = self.view.settings().get('ensure_newline_at_eof_on_save')
if not (trims_ws or ensures_newline):
return errors
filtered_errors = []
for error in errors:
code = error['code']
if ensures_newline and code == 'W292':
continue
if trims_ws and code in ('W291', 'W293'):
continue
if trims_ws and code == 'W391':
# Fixable if one WS line at EOF, or the view only has one line.
lines = len(virtual_view._newlines) - 1
if (
virtual_view.select_line(lines - 1).strip() == ''
and (
lines < 2
or virtual_view.select_line(lines - 2).strip() != ''
)
):
continue
filtered_errors.append(error)
return filtered_errors
def reposition_match(self, line, col, m, virtual_view):
"""Reposition white-space errors."""
code = m.code
if code in ('W291', 'W293', 'E501'):
txt = virtual_view.select_line(line).rstrip('\n')
return (line, col, len(txt))
if code.startswith('E1'):
return (line, 0, col)
if code in ('E262', 'E265'):
txt = virtual_view.select_line(line).rstrip('\n')
match = CAPTURE_WS.match(txt[col + 1:])
if match is not None:
length = len(match.group(1))
return (line, col, col + length + 1)
if code.startswith('E266'):
txt = virtual_view.select_line(line).rstrip('\n')
tail_text = txt[col:]
count_comment_sign = len(tail_text) - len(tail_text.lstrip("#"))
return (line, col, col + count_comment_sign)
if code.startswith('E2'):
txt = virtual_view.select_line(line).rstrip('\n')
match = CAPTURE_WS.match(txt[col:])
if match is not None:
length = len(match.group(1))
return (line, col, col + length)
if code in ('E302', 'E305'):
return line - 1, 0, 1
if code == 'E303':
match = re.match(r'too many blank lines \((\d+)', m.message.strip())
if match is not None:
count = int(match.group(1))
starting_line = line - count
return (
starting_line,
0,
sum(
len(virtual_view.select_line(_line))
for _line in range(starting_line, line)
)
)
if code == 'E999':
txt = virtual_view.select_line(line).rstrip('\n')
last_col = len(txt)
if col + 1 == last_col:
return line, last_col, last_col
if code == 'F401':
# Typical message from flake is "'x.y.z' imported but unused"
# The import_id will be 'z' in that case.
# Since, it is usual to spread imports on multiple lines, we
# search MAX_LINES for `import_id` starting with the reported line.
MAX_LINES = 30
match = CAPTURE_IMPORT_ID.search(m.message)
if match:
import_id = match.group(1)
if import_id == '*':
pattern = re.compile(r'(\*)')
else:
pattern = re.compile(r'\b({})\b'.format(re.escape(import_id)))
last_line = len(virtual_view._newlines) - 1
for _line in range(line, min(line + MAX_LINES, last_line)):
txt = virtual_view.select_line(_line)
# Take the right most match, to count for
# 'from util import util'
matches = list(pattern.finditer(txt))
if matches:
match = matches[-1]
return _line, match.start(1), match.end(1)
# Fallback, and mark the line.
col = None
if code == 'F403':
txt = virtual_view.select_line(line).rstrip('\n')
match = CAPTURE_F403_HINT.search(m.message)
if match:
hint = match.group(1)
start = txt.find(hint)
if start >= 0:
return line, start + len(hint) - 1, start + len(hint)
return super().reposition_match(line, col, m, virtual_view)