Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: flake8-abp/flake8_abp.py

Issue 29340727: Noissue - Added flake8 extension accounting for our coding style and some other stuff (Closed)
Patch Set: Added more checks, tests, README and addressed comments Created May 7, 2016, 5:13 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # This file is part of Adblock Plus <https://adblockplus.org/>,
2 # Copyright (C) 2006-2016 Eyeo GmbH
3 #
4 # Adblock Plus is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License version 3 as
6 # published by the Free Software Foundation.
7 #
8 # Adblock Plus is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
15
16 import ast
17 import re
18 import tokenize
19 import sys
20 import collections
21
22 try:
23 import builtins
24 except ImportError:
25 import __builtin__ as builtins
26
27 import pkg_resources
28
29 __version__ = pkg_resources.get_distribution('flake8-abp').version
30
31 DISCOURAGED_APIS = {
32 're.match': 're.search',
33 'codecs.open': 'io.open',
34 }
35
36 ESSENTIAL_BUILTINS = set(dir(builtins)) - {'apply', 'basestring', 'buffer',
Vasily Kuznetsov 2016/05/09 13:39:51 What were the criteria for deciding what is an ess
Sebastian Noack 2016/05/09 16:49:11 The names explicitly excluded were the builtins th
Vasily Kuznetsov 2016/05/09 17:37:32 Yep, makes sense. It would be good to record it so
Sebastian Noack 2016/05/11 10:50:09 I think the general rule for the style guide shoul
Vasily Kuznetsov 2016/05/11 14:09:17 Ok, I think saying that builtins should be overrid
37 'cmp', 'coerce', 'execfile',
38 'file', 'intern', 'long',
39 'raw_input', 'reduce', 'reload',
40 'unichr', 'unicode', 'xrange'}
41
42 LEAVE_BLOCK = (ast.Return, ast.Raise, ast.Continue, ast.Break)
43 VOLATILE = object()
44
45
46 def evaluate(node):
47 try:
48 return eval(compile(ast.Expression(node), '', 'eval'), {})
49 except Exception:
Vasily Kuznetsov 2016/05/09 13:39:51 As I understand this exception would be thrown whe
Sebastian Noack 2016/05/09 16:49:11 Yes, it can be one of a wide range (if not any) ex
Vasily Kuznetsov 2016/05/09 17:37:32 I see. I just got a bit confused by this being cal
50 return VOLATILE
51
52
53 def is_const(node):
54 return evaluate(node) is not VOLATILE
55
56
57 def get_identifier(node):
58 if isinstance(node, ast.Name):
59 return node.id
60 if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name):
61 return '{}.{}'.format(node.value.id, node.attr)
62
63
64 def get_statement(node):
65 return type(node).__name__.lower()
66
67
68 class TreeVisitor(ast.NodeVisitor):
69 Scope = collections.namedtuple('Scope', ['node', 'names', 'globals'])
70
71 def __init__(self):
72 self.errors = []
73 self.scope_stack = []
74
75 def _visit_block(self, nodes, block_required=False,
76 nodes_required=True, docstring=False):
77 pass_node = None
78 has_non_pass = False
79 leave_node = None
80 dead_code = False
81
82 for i, node in enumerate(nodes):
83 if isinstance(node, ast.Pass):
84 pass_node = node
85 else:
86 has_non_pass = True
87
88 if leave_node and not dead_code:
89 dead_code = True
90 statement = get_statement(leave_node)
91 self.errors.append((node, 'A202 dead code after '
92 '{}'.format(statement)))
93
94 if isinstance(node, LEAVE_BLOCK):
95 leave_node = node
96
97 if not isinstance(node, ast.Expr):
98 continue
99 if docstring and i == 0 and isinstance(node.value, ast.Str):
100 continue
101 if isinstance(node.value, (ast.Call, ast.Yield)):
102 continue
103
104 self.errors.append((node, 'A203 unused expression'))
105
106 if pass_node:
107 if not nodes_required or len(nodes) > 1:
108 self.errors.append((pass_node, 'A204 redundant '
109 'pass statement'))
110
111 if not block_required and not has_non_pass:
112 self.errors.append((pass_node, 'A205 empty block'))
113
114 def _check_redundant_else(self, node, handlers, clause):
115 if not node.orelse:
116 return
117
118 for handler in handlers:
119 for child in handler.body:
120 if isinstance(child, LEAVE_BLOCK):
121 leave_node = child
122 break
123 else:
124 return
125
126 statement = get_statement(leave_node)
127 self.errors.append((node.orelse[0],
128 'A206 redundant else statement after {} '
129 'in {}-clause'.format(statement, clause)))
130
131 def visit_If(self, node):
132 self._visit_block(node.body, block_required=bool(node.orelse))
133 self._visit_block(node.orelse)
134 self._check_redundant_else(node, [node], 'if')
135 self.generic_visit(node)
136
137 def visit_Try(self, node):
138 self._visit_block(node.body)
139 self._visit_block(node.orelse)
140 self._visit_block(node.finalbody)
141 self._check_redundant_else(node, node.handlers, 'except')
142 self.generic_visit(node)
143
144 def visit_TryExcept(self, node):
145 self._visit_block(node.body)
146 self._visit_block(node.orelse)
147 self._check_redundant_else(node, node.handlers, 'except')
148 self.generic_visit(node)
149
150 def visit_TryFinally(self, node):
151 self._visit_block(node.body)
152 self._visit_block(node.finalbody)
153 self.generic_visit(node)
154
155 def visit_ExceptHandler(self, node):
156 self._visit_block(node.body, block_required=True)
157 self.generic_visit(node)
158
159 def _visit_stored_name(self, node, name):
160 scope = self.scope_stack[-1]
161 scope.names.add(name)
162
163 if name in ESSENTIAL_BUILTINS and not isinstance(scope.node,
164 ast.ClassDef):
165 self.errors.append((node, 'A302 redefined built-in ' + name))
166
167 def visit_Name(self, node):
168 if isinstance(node.ctx, ast.Store):
169 self._visit_stored_name(node, node.id)
170
171 def _visit_with_scope(self, node):
172 scope = self.Scope(node, names=set(), globals=[])
173 self.scope_stack.append(scope)
174 self.generic_visit(node)
175 del self.scope_stack[-1]
176 return scope
177
178 def visit_Module(self, node):
179 self._visit_block(node.body, block_required=True,
180 nodes_required=False, docstring=True)
181 self._visit_with_scope(node)
182
183 def visit_FunctionDef(self, node):
184 self._visit_stored_name(node, node.name)
185 self._visit_block(node.body, block_required=True, docstring=True)
186
187 scope = self._visit_with_scope(node)
188 global_names = set()
189
190 for declaration in scope.globals:
191 for name in declaration.names:
192 if name not in scope.names or name in global_names:
193 statement = get_statement(declaration)
194 self.errors.append((declaration,
195 'A201 redundant {} declaration for '
196 '{}'.format(statement, name)))
197 else:
198 global_names.add(name)
199
200 visit_ClassDef = visit_FunctionDef
201
202 def visit_Global(self, node):
203 scope = self.scope_stack[-1]
204 scope.globals.append(node)
205
206 if isinstance(scope.node, ast.Module):
207 statement = get_statement(node)
208 self.errors.append((node, 'A201 {} declaration on '
209 'top-level'.format(statement)))
210
211 visit_Nonlocal = visit_Global
212
213 def _visit_iter(self, node):
214 if isinstance(node, (ast.Tuple, ast.Set)):
215 self.errors.append((node, 'A101 use lists for data '
216 'that have order'))
217
218 def visit_comprehension(self, node):
219 self._visit_iter(node.iter)
220 self.generic_visit(node)
221
222 def visit_For(self, node):
223 self._visit_iter(node.iter)
224 self._visit_block(node.body, block_required=True)
225 self._visit_block(node.orelse)
226 self.generic_visit(node)
227
228 def visit_While(self, node):
229 self._visit_block(node.body, block_required=True)
230 self._visit_block(node.orelse)
231 self.generic_visit(node)
232
233 def visit_BinOp(self, node):
234 if isinstance(node.op, ast.Mod) and isinstance(node.left, ast.Str):
235 self.errors.append((node, 'A107 use format() instead of '
236 '% operator for string formatting'))
237
238 multi_addition = (isinstance(node.op, ast.Add) and
239 isinstance(node.left, ast.BinOp) and
240 isinstance(node.left.op, ast.Add))
241 if multi_addition and (isinstance(node.left.left, ast.Str) or
242 isinstance(node.left.right, ast.Str) or
243 isinstance(node.right, ast.Str)):
244 self.errors.append((node, 'A108 use format() instead of '
245 '+ operator when concatenating '
246 '>2 strings'))
Vasily Kuznetsov 2016/05/09 13:39:51 Maybe write it out: "more than two strings"? It's
Sebastian Noack 2016/05/09 16:49:11 I had it spelled out initially, but the message is
Vasily Kuznetsov 2016/05/09 17:37:32 I see. I don't feel too strongly about it, so up t
247
248 self.generic_visit(node)
249
250 def visit_Compare(self, node):
251 left = node.left
252 single = len(node.ops) == 1
253
254 for op, right in zip(node.ops, node.comparators):
255 membership = isinstance(op, (ast.In, ast.NotIn))
256 symmetric = isinstance(op, (ast.Eq, ast.NotEq, ast.Is, ast.IsNot))
257
258 if membership and isinstance(right, (ast.Tuple, ast.List)):
259 self.errors.append((right, 'A102 use sets for distinct '
260 'unordered data'))
261
262 consts_first = single and not membership or symmetric
263 if consts_first and is_const(left) and not is_const(right):
264 self.errors.append((left, 'A103 yoda condition'))
265
266 left = right
267
268 self.generic_visit(node)
269
270 def _check_deprecated(self, node, name):
271 substitute = DISCOURAGED_APIS.get(name)
272 if substitute:
273 self.errors.append((node, 'A301 use {}() instead '
Vasily Kuznetsov 2016/05/09 13:39:51 instead -> instead of
Sebastian Noack 2016/05/09 16:49:12 Done.
274 '{}()'.format(substitute, name)))
275
276 def visit_Call(self, node):
277 func = get_identifier(node.func)
278 arg = next(iter(node.args), None)
279 redundant_literal = False
280
281 if isinstance(arg, ast.Lambda) and func in {'map', 'filter',
282 'imap', 'ifilter',
283 'itertools.imap',
284 'itertools.ifilter'}:
285 self.errors.append((node, 'A104 use a comprehension '
286 'instead calling {}() with '
Vasily Kuznetsov 2016/05/09 13:39:50 instead -> instead of
Sebastian Noack 2016/05/09 16:49:11 Done.
287 'lambda function'.format(func)))
288 elif isinstance(arg, (ast.List, ast.Tuple)):
289 if func == 'dict':
290 redundant_literal = all(isinstance(elt, (ast.Tuple, ast.List))
291 for elt in arg.elts)
292 else:
293 redundant_literal = func in {'list', 'set', 'tuple'}
294 elif isinstance(arg, (ast.ListComp, ast.GeneratorExp)):
295 if func == 'dict':
296 redundant_literal = isinstance(arg.elt, (ast.Tuple, ast.List))
297 else:
298 redundant_literal = func in {'list', 'set'}
299
300 if redundant_literal:
301 self.errors.append((node, 'A105 use a {0} literal or '
302 'comprehension instead calling '
Vasily Kuznetsov 2016/05/09 13:39:51 instead -> instead of
Sebastian Noack 2016/05/09 16:49:11 Done.
303 '{0}()'.format(func)))
304
305 self._check_deprecated(node, func)
306 self.generic_visit(node)
307
308 def visit_Import(self, node):
309 for alias in node.names:
310 self._visit_stored_name(node, alias.asname or alias.name)
311
312 if hasattr(node, 'module'):
313 self._check_deprecated(node, '{}.{}'.format(node.module,
314 alias.name))
315
316 visit_ImportFrom = visit_Import
317
318 def visit_Assign(self, node):
319 if isinstance(node.value, ast.BinOp) and len(node.targets) == 1:
320 target = node.targets[0]
321 left_is_target = (isinstance(target, ast.Name) and
322 isinstance(node.value.left, ast.Name) and
323 target.id == node.value.left.id)
324 if left_is_target:
325 self.errors.append((node, 'A106 use augment assignment, '
326 'e.g. x += y instead x = x + y'))
327 self.generic_visit(node)
328
329 def _visit_hash_keys(self, nodes, what):
330 keys = []
331 for node in nodes:
332 key = evaluate(node)
333 if key is VOLATILE:
334 continue
335
336 if key in keys:
337 self.errors.append((node, 'A207 duplicate ' + what))
338 continue
339
340 keys.append(key)
341
342 def visit_Dict(self, node):
343 self._visit_hash_keys(node.keys, 'key in dict')
344
345 def visit_Set(self, node):
346 self._visit_hash_keys(node.elts, 'item in set')
347
348
349 class ASTChecker(object):
350 name = 'abp'
351 version = __version__
352
353 def __init__(self, tree, filename):
354 self.tree = tree
355
356 def run(self):
357 visitor = TreeVisitor()
358 visitor.visit(self.tree)
359
360 for node, error in visitor.errors:
361 yield (node.lineno, node.col_offset, error, type(self))
362
363
364 def check_non_default_encoding(physical_line, line_number):
365 if line_number <= 2 and re.search(r'^\s*#.*coding[:=]', physical_line):
366 return (0, 'A303 non-default file encoding')
367
368 check_non_default_encoding.name = 'abp-non-default-encoding'
369 check_non_default_encoding.version = __version__
370
371
372 def check_quotes(logical_line, tokens, previous_logical):
373 first_token = True
374
375 for kind, token, start, end, _ in tokens:
376 if kind == tokenize.INDENT or kind == tokenize.DEDENT:
377 continue
378
379 if kind == tokenize.STRING:
380 match = re.search(r'^(u)?(b)?(r)?((""")?.*)$',
381 token, re.IGNORECASE | re.DOTALL)
382 (is_unicode, is_bytes, is_raw,
383 literal, has_doc_quotes) = match.groups()
384
385 if first_token and re.search(r'^(?:(?:def|class)\s|$)',
386 previous_logical):
387 if not has_doc_quotes:
388 yield (start, 'A109 use triple double '
389 'quotes for docstrings')
390 elif is_unicode or is_bytes or is_raw:
391 yield (start, "A109 don't use u'', b'' "
392 "or r'' for doc strings")
393 elif start[0] == end[0]:
394 if is_raw:
395 literal = re.sub(r'\\(?!{})'.format(literal[0]),
396 '\\\\\\\\', literal)
397
398 if sys.version_info[0] >= 3:
399 if is_bytes:
400 literal = 'b' + literal
401 else:
402 literal = re.sub(r'(?<!\\)\\x(?!a[0d])([a-f][0-9a-f])',
403 lambda m: chr(int(m.group(1), 16)),
404 literal)
405 elif is_unicode:
406 literal = 'u' + literal
407
408 if repr(eval(literal)) != literal:
409 yield (start, "A110 string literal doesn't match repr()")
Vasily Kuznetsov 2016/05/09 13:39:51 Maybe a bit clearer: "string literal doesn't match
Sebastian Noack 2016/05/09 16:49:12 I think that would be inaccurate. A string (litera
Vasily Kuznetsov 2016/05/09 17:37:32 So is it about the difference between a string and
Sebastian Noack 2016/05/11 10:50:09 Yeah, I disagree. But also note that on Python 3 t
Vasily Kuznetsov 2016/05/11 14:09:17 Yeah, I agree with you that the proposed Python 3
410
411 first_token = False
412
413 check_quotes.name = 'abp-quotes'
414 check_quotes.version = __version__
415
416
417 def check_redundant_parenthesis(logical_line, tokens):
418 start_line = tokens[0][2][0]
419 level = 0
420 statement = None
421
422 for i, (kind, token, _, end, _) in enumerate(tokens):
423 if kind == tokenize.INDENT or kind == tokenize.DEDENT:
424 continue
425
426 if statement is None:
427 # logical line doesn't start with an if, elif or while statement
428 if kind != tokenize.NAME or token not in {'if', 'elif', 'while'}:
429 break
430
431 # expression doesn't start with parenthesis
432 next_token = tokens[i + 1]
433 if next_token[:2] != (tokenize.OP, '('):
434 break
435
436 # expression is empty tuple
437 if tokens[i + 2][:2] == (tokenize.OP, ')'):
438 break
439
440 statement = token
441 pos = next_token[2]
442 continue
443
444 # expression ends on a different line, parenthesis are necessary
445 if end[0] > start_line:
446 break
447
448 if kind == tokenize.OP:
449 if token == ',':
450 # expression is non-empty tuple
451 if level == 1:
452 break
453 elif token == '(':
454 level += 1
455 elif token == ')':
456 level -= 1
457 if level == 0:
458 # outer parenthesis closed before end of expression
459 if tokens[i + 1][:2] != (tokenize.OP, ':'):
460 break
461
462 return [(pos, 'A111 redundant parenthesis for {} '
463 'statement'.format(statement))]
464
465 return []
466
467 check_redundant_parenthesis.name = 'abp-redundant-parenthesis'
468 check_redundant_parenthesis.version = __version__
OLDNEW
« no previous file with comments | « .hgignore ('k') | flake8-abp/setup.py » ('j') | flake8-abp/setup.py » ('J')

Powered by Google App Engine
This is Rietveld