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: Addressed comments, fixed two bugs, use ascii() Created May 9, 2016, 4:47 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
« no previous file with comments | « flake8-abp/README.md ('k') | flake8-abp/setup.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 try:
30 ascii
31 except NameError:
32 ascii = repr
33
34 __version__ = pkg_resources.get_distribution('flake8-abp').version
35
36 DISCOURAGED_APIS = {
37 're.match': 're.search',
38 'codecs.open': 'io.open',
39 }
40
41 ESSENTIAL_BUILTINS = set(dir(builtins)) - {'apply', 'buffer', 'coerce',
42 'intern', 'file'}
43
44 LEAVE_BLOCK = (ast.Return, ast.Raise, ast.Continue, ast.Break)
45 VOLATILE = object()
46
47
48 def evaluate(node):
49 try:
50 return eval(compile(ast.Expression(node), '', 'eval'), {})
51 except Exception:
52 return VOLATILE
53
54
55 def is_const(node):
56 return evaluate(node) is not VOLATILE
57
58
59 def get_identifier(node):
60 if isinstance(node, ast.Name):
61 return node.id
62 if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name):
63 return '{}.{}'.format(node.value.id, node.attr)
64
65
66 def get_statement(node):
67 return type(node).__name__.lower()
68
69
70 class TreeVisitor(ast.NodeVisitor):
71 Scope = collections.namedtuple('Scope', ['node', 'names', 'globals'])
72
73 def __init__(self):
74 self.errors = []
75 self.scope_stack = []
76
77 def _visit_block(self, nodes, block_required=False,
78 nodes_required=True, docstring=False,
79 can_have_unused_expr=False):
80 pass_node = None
81 has_non_pass = False
82 leave_node = None
83 dead_code = False
84
85 for i, node in enumerate(nodes):
86 if isinstance(node, ast.Pass):
87 pass_node = node
88 else:
89 has_non_pass = True
90
91 if leave_node and not dead_code:
92 dead_code = True
93 statement = get_statement(leave_node)
94 self.errors.append((node, 'A202 dead code after '
95 '{}'.format(statement)))
96
97 if isinstance(node, LEAVE_BLOCK):
98 leave_node = node
99
100 if can_have_unused_expr or not isinstance(node, ast.Expr):
101 continue
102 if docstring and i == 0 and isinstance(node.value, ast.Str):
103 continue
104 if isinstance(node.value, (ast.Call, ast.Yield)):
105 continue
106
107 self.errors.append((node, 'A203 unused expression'))
108
109 if pass_node:
110 if not nodes_required or len(nodes) > 1:
111 self.errors.append((pass_node, 'A204 redundant '
112 'pass statement'))
113
114 if not block_required and not has_non_pass:
115 self.errors.append((pass_node, 'A205 empty block'))
116
117 def _check_redundant_else(self, node, handlers, clause):
118 if not node.orelse:
119 return
120
121 for handler in handlers:
122 for child in handler.body:
123 if isinstance(child, LEAVE_BLOCK):
124 leave_node = child
125 break
126 else:
127 return
128
129 statement = get_statement(leave_node)
130 self.errors.append((node.orelse[0],
131 'A206 Extraneous else statement after {} '
132 'in {}-clause'.format(statement, clause)))
133
134 def visit_If(self, node):
135 self._visit_block(node.body, block_required=bool(node.orelse))
136 self._visit_block(node.orelse)
137 self._check_redundant_else(node, [node], 'if')
138 self.generic_visit(node)
139
140 def visit_Try(self, node):
141 self._visit_block(node.body, can_have_unused_expr=bool(node.handlers))
142 self._visit_block(node.orelse)
143 self._visit_block(node.finalbody)
144 self._check_redundant_else(node, node.handlers, 'except')
145 self.generic_visit(node)
146
147 def visit_TryExcept(self, node):
148 self._visit_block(node.body, can_have_unused_expr=True)
149 self._visit_block(node.orelse)
150 self._check_redundant_else(node, node.handlers, 'except')
151 self.generic_visit(node)
152
153 def visit_TryFinally(self, node):
154 self._visit_block(node.body)
155 self._visit_block(node.finalbody)
156 self.generic_visit(node)
157
158 def visit_ExceptHandler(self, node):
159 self._visit_block(node.body, block_required=True)
160 self.generic_visit(node)
161
162 def _visit_stored_name(self, node, name):
163 scope = self.scope_stack[-1]
164 scope.names.add(name)
165
166 if name in ESSENTIAL_BUILTINS and isinstance(scope.node,
167 ast.FunctionDef):
168 self.errors.append((node, 'A302 redefined built-in ' + name))
169
170 def visit_Name(self, node):
171 if isinstance(node.ctx, (ast.Store, ast.Param)):
172 self._visit_stored_name(node, node.id)
173
174 def visit_arg(self, node):
175 self._visit_stored_name(node, node.arg)
176
177 def _visit_with_scope(self, node):
178 scope = self.Scope(node, names=set(), globals=[])
179 self.scope_stack.append(scope)
180 self.generic_visit(node)
181 del self.scope_stack[-1]
182 return scope
183
184 def visit_Module(self, node):
185 self._visit_block(node.body, block_required=True,
186 nodes_required=False, docstring=True)
187 self._visit_with_scope(node)
188
189 def visit_FunctionDef(self, node):
190 self._visit_stored_name(node, node.name)
191 self._visit_block(node.body, block_required=True, docstring=True)
192
193 scope = self._visit_with_scope(node)
194 global_names = set()
195
196 for declaration in scope.globals:
197 for name in declaration.names:
198 if name not in scope.names or name in global_names:
199 statement = get_statement(declaration)
200 self.errors.append((declaration,
201 'A201 redundant {} declaration for '
202 '{}'.format(statement, name)))
203 else:
204 global_names.add(name)
205
206 visit_ClassDef = visit_FunctionDef
207
208 def visit_Global(self, node):
209 scope = self.scope_stack[-1]
210 scope.globals.append(node)
211
212 if isinstance(scope.node, ast.Module):
213 statement = get_statement(node)
214 self.errors.append((node, 'A201 {} declaration on '
215 'top-level'.format(statement)))
216
217 visit_Nonlocal = visit_Global
218
219 def _visit_iter(self, node):
220 if isinstance(node, (ast.Tuple, ast.Set)):
221 self.errors.append((node, 'A101 use lists for data '
222 'that have order'))
223
224 def visit_comprehension(self, node):
225 self._visit_iter(node.iter)
226 self.generic_visit(node)
227
228 def visit_For(self, node):
229 self._visit_iter(node.iter)
230 self._visit_block(node.body, block_required=True)
231 self._visit_block(node.orelse)
232 self.generic_visit(node)
233
234 def visit_While(self, node):
235 self._visit_block(node.body, block_required=True)
236 self._visit_block(node.orelse)
237 self.generic_visit(node)
238
239 def visit_BinOp(self, node):
240 if isinstance(node.op, ast.Mod) and isinstance(node.left, ast.Str):
241 self.errors.append((node, 'A107 use format() instead of '
242 '% operator for string formatting'))
243
244 multi_addition = (isinstance(node.op, ast.Add) and
245 isinstance(node.left, ast.BinOp) and
246 isinstance(node.left.op, ast.Add))
247 if multi_addition and (isinstance(node.left.left, ast.Str) or
248 isinstance(node.left.right, ast.Str) or
249 isinstance(node.right, ast.Str)):
250 self.errors.append((node, 'A108 use format() instead of '
251 '+ operator when concatenating '
252 'more than two strings'))
253
254 self.generic_visit(node)
255
256 def visit_Compare(self, node):
257 left = node.left
258 single = len(node.ops) == 1
259
260 for op, right in zip(node.ops, node.comparators):
261 membership = isinstance(op, (ast.In, ast.NotIn))
262 symmetric = isinstance(op, (ast.Eq, ast.NotEq, ast.Is, ast.IsNot))
263
264 if membership and isinstance(right, (ast.Tuple, ast.List)):
265 self.errors.append((right, 'A102 use sets for distinct '
266 'unordered data'))
267
268 consts_first = single and not membership or symmetric
269 if consts_first and is_const(left) and not is_const(right):
270 self.errors.append((left, 'A103 yoda condition'))
271
272 left = right
273
274 self.generic_visit(node)
275
276 def _check_deprecated(self, node, name):
277 substitute = DISCOURAGED_APIS.get(name)
278 if substitute:
279 self.errors.append((node, 'A301 use {}() instead of '
280 '{}()'.format(substitute, name)))
281
282 def visit_Call(self, node):
283 func = get_identifier(node.func)
284 arg = next(iter(node.args), None)
285 redundant_literal = False
286
287 if isinstance(arg, ast.Lambda) and func in {'map', 'filter',
288 'imap', 'ifilter',
289 'itertools.imap',
290 'itertools.ifilter'}:
291 self.errors.append((node, 'A104 use a comprehension '
292 'instead of calling {}() with '
293 'lambda function'.format(func)))
294 elif isinstance(arg, (ast.List, ast.Tuple)):
295 if func == 'dict':
296 redundant_literal = all(isinstance(elt, (ast.Tuple, ast.List))
297 for elt in arg.elts)
298 else:
299 redundant_literal = func in {'list', 'set', 'tuple'}
300 elif isinstance(arg, (ast.ListComp, ast.GeneratorExp)):
301 if func == 'dict':
302 redundant_literal = isinstance(arg.elt, (ast.Tuple, ast.List))
303 else:
304 redundant_literal = func in {'list', 'set'}
305
306 if redundant_literal:
307 self.errors.append((node, 'A105 use a {0} literal or '
308 'comprehension instead of calling '
309 '{0}()'.format(func)))
310
311 self._check_deprecated(node, func)
312 self.generic_visit(node)
313
314 def visit_Import(self, node):
315 for alias in node.names:
316 self._visit_stored_name(node, alias.asname or alias.name)
317
318 if hasattr(node, 'module'):
319 self._check_deprecated(node, '{}.{}'.format(node.module,
320 alias.name))
321
322 visit_ImportFrom = visit_Import
323
324 def visit_Assign(self, node):
325 if isinstance(node.value, ast.BinOp) and len(node.targets) == 1:
326 target = node.targets[0]
327 left_is_target = (isinstance(target, ast.Name) and
328 isinstance(node.value.left, ast.Name) and
329 target.id == node.value.left.id)
330 if left_is_target:
331 self.errors.append((node, 'A106 use augment assignment, '
332 'e.g. x += y instead x = x + y'))
333 self.generic_visit(node)
334
335 def _visit_hash_keys(self, nodes, what):
336 keys = []
337 for node in nodes:
338 key = evaluate(node)
339 if key is VOLATILE:
340 continue
341
342 if key in keys:
343 self.errors.append((node, 'A207 duplicate ' + what))
344 continue
345
346 keys.append(key)
347
348 def visit_Dict(self, node):
349 self._visit_hash_keys(node.keys, 'key in dict')
350
351 def visit_Set(self, node):
352 self._visit_hash_keys(node.elts, 'item in set')
353
354
355 class ASTChecker(object):
356 name = 'abp'
357 version = __version__
358
359 def __init__(self, tree, filename):
360 self.tree = tree
361
362 def run(self):
363 visitor = TreeVisitor()
364 visitor.visit(self.tree)
365
366 for node, error in visitor.errors:
367 yield (node.lineno, node.col_offset, error, type(self))
368
369
370 def check_non_default_encoding(physical_line, line_number):
371 if line_number <= 2 and re.search(r'^\s*#.*coding[:=]', physical_line):
372 return (0, 'A303 non-default file encoding')
373
374 check_non_default_encoding.name = 'abp-non-default-encoding'
375 check_non_default_encoding.version = __version__
376
377
378 def check_quotes(logical_line, tokens, previous_logical):
379 first_token = True
380
381 for kind, token, start, end, _ in tokens:
382 if kind == tokenize.INDENT or kind == tokenize.DEDENT:
383 continue
384
385 if kind == tokenize.STRING:
386 match = re.search(r'^(u)?(b)?(r)?((""")?.*)$',
387 token, re.IGNORECASE | re.DOTALL)
388 (is_unicode, is_bytes, is_raw,
389 literal, has_doc_quotes) = match.groups()
390
391 if first_token and re.search(r'^(?:(?:def|class)\s|$)',
392 previous_logical):
393 if not has_doc_quotes:
394 yield (start, 'A109 use triple double '
395 'quotes for docstrings')
396 elif is_unicode or is_bytes or is_raw:
397 yield (start, "A109 don't use u'', b'' "
398 "or r'' for doc strings")
399 elif start[0] == end[0]:
400 if is_raw:
401 literal = re.sub(r'\\(?!{})'.format(literal[0]),
402 '\\\\\\\\', literal)
403
404 if sys.version_info[0] >= 3:
405 if is_bytes:
406 literal = 'b' + literal
407 elif is_unicode:
408 literal = 'u' + literal
409
410 if ascii(eval(literal)) != literal:
411 yield (start, "A110 string literal doesn't match"
412 '{}()'.format(ascii.__name__))
413
414 first_token = False
415
416 check_quotes.name = 'abp-quotes'
417 check_quotes.version = __version__
418
419
420 def check_redundant_parenthesis(logical_line, tokens):
421 start_line = tokens[0][2][0]
422 level = 0
423 statement = None
424
425 for i, (kind, token, _, end, _) in enumerate(tokens):
426 if kind == tokenize.INDENT or kind == tokenize.DEDENT:
427 continue
428
429 if statement is None:
430 # logical line doesn't start with an if, elif or while statement
431 if kind != tokenize.NAME or token not in {'if', 'elif', 'while'}:
432 break
433
434 # expression doesn't start with parenthesis
435 next_token = tokens[i + 1]
436 if next_token[:2] != (tokenize.OP, '('):
437 break
438
439 # expression is empty tuple
440 if tokens[i + 2][:2] == (tokenize.OP, ')'):
441 break
442
443 statement = token
444 pos = next_token[2]
445 continue
446
447 # expression ends on a different line, parenthesis are necessary
448 if end[0] > start_line:
449 break
450
451 if kind == tokenize.OP:
452 if token == ',':
453 # expression is non-empty tuple
454 if level == 1:
455 break
456 elif token == '(':
457 level += 1
458 elif token == ')':
459 level -= 1
460 if level == 0:
461 # outer parenthesis closed before end of expression
462 if tokens[i + 1][:2] != (tokenize.OP, ':'):
463 break
464
465 return [(pos, 'A111 redundant parenthesis for {} '
466 'statement'.format(statement))]
467
468 return []
469
470 check_redundant_parenthesis.name = 'abp-redundant-parenthesis'
471 check_redundant_parenthesis.version = __version__
OLDNEW
« no previous file with comments | « flake8-abp/README.md ('k') | flake8-abp/setup.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld