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

Delta Between Two Patch Sets: flake8-abp/flake8_abp.py

Issue 29340727: Noissue - Added flake8 extension accounting for our coding style and some other stuff (Closed)
Left Patch Set: Added more checks Created April 22, 2016, 12:02 p.m.
Right 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:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « flake8-abp/README.md ('k') | flake8-abp/setup.py » ('j') | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
1 # This file is part of Adblock Plus <https://adblockplus.org/>, 1 # This file is part of Adblock Plus <https://adblockplus.org/>,
2 # Copyright (C) 2006-2016 Eyeo GmbH 2 # Copyright (C) 2006-2016 Eyeo GmbH
3 # 3 #
4 # Adblock Plus is free software: you can redistribute it and/or modify 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 5 # it under the terms of the GNU General Public License version 3 as
6 # published by the Free Software Foundation. 6 # published by the Free Software Foundation.
7 # 7 #
8 # Adblock Plus is distributed in the hope that it will be useful, 8 # Adblock Plus is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details. 11 # GNU General Public License for more details.
12 # 12 #
13 # You should have received a copy of the GNU General Public License 13 # You should have received a copy of the GNU General Public License
Vasily Kuznetsov 2016/04/22 14:48:26 Probably we should have some tests for it too. It
Sebastian Noack 2016/05/07 17:14:03 Done.
14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. 14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
15 15
Vasily Kuznetsov 2016/04/22 14:48:26 It would be good to describe how to use this exten
16 import ast 16 import ast
17 import re 17 import re
18 import tokenize 18 import tokenize
19 import sys 19 import sys
20 20 import collections
21 __version__ = '0.1' 21
22 22 try:
23 DEPRECATED_APIS = { 23 import builtins
24 ('re', 'match'): 'A101 use re.search() instead re.match()', 24 except ImportError:
25 ('codecs', 'open'): 'A102 use io.open() instead codecs.open()', 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',
26 } 39 }
27 40
28 BAILOUT = (ast.Return, ast.Raise, ast.Continue, ast.Break) 41 ESSENTIAL_BUILTINS = set(dir(builtins)) - {'apply', 'buffer', 'coerce',
Vasily Kuznetsov 2016/04/22 14:48:26 Maybe something like LEAVE_BLOCK would be a better
Sebastian Noack 2016/05/07 17:14:01 Done.
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
29 53
30 54
31 def is_const(node): 55 def is_const(node):
32 if isinstance(node, (ast.Str, ast.Num)): 56 return evaluate(node) is not VOLATILE
33 return True 57
34 if hasattr(ast, 'Bytes') and isinstance(node, ast.Bytes): 58
35 return True 59 def get_identifier(node):
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): 60 if isinstance(node, ast.Name):
40 return node.id in {'None', 'True', 'False'} 61 return node.id
41 if isinstance(node, ast.BinOp): 62 if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name):
42 return is_const(node.left) and is_const(node.right) 63 return '{}.{}'.format(node.value.id, node.attr)
43 if isinstance(node, ast.UnaryOp): 64
44 return is_const(node.operand) 65
45 66 def get_statement(node):
46 return False 67 return type(node).__name__.lower()
47 68
48 69
49 class TreeVisitor(ast.NodeVisitor): 70 class TreeVisitor(ast.NodeVisitor):
71 Scope = collections.namedtuple('Scope', ['node', 'names', 'globals'])
72
50 def __init__(self): 73 def __init__(self):
51 self.errors = [] 74 self.errors = []
52 self.stack = [] 75 self.scope_stack = []
Vasily Kuznetsov 2016/04/22 14:48:25 Perhaps it would be good to give this a more descr
Sebastian Noack 2016/05/07 17:14:02 Done.
53 76
54 def _visit_block(self, nodes, mandatory=False, docstring=False): 77 def _visit_block(self, nodes, block_required=False,
78 nodes_required=True, docstring=False,
79 can_have_unused_expr=False):
55 pass_node = None 80 pass_node = None
56 bailed = False 81 has_non_pass = False
Vasily Kuznetsov 2016/04/22 14:48:25 And here same as for BAILOUT, perhaps "left_block"
Sebastian Noack 2016/05/07 17:14:02 I went for leave_node, for consistency with pass_n
82 leave_node = None
57 dead_code = False 83 dead_code = False
58 84
59 for node in nodes: 85 for i, node in enumerate(nodes):
60 if isinstance(node, ast.Pass): 86 if isinstance(node, ast.Pass):
61 pass_node = node 87 pass_node = node
62 88 else:
63 if bailed and not dead_code: 89 has_non_pass = True
90
91 if leave_node and not dead_code:
64 dead_code = True 92 dead_code = True
65 self.errors.append((node, 'A151 dead code after ' 93 statement = get_statement(leave_node)
66 'return/raise/continue/break')) 94 self.errors.append((node, 'A202 dead code after '
67 95 '{}'.format(statement)))
68 if isinstance(node, BAILOUT): 96
69 bailed = True 97 if isinstance(node, LEAVE_BLOCK):
70 98 leave_node = node
71 if not isinstance(node, ast.Expr): 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):
72 continue 103 continue
73 if isinstance(node.value, (ast.Call, ast.Yield)): 104 if isinstance(node.value, (ast.Call, ast.Yield)):
74 continue 105 continue
75 if docstring and node is nodes[0] and isinstance(node.value, ast.Str ): 106
Vasily Kuznetsov 2016/04/22 14:48:25 This line and A153 line are too long. Not by much
Sebastian Noack 2016/05/07 17:14:01 I made this line shorter by simply comparing again
76 continue 107 self.errors.append((node, 'A203 unused expression'))
77
78 self.errors.append((node, 'A152 unused expression'))
79 108
80 if pass_node: 109 if pass_node:
81 if len(nodes) > 1: 110 if not nodes_required or len(nodes) > 1:
82 self.errors.append((pass_node, 'A153 redundant pass statement')) 111 self.errors.append((pass_node, 'A204 redundant '
83 112 'pass statement'))
84 if not mandatory and all(isinstance(n, ast.Pass) for n in nodes): 113
85 self.errors.append((pass_node, 'A154 empty block')) 114 if not block_required and not has_non_pass:
86 115 self.errors.append((pass_node, 'A205 empty block'))
87 def _visit_block_node(self, node, **kwargs): 116
88 self._visit_block(node.body, **kwargs) 117 def _check_redundant_else(self, node, handlers, clause):
89 if hasattr(node, 'orelse'): 118 if not node.orelse:
90 self._visit_block(node.orelse) 119 return
91 if hasattr(node, 'finalbody'): 120
92 self._visit_block(node.finalbody) 121 for handler in handlers:
93 self.generic_visit(node) 122 for child in handler.body:
94 123 if isinstance(child, LEAVE_BLOCK):
95 visit_Try = visit_TryExcept = visit_TryFinally = _visit_block_node 124 leave_node = child
96 125 break
97 visit_ExceptHandler = visit_While = \ 126 else:
98 lambda self, node: self._visit_block_node(node, mandatory=True) 127 return
99 128
100 visit_Module = visit_ClassDef = \ 129 statement = get_statement(leave_node)
101 lambda self, node: self._visit_block_node(node, mandatory=True, 130 self.errors.append((node.orelse[0],
102 docstring=True) 131 'A206 Extraneous else statement after {} '
Vasily Kuznetsov 2016/04/22 14:48:25 E127 on this line, it should be under "node".
Sebastian Noack 2016/05/07 17:14:03 This is one of indentation errors I disagree with.
Vasily Kuznetsov 2016/05/09 13:39:50 Although I follow your logic and it's probably ind
103 132 'in {}-clause'.format(statement, clause)))
104 def visit_Attribute(self, node): 133
105 if isinstance(node.ctx, ast.Load) and isinstance(node.value, ast.Name): 134 def visit_If(self, node):
106 error = DEPRECATED_APIS.get((node.value.id, node.attr)) 135 self._visit_block(node.body, block_required=bool(node.orelse))
107 if error: 136 self._visit_block(node.orelse)
108 self.errors.append((node, error)) 137 self._check_redundant_else(node, [node], 'if')
109 self.generic_visit(node) 138 self.generic_visit(node)
110 139
111 def visit_ImportFrom(self, node): 140 def visit_Try(self, node):
112 for alias in node.names: 141 self._visit_block(node.body, can_have_unused_expr=bool(node.handlers))
113 error = DEPRECATED_APIS.get((node.module, alias.name)) 142 self._visit_block(node.orelse)
114 if error: 143 self._visit_block(node.finalbody)
115 self.errors.append((node, error)) 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)
116 238
117 def visit_BinOp(self, node): 239 def visit_BinOp(self, node):
118 if isinstance(node.op, ast.Mod) and isinstance(node.left, ast.Str): 240 if isinstance(node.op, ast.Mod) and isinstance(node.left, ast.Str):
119 self.errors.append((node, 'A111 use format() instead % operator ' 241 self.errors.append((node, 'A107 use format() instead of '
Vasily Kuznetsov 2016/04/22 14:48:26 Nit: "instead % operator" -> "instead of % operato
Sebastian Noack 2016/05/07 17:13:59 Done.
120 'for string formatting')) 242 '% operator for string formatting'))
121 243
122 multi_addition = (isinstance(node.op, ast.Add) and 244 multi_addition = (isinstance(node.op, ast.Add) and
123 isinstance(node.left, ast.BinOp) and 245 isinstance(node.left, ast.BinOp) and
124 isinstance(node.left.op, ast.Add)) 246 isinstance(node.left.op, ast.Add))
125 if multi_addition and (isinstance(node.left.left, ast.Str) or 247 if multi_addition and (isinstance(node.left.left, ast.Str) or
126 isinstance(node.left.right, ast.Str) or 248 isinstance(node.left.right, ast.Str) or
127 isinstance(node.right, ast.Str)): 249 isinstance(node.right, ast.Str)):
128 self.errors.append((node, 'A112 use format() instead + operator ' 250 self.errors.append((node, 'A108 use format() instead of '
Vasily Kuznetsov 2016/04/22 14:48:26 Same as above, "instead of + operator"
Sebastian Noack 2016/05/07 17:14:02 Done.
129 'when concatenating >2 strings')) 251 '+ operator when concatenating '
130 252 'more than two strings'))
131 self.generic_visit(node) 253
132 254 self.generic_visit(node)
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 255
143 def visit_Compare(self, node): 256 def visit_Compare(self, node):
144 left = node.left 257 left = node.left
145 single = len(node.ops) == 1 258 single = len(node.ops) == 1
146 259
147 for op, right in zip(node.ops, node.comparators): 260 for op, right in zip(node.ops, node.comparators):
148 membership = isinstance(op, (ast.In, ast.NotIn)) 261 membership = isinstance(op, (ast.In, ast.NotIn))
149 symmetric = isinstance(op, (ast.Eq, ast.NotEq, ast.Is, ast.IsNot)) 262 symmetric = isinstance(op, (ast.Eq, ast.NotEq, ast.Is, ast.IsNot))
150 263
151 if membership and isinstance(right, (ast.Tuple, ast.List)): 264 if membership and isinstance(right, (ast.Tuple, ast.List)):
152 self.errors.append((right, 'A122 use sets for distinct ' 265 self.errors.append((right, 'A102 use sets for distinct '
153 'unordered data')) 266 'unordered data'))
154 267
155 consts_first = single and not membership or symmetric 268 consts_first = single and not membership or symmetric
156 if consts_first and is_const(left) and not is_const(right): 269 if consts_first and is_const(left) and not is_const(right):
157 self.errors.append((left, 'A123 yoda condition')) 270 self.errors.append((left, 'A103 yoda condition'))
158 271
159 left = right 272 left = right
160 273
161 self.generic_visit(node) 274 self.generic_visit(node)
162 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
163 def visit_Call(self, node): 282 def visit_Call(self, node):
164 func = node.func 283 func = get_identifier(node.func)
165 if isinstance(func, ast.Name) and len(node.args) > 0: 284 arg = next(iter(node.args), None)
166 arg = node.args[0] 285 redundant_literal = False
167 286
168 if isinstance(arg, ast.Lambda) and func.id in {'filter', 'map'}: 287 if isinstance(arg, ast.Lambda) and func in {'map', 'filter',
169 self.errors.append((node, 'A131 use a comprehension ' 288 'imap', 'ifilter',
170 'instead calling {}() with ' 289 'itertools.imap',
171 'lambda function'.format(func.id))) 290 'itertools.ifilter'}:
172 291 self.errors.append((node, 'A104 use a comprehension '
173 if isinstance(arg, (ast.ListComp, ast.GeneratorExp)): 292 'instead of calling {}() with '
174 redundant_comp = func.id in {'list', 'set'} 293 'lambda function'.format(func)))
175 if func.id == 'dict': 294 elif isinstance(arg, (ast.List, ast.Tuple)):
176 redundant_comp = isinstance(arg.elt, (ast.Tuple, ast.List)) 295 if func == 'dict':
177 296 redundant_literal = all(isinstance(elt, (ast.Tuple, ast.List))
178 if redundant_comp: 297 for elt in arg.elts)
179 self.errors.append((node, 'A132 use a {0} comprehension ' 298 else:
180 'instead calling the {0}() ' 299 redundant_literal = func in {'list', 'set', 'tuple'}
181 'constructor'.format(func.id))) 300 elif isinstance(arg, (ast.ListComp, ast.GeneratorExp)):
182 301 if func == 'dict':
Vasily Kuznetsov 2016/04/22 14:48:27 We could probably also watch for list/tuple/set li
Sebastian Noack 2016/05/07 17:14:01 Done. However, I intentionally ignore set literals
Vasily Kuznetsov 2016/05/09 13:39:50 Acknowledged.
183 self.generic_visit(node) 302 redundant_literal = isinstance(arg.elt, (ast.Tuple, ast.List))
184 303 else:
185 def visit_FunctionDef(self, node): 304 redundant_literal = func in {'list', 'set'}
186 self._visit_block(node.body, mandatory=True, docstring=True) 305
187 306 if redundant_literal:
188 self.stack.append((set(), [])) 307 self.errors.append((node, 'A105 use a {0} literal or '
189 self.generic_visit(node) 308 'comprehension instead of calling '
190 targets, globals = self.stack.pop() 309 '{0}()'.format(func)))
191 310
192 for var in globals: 311 self._check_deprecated(node, func)
193 if any(name not in targets for name in var.names): 312 self.generic_visit(node)
194 self.errors.append((var, 'A141 redundant global/nonlocal ' 313
195 'declaration')) 314 def visit_Import(self, node):
196 315 for alias in node.names:
197 def visit_Name(self, node): 316 self._visit_stored_name(node, alias.asname or alias.name)
198 if self.stack and isinstance(node.ctx, ast.Store): 317
199 self.stack[-1][0].add(node.id) 318 if hasattr(node, 'module'):
200 319 self._check_deprecated(node, '{}.{}'.format(node.module,
201 def visit_Global(self, node): 320 alias.name))
202 if self.stack: 321
203 self.stack[-1][1].append(node) 322 visit_ImportFrom = visit_Import
204 else:
205 self.errors.append((node, 'A141 global/nonlocal declaration '
Vasily Kuznetsov 2016/04/22 14:48:26 This complains at the following snippet: t = 1 c
Sebastian Noack 2016/05/07 17:14:03 Done.
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 '
Vasily Kuznetsov 2016/04/22 14:48:26 This error is shown for the location of `if`. Woul
Sebastian Noack 2016/05/07 17:13:59 Well, the position of else is unknown since the el
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 323
222 def visit_Assign(self, node): 324 def visit_Assign(self, node):
223 if isinstance(node.value, ast.BinOp) and len(node.targets) == 1: 325 if isinstance(node.value, ast.BinOp) and len(node.targets) == 1:
224 target = node.targets[0] 326 target = node.targets[0]
225 left_is_target = (isinstance(target, ast.Name) and 327 left_is_target = (isinstance(target, ast.Name) and
226 isinstance(node.value.left, ast.Name) and 328 isinstance(node.value.left, ast.Name) and
227 target.id == node.value.left.id) 329 target.id == node.value.left.id)
228 if left_is_target: 330 if left_is_target:
229 self.errors.append((node, 'A161 use in-place assignment, ' 331 self.errors.append((node, 'A106 use augment assignment, '
230 'e.g. x += y instead x = x + y')) 332 'e.g. x += y instead x = x + y'))
231 self.generic_visit(node) 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')
232 353
233 354
234 class ASTChecker(object): 355 class ASTChecker(object):
235 name = 'abp' 356 name = 'abp'
236 version = __version__ 357 version = __version__
237 358
238 def __init__(self, tree, filename): 359 def __init__(self, tree, filename):
239 self.tree = tree 360 self.tree = tree
240 361
241 def run(self): 362 def run(self):
242 visitor = TreeVisitor() 363 visitor = TreeVisitor()
243 visitor.visit(self.tree) 364 visitor.visit(self.tree)
244 365
245 for node, error in visitor.errors: 366 for node, error in visitor.errors:
246 yield (node.lineno, node.col_offset, error, type(self)) 367 yield (node.lineno, node.col_offset, error, type(self))
247 368
248 369
249 def check_non_default_encoding(physical_line, line_number): 370 def check_non_default_encoding(physical_line, line_number):
250 if (line_number <= 2 and re.search(r'^\s*#.*coding[:=]', physical_line)): 371 if line_number <= 2 and re.search(r'^\s*#.*coding[:=]', physical_line):
251 return (0, 'A201 non-default file encoding') 372 return (0, 'A303 non-default file encoding')
252 373
253 check_non_default_encoding.name = 'abp-non-default-encoding' 374 check_non_default_encoding.name = 'abp-non-default-encoding'
254 check_non_default_encoding.version = __version__ 375 check_non_default_encoding.version = __version__
255 376
256 377
257 def check_quotes(logical_line, tokens, previous_logical): 378 def check_quotes(logical_line, tokens, previous_logical):
258 first_token = True 379 first_token = True
259 offset = 0
260 380
261 for kind, token, start, end, _ in tokens: 381 for kind, token, start, end, _ in tokens:
262 if kind == tokenize.INDENT: 382 if kind == tokenize.INDENT or kind == tokenize.DEDENT:
263 offset = end[1]
264 continue 383 continue
265 384
266 if kind == tokenize.STRING: 385 if kind == tokenize.STRING:
267 pos = start[1] - offset
268 match = re.search(r'^(u)?(b)?(r)?((""")?.*)$', 386 match = re.search(r'^(u)?(b)?(r)?((""")?.*)$',
269 token, re.IGNORECASE | re.DOTALL) 387 token, re.IGNORECASE | re.DOTALL)
270 (is_unicode, is_bytes, is_raw, 388 (is_unicode, is_bytes, is_raw,
271 literal, has_doc_quotes) = match.groups() 389 literal, has_doc_quotes) = match.groups()
272 390
273 if first_token and re.search(r'^(?:(?:def|class)\s|$)', 391 if first_token and re.search(r'^(?:(?:def|class)\s|$)',
274 previous_logical): 392 previous_logical):
275 if not has_doc_quotes: 393 if not has_doc_quotes:
276 yield (pos, 'A301 use triple double quotes for docstrings') 394 yield (start, 'A109 use triple double '
277 if is_unicode or is_bytes or is_raw: 395 'quotes for docstrings')
278 yield (pos, "A302 don't use u, b or r for doc strings") 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")
279 elif start[0] == end[0]: 399 elif start[0] == end[0]:
280 if is_raw: 400 if is_raw:
281 literal = re.sub(r'\\(?!{})'.format(literal[0]), 401 literal = re.sub(r'\\(?!{})'.format(literal[0]),
Vasily Kuznetsov 2016/04/22 14:48:25 I think I still don't quite get the point of this
Sebastian Noack 2016/05/07 17:14:01 Well, the whole point of raw strings is that backs
282 '\\\\\\\\', literal) 402 '\\\\\\\\', literal)
283 403
284 if sys.version_info[0] >= 3: 404 if sys.version_info[0] >= 3:
285 if is_bytes: 405 if is_bytes:
286 literal = 'b' + literal 406 literal = 'b' + literal
287 else:
288 literal = re.sub(r'(?<!\\)\\x(?!a[0d])([a-f][0-9a-f])',
289 lambda m: chr(int(m.group(1), 16)),
290 literal)
291 elif is_unicode: 407 elif is_unicode:
292 literal = 'u' + literal 408 literal = 'u' + literal
293 409
294 if repr(eval(literal)) != literal: 410 if ascii(eval(literal)) != literal:
295 yield (pos, "A311 string literal doesn't match repr()") 411 yield (start, "A110 string literal doesn't match"
Vasily Kuznetsov 2016/04/22 14:48:26 It would be nice to explain in more detail what ex
Sebastian Noack 2016/05/07 17:14:00 Yes, that is something for the style guide. I will
412 '{}()'.format(ascii.__name__))
296 413
297 first_token = False 414 first_token = False
298 415
299 check_quotes.name = 'abp-quotes' 416 check_quotes.name = 'abp-quotes'
300 check_quotes.version = __version__ 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__
LEFTRIGHT

Powered by Google App Engine
This is Rietveld