| 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 class TreeVisitor(ast.NodeVisitor): | 
 |   32     def __init__(self): | 
 |   33         self.errors = [] | 
 |   34         self.stack = [] | 
 |   35  | 
 |   36     def _visit_block(self, nodes, mandatory=False, docstring=False): | 
 |   37         pass_node = None | 
 |   38         bailed = False | 
 |   39         dead_code = False | 
 |   40  | 
 |   41         for node in nodes: | 
 |   42             if isinstance(node, ast.Pass): | 
 |   43                 pass_node = node | 
 |   44  | 
 |   45             if bailed and not dead_code: | 
 |   46                 dead_code = True | 
 |   47                 self.errors.append((node, 'A151 dead code after ' | 
 |   48                                           'return/raise/continue/break')) | 
 |   49  | 
 |   50             if isinstance(node, BAILOUT): | 
 |   51                 bailed = True | 
 |   52  | 
 |   53             if not isinstance(node, ast.Expr): | 
 |   54                 continue | 
 |   55             if isinstance(node.value, (ast.Call, ast.Yield)): | 
 |   56                 continue | 
 |   57             if docstring and node is nodes[0] and isinstance(node.value, ast.Str
     ): | 
 |   58                 continue | 
 |   59  | 
 |   60             self.errors.append((node, 'A152 unused expression')) | 
 |   61  | 
 |   62         if pass_node: | 
 |   63             if len(nodes) > 1: | 
 |   64                 self.errors.append((pass_node, 'A153 redundant pass statement')) | 
 |   65  | 
 |   66             if not mandatory and all(isinstance(n, ast.Pass) for n in nodes): | 
 |   67                 self.errors.append((pass_node, 'A154 empty block')) | 
 |   68  | 
 |   69     def _visit_block_node(self, node, **kwargs): | 
 |   70         self._visit_block(node.body, **kwargs) | 
 |   71         if hasattr(node, 'orelse'): | 
 |   72             self._visit_block(node.orelse) | 
 |   73         if hasattr(node, 'finalbody'): | 
 |   74             self._visit_block(node.finalbody) | 
 |   75         self.generic_visit(node) | 
 |   76  | 
 |   77     visit_Try = visit_TryExcept = visit_TryFinally = _visit_block_node | 
 |   78  | 
 |   79     visit_ExceptHandler = visit_While = \ | 
 |   80         lambda self, node: self._visit_block_node(node, mandatory=True) | 
 |   81  | 
 |   82     visit_Module = visit_ClassDef = \ | 
 |   83         lambda self, node: self._visit_block_node(node, mandatory=True, | 
 |   84                                                         docstring=True) | 
 |   85  | 
 |   86     def visit_Attribute(self, node): | 
 |   87         if isinstance(node.ctx, ast.Load) and isinstance(node.value, ast.Name): | 
 |   88             error = DEPRECATED_APIS.get((node.value.id, node.attr)) | 
 |   89             if error: | 
 |   90                 self.errors.append((node, error)) | 
 |   91         self.generic_visit(node) | 
 |   92  | 
 |   93     def visit_ImportFrom(self, node): | 
 |   94         for alias in node.names: | 
 |   95             error = DEPRECATED_APIS.get((node.module, alias.name)) | 
 |   96             if error: | 
 |   97                 self.errors.append((node, error)) | 
 |   98  | 
 |   99     def visit_BinOp(self, node): | 
 |  100         if isinstance(node.op, ast.Mod) and isinstance(node.left, ast.Str): | 
 |  101             self.errors.append((node, 'A111 use format() instead % operator ' | 
 |  102                                       'for string formatting')) | 
 |  103  | 
 |  104         multi_addition = (isinstance(node.op, ast.Add) and | 
 |  105                           isinstance(node.left, ast.BinOp) and | 
 |  106                           isinstance(node.left.op, ast.Add)) | 
 |  107         if multi_addition and (isinstance(node.left.left, ast.Str) or | 
 |  108                                isinstance(node.left.right, ast.Str) or | 
 |  109                                isinstance(node.right, ast.Str)): | 
 |  110             self.errors.append((node, 'A112 use format() instead + operator ' | 
 |  111                                       'when concatenating >2 strings')) | 
 |  112  | 
 |  113         self.generic_visit(node) | 
 |  114  | 
 |  115     def visit_comprehension(self, node): | 
 |  116         if isinstance(node.iter, (ast.Tuple, ast.Set, ast.Dict)): | 
 |  117             self.errors.append((node.iter, 'A121 use lists for data ' | 
 |  118                                            'that have order')) | 
 |  119         self.generic_visit(node) | 
 |  120  | 
 |  121     def visit_For(self, node): | 
 |  122         self._visit_block(node.body, mandatory=True) | 
 |  123         self.visit_comprehension(node) | 
 |  124  | 
 |  125     def visit_Call(self, node): | 
 |  126         func = node.func | 
 |  127         if isinstance(func, ast.Name) and func.id in {'filter', 'map'}: | 
 |  128             if len(node.args) > 0 and isinstance(node.args[0], ast.Lambda): | 
 |  129                 self.errors.append((node, 'A131 use a comprehension ' | 
 |  130                                           'instead calling {}() with ' | 
 |  131                                           'lambda function'.format(func.id))) | 
 |  132         self.generic_visit(node) | 
 |  133  | 
 |  134     def visit_FunctionDef(self, node): | 
 |  135         self._visit_block(node.body, mandatory=True, docstring=True) | 
 |  136  | 
 |  137         self.stack.append((set(), [])) | 
 |  138         self.generic_visit(node) | 
 |  139         targets, globals = self.stack.pop() | 
 |  140  | 
 |  141         for var in globals: | 
 |  142             if any(name not in targets for name in var.names): | 
 |  143                 self.errors.append((var, 'A141 redundant global/nonlocal ' | 
 |  144                                          'declaration')) | 
 |  145  | 
 |  146     def visit_Name(self, node): | 
 |  147         if self.stack and isinstance(node.ctx, ast.Store): | 
 |  148             self.stack[-1][0].add(node.id) | 
 |  149  | 
 |  150     def visit_Global(self, node): | 
 |  151         if self.stack: | 
 |  152             self.stack[-1][1].append(node) | 
 |  153         else: | 
 |  154             self.errors.append((node, 'A141 global/nonlocal declaration ' | 
 |  155                                       'on top-level')) | 
 |  156  | 
 |  157     visit_Nonlocal = visit_Global | 
 |  158  | 
 |  159     def visit_If(self, node): | 
 |  160         has_else = bool(node.orelse) | 
 |  161  | 
 |  162         if has_else and any(isinstance(m, BAILOUT) for n in node.body): | 
 |  163             self.errors.append((node, 'A159 redundant else statement after ' | 
 |  164                                       'return/raise/continue/break ' | 
 |  165                                       'in if-clause')) | 
 |  166  | 
 |  167         self._visit_block(node.body, mandatory=has_else) | 
 |  168         self._visit_block(node.orelse) | 
 |  169         self.generic_visit(node) | 
 |  170  | 
 |  171  | 
 |  172 class ASTChecker(object): | 
 |  173     name = 'abp' | 
 |  174     version = __version__ | 
 |  175  | 
 |  176     def __init__(self, tree, filename): | 
 |  177         self.tree = tree | 
 |  178  | 
 |  179     def run(self): | 
 |  180         visitor = TreeVisitor() | 
 |  181         visitor.visit(self.tree) | 
 |  182  | 
 |  183         for node, error in visitor.errors: | 
 |  184             yield (node.lineno, node.col_offset, error, type(self)) | 
 |  185  | 
 |  186  | 
 |  187 def check_non_default_encoding(physical_line, line_number): | 
 |  188     if (line_number <= 2 and re.search(r'^\s*#.*coding[:=]', physical_line)): | 
 |  189         return (0, 'A201 non-default file encoding') | 
 |  190  | 
 |  191 check_non_default_encoding.name = 'abp-non-default-encoding' | 
 |  192 check_non_default_encoding.version = __version__ | 
 |  193  | 
 |  194  | 
 |  195 def check_quotes(logical_line, tokens, previous_logical): | 
 |  196     first_token = True | 
 |  197     offset = 0 | 
 |  198  | 
 |  199     for kind, token, start, end, _ in tokens: | 
 |  200         if kind == tokenize.INDENT: | 
 |  201             offset = end[1] | 
 |  202             continue | 
 |  203  | 
 |  204         if kind == tokenize.STRING: | 
 |  205             pos = start[1] - offset | 
 |  206             match = re.search(r'^(u)?(b)?(r)?((""")?.*)$', | 
 |  207                               token, re.IGNORECASE | re.DOTALL) | 
 |  208             (is_unicode, is_bytes, is_raw, | 
 |  209              literal, has_doc_quotes) = match.groups() | 
 |  210  | 
 |  211             if first_token and re.search(r'^(?:(?:def|class)\s|$)', | 
 |  212                                          previous_logical): | 
 |  213                 if not has_doc_quotes: | 
 |  214                     yield (pos, 'A301 use triple double quotes for docstrings') | 
 |  215                 if is_unicode or is_bytes or is_raw: | 
 |  216                     yield (pos, "A302 don't use u, b or for doc strings") | 
 |  217             elif start[0] == end[0]: | 
 |  218                 if is_raw: | 
 |  219                     literal = re.sub(r'\\(?!{})'.format(literal[0]), | 
 |  220                                      '\\\\\\\\', literal) | 
 |  221  | 
 |  222                 if sys.version_info[0] >= 3: | 
 |  223                     if is_bytes: | 
 |  224                         literal = 'b' + literal | 
 |  225                     else: | 
 |  226                         literal = re.sub(r'(?<!\\)\\x(?!a[0d])([a-f][0-9a-f])', | 
 |  227                                          lambda m: chr(int(m.group(1), 16)), | 
 |  228                                          literal) | 
 |  229                 elif is_unicode: | 
 |  230                     literal = 'u' + literal | 
 |  231  | 
 |  232                 if repr(eval(literal)) != literal: | 
 |  233                     yield (pos, "A311 string literal doesn't match repr()") | 
 |  234  | 
 |  235         first_token = False | 
 |  236  | 
 |  237 check_quotes.name = 'abp-quotes' | 
 |  238 check_quotes.version = __version__ | 
| OLD | NEW |