| OLD | NEW | 
|---|
| (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 | 
|  | 21 __version__ = '0.1' | 
|  | 22 | 
|  | 23 DEPRECATED_APIS = { | 
|  | 24     ('re', 'match'): 'A101 use re.search() instead re.match()', | 
|  | 25     ('codecs', 'open'): 'A102 use io.open() instead codecs.open()', | 
|  | 26 } | 
|  | 27 | 
|  | 28 BAILOUT = (ast.Return, ast.Raise, ast.Continue, ast.Break) | 
|  | 29 | 
|  | 30 | 
|  | 31 def is_const(node): | 
|  | 32     if isinstance(node, (ast.Str, ast.Num)): | 
|  | 33         return True | 
|  | 34     if hasattr(ast, 'Bytes') and isinstance(node, ast.Bytes): | 
|  | 35         return True | 
|  | 36 | 
|  | 37     if isinstance(node, (ast.Tuple, ast.List, ast.Set)): | 
|  | 38         return all(is_const(elt) for elt in node.elts) | 
|  | 39     if isinstance(node, ast.Name): | 
|  | 40         return node.id in {'None', 'True', 'False'} | 
|  | 41     if isinstance(node, ast.BinOp): | 
|  | 42         return is_const(node.left) and is_const(node.right) | 
|  | 43     if isinstance(node, ast.UnaryOp): | 
|  | 44         return is_const(node.operand) | 
|  | 45 | 
|  | 46     return False | 
|  | 47 | 
|  | 48 | 
|  | 49 class TreeVisitor(ast.NodeVisitor): | 
|  | 50     def __init__(self): | 
|  | 51         self.errors = [] | 
|  | 52         self.stack = [] | 
|  | 53 | 
|  | 54     def _visit_block(self, nodes, mandatory=False, docstring=False): | 
|  | 55         pass_node = None | 
|  | 56         bailed = False | 
|  | 57         dead_code = False | 
|  | 58 | 
|  | 59         for node in nodes: | 
|  | 60             if isinstance(node, ast.Pass): | 
|  | 61                 pass_node = node | 
|  | 62 | 
|  | 63             if bailed and not dead_code: | 
|  | 64                 dead_code = True | 
|  | 65                 self.errors.append((node, 'A151 dead code after ' | 
|  | 66                                           'return/raise/continue/break')) | 
|  | 67 | 
|  | 68             if isinstance(node, BAILOUT): | 
|  | 69                 bailed = True | 
|  | 70 | 
|  | 71             if not isinstance(node, ast.Expr): | 
|  | 72                 continue | 
|  | 73             if isinstance(node.value, (ast.Call, ast.Yield)): | 
|  | 74                 continue | 
|  | 75             if docstring and node is nodes[0] and isinstance(node.value, ast.Str
     ): | 
|  | 76                 continue | 
|  | 77 | 
|  | 78             self.errors.append((node, 'A152 unused expression')) | 
|  | 79 | 
|  | 80         if pass_node: | 
|  | 81             if len(nodes) > 1: | 
|  | 82                 self.errors.append((pass_node, 'A153 redundant pass statement')) | 
|  | 83 | 
|  | 84             if not mandatory and all(isinstance(n, ast.Pass) for n in nodes): | 
|  | 85                 self.errors.append((pass_node, 'A154 empty block')) | 
|  | 86 | 
|  | 87     def _visit_block_node(self, node, **kwargs): | 
|  | 88         self._visit_block(node.body, **kwargs) | 
|  | 89         if hasattr(node, 'orelse'): | 
|  | 90             self._visit_block(node.orelse) | 
|  | 91         if hasattr(node, 'finalbody'): | 
|  | 92             self._visit_block(node.finalbody) | 
|  | 93         self.generic_visit(node) | 
|  | 94 | 
|  | 95     visit_Try = visit_TryExcept = visit_TryFinally = _visit_block_node | 
|  | 96 | 
|  | 97     visit_ExceptHandler = visit_While = \ | 
|  | 98         lambda self, node: self._visit_block_node(node, mandatory=True) | 
|  | 99 | 
|  | 100     visit_Module = visit_ClassDef = \ | 
|  | 101         lambda self, node: self._visit_block_node(node, mandatory=True, | 
|  | 102                                                         docstring=True) | 
|  | 103 | 
|  | 104     def visit_Attribute(self, node): | 
|  | 105         if isinstance(node.ctx, ast.Load) and isinstance(node.value, ast.Name): | 
|  | 106             error = DEPRECATED_APIS.get((node.value.id, node.attr)) | 
|  | 107             if error: | 
|  | 108                 self.errors.append((node, error)) | 
|  | 109         self.generic_visit(node) | 
|  | 110 | 
|  | 111     def visit_ImportFrom(self, node): | 
|  | 112         for alias in node.names: | 
|  | 113             error = DEPRECATED_APIS.get((node.module, alias.name)) | 
|  | 114             if error: | 
|  | 115                 self.errors.append((node, error)) | 
|  | 116 | 
|  | 117     def visit_BinOp(self, node): | 
|  | 118         if isinstance(node.op, ast.Mod) and isinstance(node.left, ast.Str): | 
|  | 119             self.errors.append((node, 'A111 use format() instead % operator ' | 
|  | 120                                       'for string formatting')) | 
|  | 121 | 
|  | 122         multi_addition = (isinstance(node.op, ast.Add) and | 
|  | 123                           isinstance(node.left, ast.BinOp) and | 
|  | 124                           isinstance(node.left.op, ast.Add)) | 
|  | 125         if multi_addition and (isinstance(node.left.left, ast.Str) or | 
|  | 126                                isinstance(node.left.right, ast.Str) or | 
|  | 127                                isinstance(node.right, ast.Str)): | 
|  | 128             self.errors.append((node, 'A112 use format() instead + operator ' | 
|  | 129                                       'when concatenating >2 strings')) | 
|  | 130 | 
|  | 131         self.generic_visit(node) | 
|  | 132 | 
|  | 133     def visit_comprehension(self, node): | 
|  | 134         if isinstance(node.iter, (ast.Tuple, ast.Set)): | 
|  | 135             self.errors.append((node.iter, 'A121 use lists for data ' | 
|  | 136                                            'that have order')) | 
|  | 137         self.generic_visit(node) | 
|  | 138 | 
|  | 139     def visit_For(self, node): | 
|  | 140         self._visit_block(node.body, mandatory=True) | 
|  | 141         self.visit_comprehension(node) | 
|  | 142 | 
|  | 143     def visit_Compare(self, node): | 
|  | 144         left = node.left | 
|  | 145         single = len(node.ops) == 1 | 
|  | 146 | 
|  | 147         for op, right in zip(node.ops, node.comparators): | 
|  | 148             membership = isinstance(op, (ast.In, ast.NotIn)) | 
|  | 149             symmetric = isinstance(op, (ast.Eq, ast.NotEq, ast.Is, ast.IsNot)) | 
|  | 150 | 
|  | 151             if membership and isinstance(right, (ast.Tuple, ast.List)): | 
|  | 152                 self.errors.append((right, 'A122 use sets for distinct ' | 
|  | 153                                            'unordered data')) | 
|  | 154 | 
|  | 155             consts_first = single and not membership or symmetric | 
|  | 156             if consts_first and is_const(left) and not is_const(right): | 
|  | 157                 self.errors.append((left, 'A123 yoda condition')) | 
|  | 158 | 
|  | 159             left = right | 
|  | 160 | 
|  | 161         self.generic_visit(node) | 
|  | 162 | 
|  | 163     def visit_Call(self, node): | 
|  | 164         func = node.func | 
|  | 165         if isinstance(func, ast.Name) and len(node.args) > 0: | 
|  | 166             arg = node.args[0] | 
|  | 167 | 
|  | 168             if isinstance(arg, ast.Lambda) and func.id in {'filter', 'map'}: | 
|  | 169                 self.errors.append((node, 'A131 use a comprehension ' | 
|  | 170                                           'instead calling {}() with ' | 
|  | 171                                           'lambda function'.format(func.id))) | 
|  | 172 | 
|  | 173             if isinstance(arg, (ast.ListComp, ast.GeneratorExp)): | 
|  | 174                 redundant_comp = func.id in {'list', 'set'} | 
|  | 175                 if func.id == 'dict': | 
|  | 176                     redundant_comp = isinstance(arg.elt, (ast.Tuple, ast.List)) | 
|  | 177 | 
|  | 178                 if redundant_comp: | 
|  | 179                     self.errors.append((node, 'A132 use a {0} comprehension ' | 
|  | 180                                               'instead calling the {0}() ' | 
|  | 181                                               'constructor'.format(func.id))) | 
|  | 182 | 
|  | 183         self.generic_visit(node) | 
|  | 184 | 
|  | 185     def visit_FunctionDef(self, node): | 
|  | 186         self._visit_block(node.body, mandatory=True, docstring=True) | 
|  | 187 | 
|  | 188         self.stack.append((set(), [])) | 
|  | 189         self.generic_visit(node) | 
|  | 190         targets, globals = self.stack.pop() | 
|  | 191 | 
|  | 192         for var in globals: | 
|  | 193             if any(name not in targets for name in var.names): | 
|  | 194                 self.errors.append((var, 'A141 redundant global/nonlocal ' | 
|  | 195                                          'declaration')) | 
|  | 196 | 
|  | 197     def visit_Name(self, node): | 
|  | 198         if self.stack and isinstance(node.ctx, ast.Store): | 
|  | 199             self.stack[-1][0].add(node.id) | 
|  | 200 | 
|  | 201     def visit_Global(self, node): | 
|  | 202         if self.stack: | 
|  | 203             self.stack[-1][1].append(node) | 
|  | 204         else: | 
|  | 205             self.errors.append((node, 'A141 global/nonlocal declaration ' | 
|  | 206                                       'on top-level')) | 
|  | 207 | 
|  | 208     visit_Nonlocal = visit_Global | 
|  | 209 | 
|  | 210     def visit_If(self, node): | 
|  | 211         has_else = bool(node.orelse) | 
|  | 212 | 
|  | 213         if has_else and any(isinstance(n, BAILOUT) for n in node.body): | 
|  | 214             self.errors.append((node, 'A159 redundant else statement after ' | 
|  | 215                                       'return/raise/continue/break ' | 
|  | 216                                       'in if-clause')) | 
|  | 217 | 
|  | 218         self._visit_block(node.body, mandatory=has_else) | 
|  | 219         self._visit_block(node.orelse) | 
|  | 220         self.generic_visit(node) | 
|  | 221 | 
|  | 222     def visit_Assign(self, node): | 
|  | 223         if isinstance(node.value, ast.BinOp) and len(node.targets) == 1: | 
|  | 224             target = node.targets[0] | 
|  | 225             left_is_target = (isinstance(target, ast.Name) and | 
|  | 226                               isinstance(node.value.left, ast.Name) and | 
|  | 227                               target.id == node.value.left.id) | 
|  | 228             if left_is_target: | 
|  | 229                 self.errors.append((node, 'A161 use in-place assignment, ' | 
|  | 230                                           'e.g. x += y instead x = x + y')) | 
|  | 231         self.generic_visit(node) | 
|  | 232 | 
|  | 233 | 
|  | 234 class ASTChecker(object): | 
|  | 235     name = 'abp' | 
|  | 236     version = __version__ | 
|  | 237 | 
|  | 238     def __init__(self, tree, filename): | 
|  | 239         self.tree = tree | 
|  | 240 | 
|  | 241     def run(self): | 
|  | 242         visitor = TreeVisitor() | 
|  | 243         visitor.visit(self.tree) | 
|  | 244 | 
|  | 245         for node, error in visitor.errors: | 
|  | 246             yield (node.lineno, node.col_offset, error, type(self)) | 
|  | 247 | 
|  | 248 | 
|  | 249 def check_non_default_encoding(physical_line, line_number): | 
|  | 250     if line_number <= 2 and re.search(r'^\s*#.*coding[:=]', physical_line): | 
|  | 251         return (0, 'A201 non-default file encoding') | 
|  | 252 | 
|  | 253 check_non_default_encoding.name = 'abp-non-default-encoding' | 
|  | 254 check_non_default_encoding.version = __version__ | 
|  | 255 | 
|  | 256 | 
|  | 257 def check_quotes(logical_line, tokens, previous_logical, indent_level): | 
|  | 258     first_token = True | 
|  | 259 | 
|  | 260     for kind, token, start, end, _ in tokens: | 
|  | 261         if kind == tokenize.INDENT: | 
|  | 262             continue | 
|  | 263 | 
|  | 264         if kind == tokenize.STRING: | 
|  | 265             pos = start[1] - indent_level | 
|  | 266             match = re.search(r'^(u)?(b)?(r)?((""")?.*)$', | 
|  | 267                               token, re.IGNORECASE | re.DOTALL) | 
|  | 268             (is_unicode, is_bytes, is_raw, | 
|  | 269              literal, has_doc_quotes) = match.groups() | 
|  | 270 | 
|  | 271             if first_token and re.search(r'^(?:(?:def|class)\s|$)', | 
|  | 272                                          previous_logical): | 
|  | 273                 if not has_doc_quotes: | 
|  | 274                     yield (pos, 'A301 use triple double quotes for docstrings') | 
|  | 275                 if is_unicode or is_bytes or is_raw: | 
|  | 276                     yield (pos, "A302 don't use u, b or r for doc strings") | 
|  | 277             elif start[0] == end[0]: | 
|  | 278                 if is_raw: | 
|  | 279                     literal = re.sub(r'\\(?!{})'.format(literal[0]), | 
|  | 280                                      '\\\\\\\\', literal) | 
|  | 281 | 
|  | 282                 if sys.version_info[0] >= 3: | 
|  | 283                     if is_bytes: | 
|  | 284                         literal = 'b' + literal | 
|  | 285                     else: | 
|  | 286                         literal = re.sub(r'(?<!\\)\\x(?!a[0d])([a-f][0-9a-f])', | 
|  | 287                                          lambda m: chr(int(m.group(1), 16)), | 
|  | 288                                          literal) | 
|  | 289                 elif is_unicode: | 
|  | 290                     literal = 'u' + literal | 
|  | 291 | 
|  | 292                 if repr(eval(literal)) != literal: | 
|  | 293                     yield (pos, "A311 string literal doesn't match repr()") | 
|  | 294 | 
|  | 295         first_token = False | 
|  | 296 | 
|  | 297 check_quotes.name = 'abp-quotes' | 
|  | 298 check_quotes.version = __version__ | 
|  | 299 | 
|  | 300 | 
|  | 301 def check_redundant_parenthesis(logical_line, tokens, indent_level): | 
|  | 302     start_line = tokens[0][2][0] | 
|  | 303     level = 0 | 
|  | 304     statement = None | 
|  | 305 | 
|  | 306     for i, (kind, token, _, end, _) in enumerate(tokens): | 
|  | 307         if kind == tokenize.INDENT: | 
|  | 308             continue | 
|  | 309 | 
|  | 310         if statement is None: | 
|  | 311             # logical line doesn't start with an if, elif or while statement | 
|  | 312             if kind != tokenize.NAME or token not in {'if', 'elif', 'while'}: | 
|  | 313                 break | 
|  | 314 | 
|  | 315             # expression doesn't start with parenthesis | 
|  | 316             next_token = tokens[i + 1] | 
|  | 317             if next_token[:2] != (tokenize.OP, '('): | 
|  | 318                 break | 
|  | 319 | 
|  | 320             # expression is empty tuple | 
|  | 321             if tokens[i + 2][:2] == (tokenize.OP, ')'): | 
|  | 322                 break | 
|  | 323 | 
|  | 324             statement = token | 
|  | 325             pos = next_token[2][1] - indent_level | 
|  | 326             continue | 
|  | 327 | 
|  | 328         # expression ends on a different line, parenthesis are necessary | 
|  | 329         if end[0] > start_line: | 
|  | 330             break | 
|  | 331 | 
|  | 332         if kind == tokenize.OP: | 
|  | 333             if token == ',': | 
|  | 334                 # expression is non-empty tuple | 
|  | 335                 if level == 1: | 
|  | 336                     break | 
|  | 337             elif token == '(': | 
|  | 338                 level += 1 | 
|  | 339             elif token == ')': | 
|  | 340                 level -= 1 | 
|  | 341                 if level == 0: | 
|  | 342                     # outer parenthesis closed before end of expression | 
|  | 343                     if tokens[i + 1][:2] != (tokenize.OP, ':'): | 
|  | 344                         break | 
|  | 345 | 
|  | 346                     return [(pos, 'A401 redundant parenthesis for {} ' | 
|  | 347                                   'statement'.format(statement))] | 
|  | 348 | 
|  | 349     return [] | 
|  | 350 | 
|  | 351 check_redundant_parenthesis.name = 'abp-redundant-parenthesis' | 
|  | 352 check_redundant_parenthesis.version = __version__ | 
| OLD | NEW | 
|---|