-
Notifications
You must be signed in to change notification settings - Fork 24
/
linter.py
318 lines (257 loc) · 10.6 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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
#
# linter.py
# Linter for SublimeLinter3, a code checking framework for Sublime Text 3
#
# Written by Clifton Kaznocha
# Copyright (c) 2014 Clifton Kaznocha
#
# License: MIT
#
"""This module exports the Flow plugin class."""
import logging
import json
import re
from itertools import chain, repeat
from SublimeLinter.lint import NodeLinter
logger = logging.getLogger("SublimeLinter.plugin.flow")
class Flow(NodeLinter):
"""Provides an interface to flow."""
cmd = ['flow', 'check-contents', '$file', '${args}', '--json']
defaults = {
'selector': 'source.js',
# Allow to bypass the 50 errors cap
'--show-all-errors': True,
# Run against *all* files regardless of `@flow` comment
'all': False,
# Show flow coverage warnings
'coverage': False
}
__flow_near_re = '`(?P<near>[^`]+)`'
def run(self, cmd, code):
"""
Flow lint code if `@flow` pragma is present.
if not present, this method noops
"""
_flow_comment_re = r'\@flow'
if not (
re.search(_flow_comment_re, code)
or self.settings['all']
):
logger.info("did not find @flow pragma")
return ''
logger.info("found flow pragma!")
check = super().run(cmd, code)
coverage = (
super().run(_build_coverage_cmd(cmd), code)
if self.settings['coverage']
else '{}'
)
return '[%s,%s]' % (check, coverage)
def _error_to_tuple(self, error):
"""
Map an array of flow error messages to a fake regex match tuple.
this is described in `flow/tsrc/flowResult.js` in the flow repo
flow returns errors like this
type FlowError = {
kind: string,
level: string,
message: Array<FlowMessage>,
trace: ?Array<FlowMessage>,
operation?: FlowMessage,
extra?: FlowExtra,
};
type FlowMessage = {
descr: string,
type: "Blame" | "Comment",
context?: ?string,
loc?: ?FlowLoc,
indent?: number,
};
Which means we can mostly avoid dealing with regex parsing since the
flow devs have already done that for us. Thanks flow devs!
"""
error_messages = error.get('message', [])
# TODO(nsfmc): `line_col_base` won't work b/c we avoid `split_match`'s
# codepath
operation = error.get('operation', {})
loc = operation.get('loc') or error_messages[0].get('loc', {})
message = self._find_matching_msg_for_file(error)
if message is None:
return (False, 0, 0, False, False, '', None)
error_context = message.get('context', '')
loc = message.get('loc')
message_start = loc.get('start', {})
message_end = loc.get('end', {})
line = message_start.get('line', None)
if line:
line -= 1
col = message_start.get('column', None)
if col:
col -= 1
end = message_end.get('column', None)
# slice the error message from the context and loc positions
# If error spans multiple lines, though, don't highlight them all
# but highlight the 1st error character by passing None as near
# SublimeLinter will strip quotes of `near` strings as documented in
# http://www.sublimelinter.com/en/latest/linter_attributes.html#regex
# In order to preserve quotes, we have to wrap strings with more
# quotes.
if end and line == (message_end.get('line') - 1):
near = '"' + error_context[col:end] + '"'
else:
near = None
kind = error.get('kind', False)
level = error.get('level', False)
error = kind if level == 'error' else False
warning = kind if level == 'warning' else False
combined_message = " ".join(
[self._format_message(msg) for msg in error_messages]
).strip()
logger.info('flow line: {}, col: {}, level: {}, message: {}'.format(
line, col, level, combined_message))
return (True, line, col, error, warning, combined_message, near)
def _find_matching_msg_for_file(self, flow_error):
"""
Find the first match for the current file.
Flow errors might point to other files, and have the current file only
deep in additional information of the top level error.
The error format is described in `tsrc/flowResult.js` in the flow repo:
type FlowError = {
kind: string,
level: string,
message: Array<FlowMessage>,
trace: ?Array<FlowMessage>,
operation?: FlowMessage,
extra?: FlowExtra,
};
type FlowMessage = {
descr: string,
type: "Blame" | "Comment",
context?: ?string,
loc?: ?FlowLoc,
indent?: number,
};
type FlowExtra = Array<{
message: Array<FlowMessage>,
children: FlowExtra,
}>
"""
messages = chain(
(flow_error['operation'],) if 'operation' in flow_error else (),
flow_error['message'],
_traverse_extra(flow_error.get('extra')),
)
for message in messages:
source = message.get('loc', {}).get('source')
if source == self.filename:
return message
def _format_message(self, flow_message):
"""
Format sequences of error messages depending on their type.
comments typically contains text linking text describing the
type of error violation
blame messages are typically code snippets with a `descr` that
identifies the failing error type and a loc that identifies the
snippet that triggered the error (would typically be underlined
with ^^^^^^ in terminal invocations)
if possible, will try to reduce the context message (which may
already be highlighted by the linter) to the `parameter (Type)`
so that status bar messages will read like
foo (String) This type is incompatible with expectedFoo (Number)
"""
msg_type = flow_message.get('type')
if msg_type == 'Comment':
return flow_message.get('descr', '').strip()
if msg_type == 'Blame':
snippet = flow_message.get('context', '')
if snippet is None:
return ""
loc = flow_message.get('loc', {})
if loc:
start = loc.get('start', {}).get('column', 1) - 1
end = loc.get('end', {}).get('column', len(snippet))
error_descr = flow_message.get('descr').strip()
error_string = snippet[start:end]
if (error_string != error_descr):
snippet = '{} ({})'.format(error_string, error_descr)
return snippet.strip()
def _uncovered_to_tuple(self, uncovered, uncovered_lines):
"""
Map an array of flow coverage locations to a fake regex match tuple.
Since flow produces JSON output, there is no need to match error
messages against regular expressions.
"""
match = self.filename == uncovered.get('source')
line = uncovered['start']['line'] - 1
col = uncovered['start']['column'] - 1
error = False
warning = 'coverage'
# SublimeLinter only uses the length of `near` if we provide the column
# That's why we can get away with a string of the right length.
near = ' ' * (
uncovered['end']['offset'] - uncovered['start']['offset']
)
message = '\u3003' # ditto mark
if line not in uncovered_lines:
message = 'Code is not covered by Flow (any type)'
uncovered_lines.add(line)
return (match, line, col, error, warning, message, near)
def _empty_to_tuple(self, empty, empty_lines):
"""
Map an array of flow coverage locations to a fake regex match tuple.
Since flow produces JSON output, there is no need to match error
messages against regular expressions.
"""
match = self.filename == empty.get('source')
line = empty['start']['line'] - 1
col = empty['start']['column'] - 1
error = False
warning = 'coverage'
# SublimeLinter only uses the length of `near` if we provide the column
# That's why we can get away with a string of the right length.
near = ' ' * (
empty['end']['offset'] - empty['start']['offset']
)
message = '\u3003' # ditto mark
if line not in empty_lines:
message = 'Code is not covered by Flow (empty type)'
empty_lines.add(line)
return (match, line, col, error, warning, message, near)
def find_errors(self, output):
"""
Convert flow's json output into a set of matches SublimeLinter can process.
I'm not sure why find_errors isn't exposed in SublimeLinter's docs, but
this would normally attempt to parse a regex and then return a generator
full of sanitized matches. Instead, this implementation returns a list
of errors processed by _error_to_tuple, ready for SublimeLinter to unpack
"""
try:
# calling flow in a matching syntax without a `flowconfig` will cause the
# output of flow to be an error message. catch and return []
check, coverage = json.loads(output)
except ValueError:
logger.info('flow {}'.format(output))
return []
errors = check.get('errors', [])
logger.info('flow {} errors. passed: {}'.format(
len(errors), check.get('passed', True)
))
return chain(
map(self._error_to_tuple, errors),
map(self._uncovered_to_tuple,
coverage.get('expressions', {}).get('uncovered_locs', []),
repeat(set())),
map(self._empty_to_tuple,
coverage.get('expressions', {}).get('empty_locs', []),
repeat(set()))
)
def _traverse_extra(flow_extra):
"""Yield all messages in `flow_extra.message` and `flow_extra.childre.message`."""
if flow_extra is None:
return
for x in flow_extra:
yield from x.get('message')
yield from _traverse_extra(x.get('children'))
def _build_coverage_cmd(cmd):
"""Infer the correct `coverage` command from the `check-contents` command."""
return cmd[:cmd.index('check-contents')] + ['coverage', '--path', '@', '--json']