]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
1 | # -*- coding: utf-8 -*- |
2 | import codecs | |
3 | import warnings | |
4 | import re | |
5 | from contextlib import contextmanager | |
6 | ||
7 | from parso.normalizer import Normalizer, NormalizerConfig, Issue, Rule | |
8 | from parso.python.tokenize import _get_token_collection | |
9 | ||
10 | _BLOCK_STMTS = ('if_stmt', 'while_stmt', 'for_stmt', 'try_stmt', 'with_stmt') | |
11 | _STAR_EXPR_PARENTS = ('testlist_star_expr', 'testlist_comp', 'exprlist') | |
12 | # This is the maximal block size given by python. | |
13 | _MAX_BLOCK_SIZE = 20 | |
14 | _MAX_INDENT_COUNT = 100 | |
15 | ALLOWED_FUTURES = ( | |
16 | 'nested_scopes', 'generators', 'division', 'absolute_import', | |
17 | 'with_statement', 'print_function', 'unicode_literals', 'generator_stop', | |
18 | ) | |
19 | _COMP_FOR_TYPES = ('comp_for', 'sync_comp_for') | |
20 | ||
21 | ||
22 | def _get_rhs_name(node, version): | |
23 | type_ = node.type | |
24 | if type_ == "lambdef": | |
25 | return "lambda" | |
26 | elif type_ == "atom": | |
27 | comprehension = _get_comprehension_type(node) | |
28 | first, second = node.children[:2] | |
29 | if comprehension is not None: | |
30 | return comprehension | |
31 | elif second.type == "dictorsetmaker": | |
32 | if version < (3, 8): | |
33 | return "literal" | |
34 | else: | |
35 | if second.children[1] == ":" or second.children[0] == "**": | |
36 | return "dict display" | |
37 | else: | |
38 | return "set display" | |
39 | elif ( | |
40 | first == "(" | |
41 | and (second == ")" | |
42 | or (len(node.children) == 3 and node.children[1].type == "testlist_comp")) | |
43 | ): | |
44 | return "tuple" | |
45 | elif first == "(": | |
46 | return _get_rhs_name(_remove_parens(node), version=version) | |
47 | elif first == "[": | |
48 | return "list" | |
49 | elif first == "{" and second == "}": | |
50 | return "dict display" | |
51 | elif first == "{" and len(node.children) > 2: | |
52 | return "set display" | |
53 | elif type_ == "keyword": | |
54 | if "yield" in node.value: | |
55 | return "yield expression" | |
56 | if version < (3, 8): | |
57 | return "keyword" | |
58 | else: | |
59 | return str(node.value) | |
60 | elif type_ == "operator" and node.value == "...": | |
61 | return "Ellipsis" | |
62 | elif type_ == "comparison": | |
63 | return "comparison" | |
64 | elif type_ in ("string", "number", "strings"): | |
65 | return "literal" | |
66 | elif type_ == "yield_expr": | |
67 | return "yield expression" | |
68 | elif type_ == "test": | |
69 | return "conditional expression" | |
70 | elif type_ in ("atom_expr", "power"): | |
71 | if node.children[0] == "await": | |
72 | return "await expression" | |
73 | elif node.children[-1].type == "trailer": | |
74 | trailer = node.children[-1] | |
75 | if trailer.children[0] == "(": | |
76 | return "function call" | |
77 | elif trailer.children[0] == "[": | |
78 | return "subscript" | |
79 | elif trailer.children[0] == ".": | |
80 | return "attribute" | |
81 | elif ( | |
82 | ("expr" in type_ and "star_expr" not in type_) # is a substring | |
83 | or "_test" in type_ | |
84 | or type_ in ("term", "factor") | |
85 | ): | |
86 | return "operator" | |
87 | elif type_ == "star_expr": | |
88 | return "starred" | |
89 | elif type_ == "testlist_star_expr": | |
90 | return "tuple" | |
91 | elif type_ == "fstring": | |
92 | return "f-string expression" | |
93 | return type_ # shouldn't reach here | |
94 | ||
95 | ||
96 | def _iter_stmts(scope): | |
97 | """ | |
98 | Iterates over all statements and splits up simple_stmt. | |
99 | """ | |
100 | for child in scope.children: | |
101 | if child.type == 'simple_stmt': | |
102 | for child2 in child.children: | |
103 | if child2.type == 'newline' or child2 == ';': | |
104 | continue | |
105 | yield child2 | |
106 | else: | |
107 | yield child | |
108 | ||
109 | ||
110 | def _get_comprehension_type(atom): | |
111 | first, second = atom.children[:2] | |
112 | if second.type == 'testlist_comp' and second.children[1].type in _COMP_FOR_TYPES: | |
113 | if first == '[': | |
114 | return 'list comprehension' | |
115 | else: | |
116 | return 'generator expression' | |
117 | elif second.type == 'dictorsetmaker' and second.children[-1].type in _COMP_FOR_TYPES: | |
118 | if second.children[1] == ':': | |
119 | return 'dict comprehension' | |
120 | else: | |
121 | return 'set comprehension' | |
122 | return None | |
123 | ||
124 | ||
125 | def _is_future_import(import_from): | |
126 | # It looks like a __future__ import that is relative is still a future | |
127 | # import. That feels kind of odd, but whatever. | |
128 | # if import_from.level != 0: | |
129 | # return False | |
130 | from_names = import_from.get_from_names() | |
131 | return [n.value for n in from_names] == ['__future__'] | |
132 | ||
133 | ||
134 | def _remove_parens(atom): | |
135 | """ | |
136 | Returns the inner part of an expression like `(foo)`. Also removes nested | |
137 | parens. | |
138 | """ | |
139 | try: | |
140 | children = atom.children | |
141 | except AttributeError: | |
142 | pass | |
143 | else: | |
144 | if len(children) == 3 and children[0] == '(': | |
145 | return _remove_parens(atom.children[1]) | |
146 | return atom | |
147 | ||
148 | ||
149 | def _skip_parens_bottom_up(node): | |
150 | """ | |
151 | Returns an ancestor node of an expression, skipping all levels of parens | |
152 | bottom-up. | |
153 | """ | |
154 | while node.parent is not None: | |
155 | node = node.parent | |
156 | if node.type != 'atom' or node.children[0] != '(': | |
157 | return node | |
158 | return None | |
159 | ||
160 | ||
161 | def _iter_params(parent_node): | |
162 | return (n for n in parent_node.children if n.type == 'param' or n.type == 'operator') | |
163 | ||
164 | ||
165 | def _is_future_import_first(import_from): | |
166 | """ | |
167 | Checks if the import is the first statement of a file. | |
168 | """ | |
169 | found_docstring = False | |
170 | for stmt in _iter_stmts(import_from.get_root_node()): | |
171 | if stmt.type == 'string' and not found_docstring: | |
172 | continue | |
173 | found_docstring = True | |
174 | ||
175 | if stmt == import_from: | |
176 | return True | |
177 | if stmt.type == 'import_from' and _is_future_import(stmt): | |
178 | continue | |
179 | return False | |
180 | ||
181 | ||
182 | def _iter_definition_exprs_from_lists(exprlist): | |
183 | def check_expr(child): | |
184 | if child.type == 'atom': | |
185 | if child.children[0] == '(': | |
186 | testlist_comp = child.children[1] | |
187 | if testlist_comp.type == 'testlist_comp': | |
188 | yield from _iter_definition_exprs_from_lists(testlist_comp) | |
189 | return | |
190 | else: | |
191 | # It's a paren that doesn't do anything, like 1 + (1) | |
192 | yield from check_expr(testlist_comp) | |
193 | return | |
194 | elif child.children[0] == '[': | |
195 | yield testlist_comp | |
196 | return | |
197 | yield child | |
198 | ||
199 | if exprlist.type in _STAR_EXPR_PARENTS: | |
200 | for child in exprlist.children[::2]: | |
201 | yield from check_expr(child) | |
202 | else: | |
203 | yield from check_expr(exprlist) | |
204 | ||
205 | ||
206 | def _get_expr_stmt_definition_exprs(expr_stmt): | |
207 | exprs = [] | |
208 | for list_ in expr_stmt.children[:-2:2]: | |
209 | if list_.type in ('testlist_star_expr', 'testlist'): | |
210 | exprs += _iter_definition_exprs_from_lists(list_) | |
211 | else: | |
212 | exprs.append(list_) | |
213 | return exprs | |
214 | ||
215 | ||
216 | def _get_for_stmt_definition_exprs(for_stmt): | |
217 | exprlist = for_stmt.children[1] | |
218 | return list(_iter_definition_exprs_from_lists(exprlist)) | |
219 | ||
220 | ||
221 | def _is_argument_comprehension(argument): | |
222 | return argument.children[1].type in _COMP_FOR_TYPES | |
223 | ||
224 | ||
225 | def _any_fstring_error(version, node): | |
226 | if version < (3, 9) or node is None: | |
227 | return False | |
228 | if node.type == "error_node": | |
229 | return any(child.type == "fstring_start" for child in node.children) | |
230 | elif node.type == "fstring": | |
231 | return True | |
232 | else: | |
233 | return node.search_ancestor("fstring") | |
234 | ||
235 | ||
236 | class _Context: | |
237 | def __init__(self, node, add_syntax_error, parent_context=None): | |
238 | self.node = node | |
239 | self.blocks = [] | |
240 | self.parent_context = parent_context | |
241 | self._used_name_dict = {} | |
242 | self._global_names = [] | |
243 | self._local_params_names = [] | |
244 | self._nonlocal_names = [] | |
245 | self._nonlocal_names_in_subscopes = [] | |
246 | self._add_syntax_error = add_syntax_error | |
247 | ||
248 | def is_async_funcdef(self): | |
249 | # Stupidly enough async funcdefs can have two different forms, | |
250 | # depending if a decorator is used or not. | |
251 | return self.is_function() \ | |
252 | and self.node.parent.type in ('async_funcdef', 'async_stmt') | |
253 | ||
254 | def is_function(self): | |
255 | return self.node.type == 'funcdef' | |
256 | ||
257 | def add_name(self, name): | |
258 | parent_type = name.parent.type | |
259 | if parent_type == 'trailer': | |
260 | # We are only interested in first level names. | |
261 | return | |
262 | ||
263 | if parent_type == 'global_stmt': | |
264 | self._global_names.append(name) | |
265 | elif parent_type == 'nonlocal_stmt': | |
266 | self._nonlocal_names.append(name) | |
267 | elif parent_type == 'funcdef': | |
268 | self._local_params_names.extend( | |
269 | [param.name.value for param in name.parent.get_params()] | |
270 | ) | |
271 | else: | |
272 | self._used_name_dict.setdefault(name.value, []).append(name) | |
273 | ||
274 | def finalize(self): | |
275 | """ | |
276 | Returns a list of nonlocal names that need to be part of that scope. | |
277 | """ | |
278 | self._analyze_names(self._global_names, 'global') | |
279 | self._analyze_names(self._nonlocal_names, 'nonlocal') | |
280 | ||
281 | global_name_strs = {n.value: n for n in self._global_names} | |
282 | for nonlocal_name in self._nonlocal_names: | |
283 | try: | |
284 | global_name = global_name_strs[nonlocal_name.value] | |
285 | except KeyError: | |
286 | continue | |
287 | ||
288 | message = "name '%s' is nonlocal and global" % global_name.value | |
289 | if global_name.start_pos < nonlocal_name.start_pos: | |
290 | error_name = global_name | |
291 | else: | |
292 | error_name = nonlocal_name | |
293 | self._add_syntax_error(error_name, message) | |
294 | ||
295 | nonlocals_not_handled = [] | |
296 | for nonlocal_name in self._nonlocal_names_in_subscopes: | |
297 | search = nonlocal_name.value | |
298 | if search in self._local_params_names: | |
299 | continue | |
300 | if search in global_name_strs or self.parent_context is None: | |
301 | message = "no binding for nonlocal '%s' found" % nonlocal_name.value | |
302 | self._add_syntax_error(nonlocal_name, message) | |
303 | elif not self.is_function() or \ | |
304 | nonlocal_name.value not in self._used_name_dict: | |
305 | nonlocals_not_handled.append(nonlocal_name) | |
306 | return self._nonlocal_names + nonlocals_not_handled | |
307 | ||
308 | def _analyze_names(self, globals_or_nonlocals, type_): | |
309 | def raise_(message): | |
310 | self._add_syntax_error(base_name, message % (base_name.value, type_)) | |
311 | ||
312 | params = [] | |
313 | if self.node.type == 'funcdef': | |
314 | params = self.node.get_params() | |
315 | ||
316 | for base_name in globals_or_nonlocals: | |
317 | found_global_or_nonlocal = False | |
318 | # Somehow Python does it the reversed way. | |
319 | for name in reversed(self._used_name_dict.get(base_name.value, [])): | |
320 | if name.start_pos > base_name.start_pos: | |
321 | # All following names don't have to be checked. | |
322 | found_global_or_nonlocal = True | |
323 | ||
324 | parent = name.parent | |
325 | if parent.type == 'param' and parent.name == name: | |
326 | # Skip those here, these definitions belong to the next | |
327 | # scope. | |
328 | continue | |
329 | ||
330 | if name.is_definition(): | |
331 | if parent.type == 'expr_stmt' \ | |
332 | and parent.children[1].type == 'annassign': | |
333 | if found_global_or_nonlocal: | |
334 | # If it's after the global the error seems to be | |
335 | # placed there. | |
336 | base_name = name | |
337 | raise_("annotated name '%s' can't be %s") | |
338 | break | |
339 | else: | |
340 | message = "name '%s' is assigned to before %s declaration" | |
341 | else: | |
342 | message = "name '%s' is used prior to %s declaration" | |
343 | ||
344 | if not found_global_or_nonlocal: | |
345 | raise_(message) | |
346 | # Only add an error for the first occurence. | |
347 | break | |
348 | ||
349 | for param in params: | |
350 | if param.name.value == base_name.value: | |
351 | raise_("name '%s' is parameter and %s"), | |
352 | ||
353 | @contextmanager | |
354 | def add_block(self, node): | |
355 | self.blocks.append(node) | |
356 | yield | |
357 | self.blocks.pop() | |
358 | ||
359 | def add_context(self, node): | |
360 | return _Context(node, self._add_syntax_error, parent_context=self) | |
361 | ||
362 | def close_child_context(self, child_context): | |
363 | self._nonlocal_names_in_subscopes += child_context.finalize() | |
364 | ||
365 | ||
366 | class ErrorFinder(Normalizer): | |
367 | """ | |
368 | Searches for errors in the syntax tree. | |
369 | """ | |
370 | def __init__(self, *args, **kwargs): | |
371 | super().__init__(*args, **kwargs) | |
372 | self._error_dict = {} | |
373 | self.version = self.grammar.version_info | |
374 | ||
375 | def initialize(self, node): | |
376 | def create_context(node): | |
377 | if node is None: | |
378 | return None | |
379 | ||
380 | parent_context = create_context(node.parent) | |
381 | if node.type in ('classdef', 'funcdef', 'file_input'): | |
382 | return _Context(node, self._add_syntax_error, parent_context) | |
383 | return parent_context | |
384 | ||
385 | self.context = create_context(node) or _Context(node, self._add_syntax_error) | |
386 | self._indentation_count = 0 | |
387 | ||
388 | def visit(self, node): | |
389 | if node.type == 'error_node': | |
390 | with self.visit_node(node): | |
391 | # Don't need to investigate the inners of an error node. We | |
392 | # might find errors in there that should be ignored, because | |
393 | # the error node itself already shows that there's an issue. | |
394 | return '' | |
395 | return super().visit(node) | |
396 | ||
397 | @contextmanager | |
398 | def visit_node(self, node): | |
399 | self._check_type_rules(node) | |
400 | ||
401 | if node.type in _BLOCK_STMTS: | |
402 | with self.context.add_block(node): | |
403 | if len(self.context.blocks) == _MAX_BLOCK_SIZE: | |
404 | self._add_syntax_error(node, "too many statically nested blocks") | |
405 | yield | |
406 | return | |
407 | elif node.type == 'suite': | |
408 | self._indentation_count += 1 | |
409 | if self._indentation_count == _MAX_INDENT_COUNT: | |
410 | self._add_indentation_error(node.children[1], "too many levels of indentation") | |
411 | ||
412 | yield | |
413 | ||
414 | if node.type == 'suite': | |
415 | self._indentation_count -= 1 | |
416 | elif node.type in ('classdef', 'funcdef'): | |
417 | context = self.context | |
418 | self.context = context.parent_context | |
419 | self.context.close_child_context(context) | |
420 | ||
421 | def visit_leaf(self, leaf): | |
422 | if leaf.type == 'error_leaf': | |
423 | if leaf.token_type in ('INDENT', 'ERROR_DEDENT'): | |
424 | # Indents/Dedents itself never have a prefix. They are just | |
425 | # "pseudo" tokens that get removed by the syntax tree later. | |
426 | # Therefore in case of an error we also have to check for this. | |
427 | spacing = list(leaf.get_next_leaf()._split_prefix())[-1] | |
428 | if leaf.token_type == 'INDENT': | |
429 | message = 'unexpected indent' | |
430 | else: | |
431 | message = 'unindent does not match any outer indentation level' | |
432 | self._add_indentation_error(spacing, message) | |
433 | else: | |
434 | if leaf.value.startswith('\\'): | |
435 | message = 'unexpected character after line continuation character' | |
436 | else: | |
437 | match = re.match('\\w{,2}("{1,3}|\'{1,3})', leaf.value) | |
438 | if match is None: | |
439 | message = 'invalid syntax' | |
440 | if ( | |
441 | self.version >= (3, 9) | |
442 | and leaf.value in _get_token_collection( | |
443 | self.version | |
444 | ).always_break_tokens | |
445 | ): | |
446 | message = "f-string: " + message | |
447 | else: | |
448 | if len(match.group(1)) == 1: | |
449 | message = 'EOL while scanning string literal' | |
450 | else: | |
451 | message = 'EOF while scanning triple-quoted string literal' | |
452 | self._add_syntax_error(leaf, message) | |
453 | return '' | |
454 | elif leaf.value == ':': | |
455 | parent = leaf.parent | |
456 | if parent.type in ('classdef', 'funcdef'): | |
457 | self.context = self.context.add_context(parent) | |
458 | ||
459 | # The rest is rule based. | |
460 | return super().visit_leaf(leaf) | |
461 | ||
462 | def _add_indentation_error(self, spacing, message): | |
463 | self.add_issue(spacing, 903, "IndentationError: " + message) | |
464 | ||
465 | def _add_syntax_error(self, node, message): | |
466 | self.add_issue(node, 901, "SyntaxError: " + message) | |
467 | ||
468 | def add_issue(self, node, code, message): | |
469 | # Overwrite the default behavior. | |
470 | # Check if the issues are on the same line. | |
471 | line = node.start_pos[0] | |
472 | args = (code, message, node) | |
473 | self._error_dict.setdefault(line, args) | |
474 | ||
475 | def finalize(self): | |
476 | self.context.finalize() | |
477 | ||
478 | for code, message, node in self._error_dict.values(): | |
479 | self.issues.append(Issue(node, code, message)) | |
480 | ||
481 | ||
482 | class IndentationRule(Rule): | |
483 | code = 903 | |
484 | ||
485 | def _get_message(self, message, node): | |
486 | message = super()._get_message(message, node) | |
487 | return "IndentationError: " + message | |
488 | ||
489 | ||
490 | @ErrorFinder.register_rule(type='error_node') | |
491 | class _ExpectIndentedBlock(IndentationRule): | |
492 | message = 'expected an indented block' | |
493 | ||
494 | def get_node(self, node): | |
495 | leaf = node.get_next_leaf() | |
496 | return list(leaf._split_prefix())[-1] | |
497 | ||
498 | def is_issue(self, node): | |
499 | # This is the beginning of a suite that is not indented. | |
500 | return node.children[-1].type == 'newline' | |
501 | ||
502 | ||
503 | class ErrorFinderConfig(NormalizerConfig): | |
504 | normalizer_class = ErrorFinder | |
505 | ||
506 | ||
507 | class SyntaxRule(Rule): | |
508 | code = 901 | |
509 | ||
510 | def _get_message(self, message, node): | |
511 | message = super()._get_message(message, node) | |
512 | if ( | |
513 | "f-string" not in message | |
514 | and _any_fstring_error(self._normalizer.version, node) | |
515 | ): | |
516 | message = "f-string: " + message | |
517 | return "SyntaxError: " + message | |
518 | ||
519 | ||
520 | @ErrorFinder.register_rule(type='error_node') | |
521 | class _InvalidSyntaxRule(SyntaxRule): | |
522 | message = "invalid syntax" | |
523 | fstring_message = "f-string: invalid syntax" | |
524 | ||
525 | def get_node(self, node): | |
526 | return node.get_next_leaf() | |
527 | ||
528 | def is_issue(self, node): | |
529 | error = node.get_next_leaf().type != 'error_leaf' | |
530 | if ( | |
531 | error | |
532 | and _any_fstring_error(self._normalizer.version, node) | |
533 | ): | |
534 | self.add_issue(node, message=self.fstring_message) | |
535 | else: | |
536 | # Error leafs will be added later as an error. | |
537 | return error | |
538 | ||
539 | ||
540 | @ErrorFinder.register_rule(value='await') | |
541 | class _AwaitOutsideAsync(SyntaxRule): | |
542 | message = "'await' outside async function" | |
543 | ||
544 | def is_issue(self, leaf): | |
545 | return not self._normalizer.context.is_async_funcdef() | |
546 | ||
547 | def get_error_node(self, node): | |
548 | # Return the whole await statement. | |
549 | return node.parent | |
550 | ||
551 | ||
552 | @ErrorFinder.register_rule(value='break') | |
553 | class _BreakOutsideLoop(SyntaxRule): | |
554 | message = "'break' outside loop" | |
555 | ||
556 | def is_issue(self, leaf): | |
557 | in_loop = False | |
558 | for block in self._normalizer.context.blocks: | |
559 | if block.type in ('for_stmt', 'while_stmt'): | |
560 | in_loop = True | |
561 | return not in_loop | |
562 | ||
563 | ||
564 | @ErrorFinder.register_rule(value='continue') | |
565 | class _ContinueChecks(SyntaxRule): | |
566 | message = "'continue' not properly in loop" | |
567 | message_in_finally = "'continue' not supported inside 'finally' clause" | |
568 | ||
569 | def is_issue(self, leaf): | |
570 | in_loop = False | |
571 | for block in self._normalizer.context.blocks: | |
572 | if block.type in ('for_stmt', 'while_stmt'): | |
573 | in_loop = True | |
574 | if block.type == 'try_stmt': | |
575 | last_block = block.children[-3] | |
576 | if ( | |
577 | last_block == "finally" | |
578 | and leaf.start_pos > last_block.start_pos | |
579 | and self._normalizer.version < (3, 8) | |
580 | ): | |
581 | self.add_issue(leaf, message=self.message_in_finally) | |
582 | return False # Error already added | |
583 | if not in_loop: | |
584 | return True | |
585 | ||
586 | ||
587 | @ErrorFinder.register_rule(value='from') | |
588 | class _YieldFromCheck(SyntaxRule): | |
589 | message = "'yield from' inside async function" | |
590 | ||
591 | def get_node(self, leaf): | |
592 | return leaf.parent.parent # This is the actual yield statement. | |
593 | ||
594 | def is_issue(self, leaf): | |
595 | return leaf.parent.type == 'yield_arg' \ | |
596 | and self._normalizer.context.is_async_funcdef() | |
597 | ||
598 | ||
599 | @ErrorFinder.register_rule(type='name') | |
600 | class _NameChecks(SyntaxRule): | |
601 | message = 'cannot assign to __debug__' | |
602 | message_none = 'cannot assign to None' | |
603 | ||
604 | def is_issue(self, leaf): | |
605 | self._normalizer.context.add_name(leaf) | |
606 | ||
607 | if leaf.value == '__debug__' and leaf.is_definition(): | |
608 | return True | |
609 | ||
610 | ||
611 | @ErrorFinder.register_rule(type='string') | |
612 | class _StringChecks(SyntaxRule): | |
613 | message = "bytes can only contain ASCII literal characters." | |
614 | ||
615 | def is_issue(self, leaf): | |
616 | string_prefix = leaf.string_prefix.lower() | |
617 | if 'b' in string_prefix \ | |
618 | and any(c for c in leaf.value if ord(c) > 127): | |
619 | # b'Ƥ' | |
620 | return True | |
621 | ||
622 | if 'r' not in string_prefix: | |
623 | # Raw strings don't need to be checked if they have proper | |
624 | # escaping. | |
625 | ||
626 | payload = leaf._get_payload() | |
627 | if 'b' in string_prefix: | |
628 | payload = payload.encode('utf-8') | |
629 | func = codecs.escape_decode | |
630 | else: | |
631 | func = codecs.unicode_escape_decode | |
632 | ||
633 | try: | |
634 | with warnings.catch_warnings(): | |
635 | # The warnings from parsing strings are not relevant. | |
636 | warnings.filterwarnings('ignore') | |
637 | func(payload) | |
638 | except UnicodeDecodeError as e: | |
639 | self.add_issue(leaf, message='(unicode error) ' + str(e)) | |
640 | except ValueError as e: | |
641 | self.add_issue(leaf, message='(value error) ' + str(e)) | |
642 | ||
643 | ||
644 | @ErrorFinder.register_rule(value='*') | |
645 | class _StarCheck(SyntaxRule): | |
646 | message = "named arguments must follow bare *" | |
647 | ||
648 | def is_issue(self, leaf): | |
649 | params = leaf.parent | |
650 | if params.type == 'parameters' and params: | |
651 | after = params.children[params.children.index(leaf) + 1:] | |
652 | after = [child for child in after | |
653 | if child not in (',', ')') and not child.star_count] | |
654 | return len(after) == 0 | |
655 | ||
656 | ||
657 | @ErrorFinder.register_rule(value='**') | |
658 | class _StarStarCheck(SyntaxRule): | |
659 | # e.g. {**{} for a in [1]} | |
660 | # TODO this should probably get a better end_pos including | |
661 | # the next sibling of leaf. | |
662 | message = "dict unpacking cannot be used in dict comprehension" | |
663 | ||
664 | def is_issue(self, leaf): | |
665 | if leaf.parent.type == 'dictorsetmaker': | |
666 | comp_for = leaf.get_next_sibling().get_next_sibling() | |
667 | return comp_for is not None and comp_for.type in _COMP_FOR_TYPES | |
668 | ||
669 | ||
670 | @ErrorFinder.register_rule(value='yield') | |
671 | @ErrorFinder.register_rule(value='return') | |
672 | class _ReturnAndYieldChecks(SyntaxRule): | |
673 | message = "'return' with value in async generator" | |
674 | message_async_yield = "'yield' inside async function" | |
675 | ||
676 | def get_node(self, leaf): | |
677 | return leaf.parent | |
678 | ||
679 | def is_issue(self, leaf): | |
680 | if self._normalizer.context.node.type != 'funcdef': | |
681 | self.add_issue(self.get_node(leaf), message="'%s' outside function" % leaf.value) | |
682 | elif self._normalizer.context.is_async_funcdef() \ | |
683 | and any(self._normalizer.context.node.iter_yield_exprs()): | |
684 | if leaf.value == 'return' and leaf.parent.type == 'return_stmt': | |
685 | return True | |
686 | ||
687 | ||
688 | @ErrorFinder.register_rule(type='strings') | |
689 | class _BytesAndStringMix(SyntaxRule): | |
690 | # e.g. 's' b'' | |
691 | message = "cannot mix bytes and nonbytes literals" | |
692 | ||
693 | def _is_bytes_literal(self, string): | |
694 | if string.type == 'fstring': | |
695 | return False | |
696 | return 'b' in string.string_prefix.lower() | |
697 | ||
698 | def is_issue(self, node): | |
699 | first = node.children[0] | |
700 | first_is_bytes = self._is_bytes_literal(first) | |
701 | for string in node.children[1:]: | |
702 | if first_is_bytes != self._is_bytes_literal(string): | |
703 | return True | |
704 | ||
705 | ||
706 | @ErrorFinder.register_rule(type='import_as_names') | |
707 | class _TrailingImportComma(SyntaxRule): | |
708 | # e.g. from foo import a, | |
709 | message = "trailing comma not allowed without surrounding parentheses" | |
710 | ||
711 | def is_issue(self, node): | |
712 | if node.children[-1] == ',' and node.parent.children[-1] != ')': | |
713 | return True | |
714 | ||
715 | ||
716 | @ErrorFinder.register_rule(type='import_from') | |
717 | class _ImportStarInFunction(SyntaxRule): | |
718 | message = "import * only allowed at module level" | |
719 | ||
720 | def is_issue(self, node): | |
721 | return node.is_star_import() and self._normalizer.context.parent_context is not None | |
722 | ||
723 | ||
724 | @ErrorFinder.register_rule(type='import_from') | |
725 | class _FutureImportRule(SyntaxRule): | |
726 | message = "from __future__ imports must occur at the beginning of the file" | |
727 | ||
728 | def is_issue(self, node): | |
729 | if _is_future_import(node): | |
730 | if not _is_future_import_first(node): | |
731 | return True | |
732 | ||
733 | for from_name, future_name in node.get_paths(): | |
734 | name = future_name.value | |
735 | allowed_futures = list(ALLOWED_FUTURES) | |
736 | if self._normalizer.version >= (3, 7): | |
737 | allowed_futures.append('annotations') | |
738 | if name == 'braces': | |
739 | self.add_issue(node, message="not a chance") | |
740 | elif name == 'barry_as_FLUFL': | |
741 | m = "Seriously I'm not implementing this :) ~ Dave" | |
742 | self.add_issue(node, message=m) | |
743 | elif name not in allowed_futures: | |
744 | message = "future feature %s is not defined" % name | |
745 | self.add_issue(node, message=message) | |
746 | ||
747 | ||
748 | @ErrorFinder.register_rule(type='star_expr') | |
749 | class _StarExprRule(SyntaxRule): | |
750 | message_iterable_unpacking = "iterable unpacking cannot be used in comprehension" | |
751 | ||
752 | def is_issue(self, node): | |
753 | def check_delete_starred(node): | |
754 | while node.parent is not None: | |
755 | node = node.parent | |
756 | if node.type == 'del_stmt': | |
757 | return True | |
758 | if node.type not in (*_STAR_EXPR_PARENTS, 'atom'): | |
759 | return False | |
760 | return False | |
761 | ||
762 | if self._normalizer.version >= (3, 9): | |
763 | ancestor = node.parent | |
764 | else: | |
765 | ancestor = _skip_parens_bottom_up(node) | |
766 | # starred expression not in tuple/list/set | |
767 | if ancestor.type not in (*_STAR_EXPR_PARENTS, 'dictorsetmaker') \ | |
768 | and not (ancestor.type == 'atom' and ancestor.children[0] != '('): | |
769 | self.add_issue(node, message="can't use starred expression here") | |
770 | return | |
771 | ||
772 | if check_delete_starred(node): | |
773 | if self._normalizer.version >= (3, 9): | |
774 | self.add_issue(node, message="cannot delete starred") | |
775 | else: | |
776 | self.add_issue(node, message="can't use starred expression here") | |
777 | return | |
778 | ||
779 | if node.parent.type == 'testlist_comp': | |
780 | # [*[] for a in [1]] | |
781 | if node.parent.children[1].type in _COMP_FOR_TYPES: | |
782 | self.add_issue(node, message=self.message_iterable_unpacking) | |
783 | ||
784 | ||
785 | @ErrorFinder.register_rule(types=_STAR_EXPR_PARENTS) | |
786 | class _StarExprParentRule(SyntaxRule): | |
787 | def is_issue(self, node): | |
788 | def is_definition(node, ancestor): | |
789 | if ancestor is None: | |
790 | return False | |
791 | ||
792 | type_ = ancestor.type | |
793 | if type_ == 'trailer': | |
794 | return False | |
795 | ||
796 | if type_ == 'expr_stmt': | |
797 | return node.start_pos < ancestor.children[-1].start_pos | |
798 | ||
799 | return is_definition(node, ancestor.parent) | |
800 | ||
801 | if is_definition(node, node.parent): | |
802 | args = [c for c in node.children if c != ','] | |
803 | starred = [c for c in args if c.type == 'star_expr'] | |
804 | if len(starred) > 1: | |
805 | if self._normalizer.version < (3, 9): | |
806 | message = "two starred expressions in assignment" | |
807 | else: | |
808 | message = "multiple starred expressions in assignment" | |
809 | self.add_issue(starred[1], message=message) | |
810 | elif starred: | |
811 | count = args.index(starred[0]) | |
812 | if count >= 256: | |
813 | message = "too many expressions in star-unpacking assignment" | |
814 | self.add_issue(starred[0], message=message) | |
815 | ||
816 | ||
817 | @ErrorFinder.register_rule(type='annassign') | |
818 | class _AnnotatorRule(SyntaxRule): | |
819 | # True: int | |
820 | # {}: float | |
821 | message = "illegal target for annotation" | |
822 | ||
823 | def get_node(self, node): | |
824 | return node.parent | |
825 | ||
826 | def is_issue(self, node): | |
827 | type_ = None | |
828 | lhs = node.parent.children[0] | |
829 | lhs = _remove_parens(lhs) | |
830 | try: | |
831 | children = lhs.children | |
832 | except AttributeError: | |
833 | pass | |
834 | else: | |
835 | if ',' in children or lhs.type == 'atom' and children[0] == '(': | |
836 | type_ = 'tuple' | |
837 | elif lhs.type == 'atom' and children[0] == '[': | |
838 | type_ = 'list' | |
839 | trailer = children[-1] | |
840 | ||
841 | if type_ is None: | |
842 | if not (lhs.type == 'name' | |
843 | # subscript/attributes are allowed | |
844 | or lhs.type in ('atom_expr', 'power') | |
845 | and trailer.type == 'trailer' | |
846 | and trailer.children[0] != '('): | |
847 | return True | |
848 | else: | |
849 | # x, y: str | |
850 | message = "only single target (not %s) can be annotated" | |
851 | self.add_issue(lhs.parent, message=message % type_) | |
852 | ||
853 | ||
854 | @ErrorFinder.register_rule(type='argument') | |
855 | class _ArgumentRule(SyntaxRule): | |
856 | def is_issue(self, node): | |
857 | first = node.children[0] | |
858 | if self._normalizer.version < (3, 8): | |
859 | # a((b)=c) is valid in <3.8 | |
860 | first = _remove_parens(first) | |
861 | if node.children[1] == '=' and first.type != 'name': | |
862 | if first.type == 'lambdef': | |
863 | # f(lambda: 1=1) | |
864 | if self._normalizer.version < (3, 8): | |
865 | message = "lambda cannot contain assignment" | |
866 | else: | |
867 | message = 'expression cannot contain assignment, perhaps you meant "=="?' | |
868 | else: | |
869 | # f(+x=1) | |
870 | if self._normalizer.version < (3, 8): | |
871 | message = "keyword can't be an expression" | |
872 | else: | |
873 | message = 'expression cannot contain assignment, perhaps you meant "=="?' | |
874 | self.add_issue(first, message=message) | |
875 | ||
876 | if _is_argument_comprehension(node) and node.parent.type == 'classdef': | |
877 | self.add_issue(node, message='invalid syntax') | |
878 | ||
879 | ||
880 | @ErrorFinder.register_rule(type='nonlocal_stmt') | |
881 | class _NonlocalModuleLevelRule(SyntaxRule): | |
882 | message = "nonlocal declaration not allowed at module level" | |
883 | ||
884 | def is_issue(self, node): | |
885 | return self._normalizer.context.parent_context is None | |
886 | ||
887 | ||
888 | @ErrorFinder.register_rule(type='arglist') | |
889 | class _ArglistRule(SyntaxRule): | |
890 | @property | |
891 | def message(self): | |
892 | if self._normalizer.version < (3, 7): | |
893 | return "Generator expression must be parenthesized if not sole argument" | |
894 | else: | |
895 | return "Generator expression must be parenthesized" | |
896 | ||
897 | def is_issue(self, node): | |
898 | arg_set = set() | |
899 | kw_only = False | |
900 | kw_unpacking_only = False | |
901 | for argument in node.children: | |
902 | if argument == ',': | |
903 | continue | |
904 | ||
905 | if argument.type == 'argument': | |
906 | first = argument.children[0] | |
907 | if _is_argument_comprehension(argument) and len(node.children) >= 2: | |
908 | # a(a, b for b in c) | |
909 | return True | |
910 | ||
911 | if first in ('*', '**'): | |
912 | if first == '*': | |
913 | if kw_unpacking_only: | |
914 | # foo(**kwargs, *args) | |
915 | message = "iterable argument unpacking " \ | |
916 | "follows keyword argument unpacking" | |
917 | self.add_issue(argument, message=message) | |
918 | else: | |
919 | kw_unpacking_only = True | |
920 | else: # Is a keyword argument. | |
921 | kw_only = True | |
922 | if first.type == 'name': | |
923 | if first.value in arg_set: | |
924 | # f(x=1, x=2) | |
925 | message = "keyword argument repeated" | |
926 | if self._normalizer.version >= (3, 9): | |
927 | message += ": {}".format(first.value) | |
928 | self.add_issue(first, message=message) | |
929 | else: | |
930 | arg_set.add(first.value) | |
931 | else: | |
932 | if kw_unpacking_only: | |
933 | # f(**x, y) | |
934 | message = "positional argument follows keyword argument unpacking" | |
935 | self.add_issue(argument, message=message) | |
936 | elif kw_only: | |
937 | # f(x=2, y) | |
938 | message = "positional argument follows keyword argument" | |
939 | self.add_issue(argument, message=message) | |
940 | ||
941 | ||
942 | @ErrorFinder.register_rule(type='parameters') | |
943 | @ErrorFinder.register_rule(type='lambdef') | |
944 | class _ParameterRule(SyntaxRule): | |
945 | # def f(x=3, y): pass | |
946 | message = "non-default argument follows default argument" | |
947 | ||
948 | def is_issue(self, node): | |
949 | param_names = set() | |
950 | default_only = False | |
951 | star_seen = False | |
952 | for p in _iter_params(node): | |
953 | if p.type == 'operator': | |
954 | if p.value == '*': | |
955 | star_seen = True | |
956 | default_only = False | |
957 | continue | |
958 | ||
959 | if p.name.value in param_names: | |
960 | message = "duplicate argument '%s' in function definition" | |
961 | self.add_issue(p.name, message=message % p.name.value) | |
962 | param_names.add(p.name.value) | |
963 | ||
964 | if not star_seen: | |
965 | if p.default is None and not p.star_count: | |
966 | if default_only: | |
967 | return True | |
968 | elif p.star_count: | |
969 | star_seen = True | |
970 | default_only = False | |
971 | else: | |
972 | default_only = True | |
973 | ||
974 | ||
975 | @ErrorFinder.register_rule(type='try_stmt') | |
976 | class _TryStmtRule(SyntaxRule): | |
977 | message = "default 'except:' must be last" | |
978 | ||
979 | def is_issue(self, try_stmt): | |
980 | default_except = None | |
981 | for except_clause in try_stmt.children[3::3]: | |
982 | if except_clause in ('else', 'finally'): | |
983 | break | |
984 | if except_clause == 'except': | |
985 | default_except = except_clause | |
986 | elif default_except is not None: | |
987 | self.add_issue(default_except, message=self.message) | |
988 | ||
989 | ||
990 | @ErrorFinder.register_rule(type='fstring') | |
991 | class _FStringRule(SyntaxRule): | |
992 | _fstring_grammar = None | |
993 | message_expr = "f-string expression part cannot include a backslash" | |
994 | message_nested = "f-string: expressions nested too deeply" | |
995 | message_conversion = "f-string: invalid conversion character: expected 's', 'r', or 'a'" | |
996 | ||
997 | def _check_format_spec(self, format_spec, depth): | |
998 | self._check_fstring_contents(format_spec.children[1:], depth) | |
999 | ||
1000 | def _check_fstring_expr(self, fstring_expr, depth): | |
1001 | if depth >= 2: | |
1002 | self.add_issue(fstring_expr, message=self.message_nested) | |
1003 | ||
1004 | expr = fstring_expr.children[1] | |
1005 | if '\\' in expr.get_code(): | |
1006 | self.add_issue(expr, message=self.message_expr) | |
1007 | ||
1008 | children_2 = fstring_expr.children[2] | |
1009 | if children_2.type == 'operator' and children_2.value == '=': | |
1010 | conversion = fstring_expr.children[3] | |
1011 | else: | |
1012 | conversion = children_2 | |
1013 | if conversion.type == 'fstring_conversion': | |
1014 | name = conversion.children[1] | |
1015 | if name.value not in ('s', 'r', 'a'): | |
1016 | self.add_issue(name, message=self.message_conversion) | |
1017 | ||
1018 | format_spec = fstring_expr.children[-2] | |
1019 | if format_spec.type == 'fstring_format_spec': | |
1020 | self._check_format_spec(format_spec, depth + 1) | |
1021 | ||
1022 | def is_issue(self, fstring): | |
1023 | self._check_fstring_contents(fstring.children[1:-1]) | |
1024 | ||
1025 | def _check_fstring_contents(self, children, depth=0): | |
1026 | for fstring_content in children: | |
1027 | if fstring_content.type == 'fstring_expr': | |
1028 | self._check_fstring_expr(fstring_content, depth) | |
1029 | ||
1030 | ||
1031 | class _CheckAssignmentRule(SyntaxRule): | |
1032 | def _check_assignment(self, node, is_deletion=False, is_namedexpr=False, is_aug_assign=False): | |
1033 | error = None | |
1034 | type_ = node.type | |
1035 | if type_ == 'lambdef': | |
1036 | error = 'lambda' | |
1037 | elif type_ == 'atom': | |
1038 | first, second = node.children[:2] | |
1039 | error = _get_comprehension_type(node) | |
1040 | if error is None: | |
1041 | if second.type == 'dictorsetmaker': | |
1042 | if self._normalizer.version < (3, 8): | |
1043 | error = 'literal' | |
1044 | else: | |
1045 | if second.children[1] == ':': | |
1046 | error = 'dict display' | |
1047 | else: | |
1048 | error = 'set display' | |
1049 | elif first == "{" and second == "}": | |
1050 | if self._normalizer.version < (3, 8): | |
1051 | error = 'literal' | |
1052 | else: | |
1053 | error = "dict display" | |
1054 | elif first == "{" and len(node.children) > 2: | |
1055 | if self._normalizer.version < (3, 8): | |
1056 | error = 'literal' | |
1057 | else: | |
1058 | error = "set display" | |
1059 | elif first in ('(', '['): | |
1060 | if second.type == 'yield_expr': | |
1061 | error = 'yield expression' | |
1062 | elif second.type == 'testlist_comp': | |
1063 | # ([a, b] := [1, 2]) | |
1064 | # ((a, b) := [1, 2]) | |
1065 | if is_namedexpr: | |
1066 | if first == '(': | |
1067 | error = 'tuple' | |
1068 | elif first == '[': | |
1069 | error = 'list' | |
1070 | ||
1071 | # This is not a comprehension, they were handled | |
1072 | # further above. | |
1073 | for child in second.children[::2]: | |
1074 | self._check_assignment(child, is_deletion, is_namedexpr, is_aug_assign) | |
1075 | else: # Everything handled, must be useless brackets. | |
1076 | self._check_assignment(second, is_deletion, is_namedexpr, is_aug_assign) | |
1077 | elif type_ == 'keyword': | |
1078 | if node.value == "yield": | |
1079 | error = "yield expression" | |
1080 | elif self._normalizer.version < (3, 8): | |
1081 | error = 'keyword' | |
1082 | else: | |
1083 | error = str(node.value) | |
1084 | elif type_ == 'operator': | |
1085 | if node.value == '...': | |
1086 | error = 'Ellipsis' | |
1087 | elif type_ == 'comparison': | |
1088 | error = 'comparison' | |
1089 | elif type_ in ('string', 'number', 'strings'): | |
1090 | error = 'literal' | |
1091 | elif type_ == 'yield_expr': | |
1092 | # This one seems to be a slightly different warning in Python. | |
1093 | message = 'assignment to yield expression not possible' | |
1094 | self.add_issue(node, message=message) | |
1095 | elif type_ == 'test': | |
1096 | error = 'conditional expression' | |
1097 | elif type_ in ('atom_expr', 'power'): | |
1098 | if node.children[0] == 'await': | |
1099 | error = 'await expression' | |
1100 | elif node.children[-2] == '**': | |
1101 | error = 'operator' | |
1102 | else: | |
1103 | # Has a trailer | |
1104 | trailer = node.children[-1] | |
1105 | assert trailer.type == 'trailer' | |
1106 | if trailer.children[0] == '(': | |
1107 | error = 'function call' | |
1108 | elif is_namedexpr and trailer.children[0] == '[': | |
1109 | error = 'subscript' | |
1110 | elif is_namedexpr and trailer.children[0] == '.': | |
1111 | error = 'attribute' | |
1112 | elif type_ == "fstring": | |
1113 | if self._normalizer.version < (3, 8): | |
1114 | error = 'literal' | |
1115 | else: | |
1116 | error = "f-string expression" | |
1117 | elif type_ in ('testlist_star_expr', 'exprlist', 'testlist'): | |
1118 | for child in node.children[::2]: | |
1119 | self._check_assignment(child, is_deletion, is_namedexpr, is_aug_assign) | |
1120 | elif ('expr' in type_ and type_ != 'star_expr' # is a substring | |
1121 | or '_test' in type_ | |
1122 | or type_ in ('term', 'factor')): | |
1123 | error = 'operator' | |
1124 | elif type_ == "star_expr": | |
1125 | if is_deletion: | |
1126 | if self._normalizer.version >= (3, 9): | |
1127 | error = "starred" | |
1128 | else: | |
1129 | self.add_issue(node, message="can't use starred expression here") | |
1130 | else: | |
1131 | if self._normalizer.version >= (3, 9): | |
1132 | ancestor = node.parent | |
1133 | else: | |
1134 | ancestor = _skip_parens_bottom_up(node) | |
1135 | if ancestor.type not in _STAR_EXPR_PARENTS and not is_aug_assign \ | |
1136 | and not (ancestor.type == 'atom' and ancestor.children[0] == '['): | |
1137 | message = "starred assignment target must be in a list or tuple" | |
1138 | self.add_issue(node, message=message) | |
1139 | ||
1140 | self._check_assignment(node.children[1]) | |
1141 | ||
1142 | if error is not None: | |
1143 | if is_namedexpr: | |
1144 | message = 'cannot use assignment expressions with %s' % error | |
1145 | else: | |
1146 | cannot = "can't" if self._normalizer.version < (3, 8) else "cannot" | |
1147 | message = ' '.join([cannot, "delete" if is_deletion else "assign to", error]) | |
1148 | self.add_issue(node, message=message) | |
1149 | ||
1150 | ||
1151 | @ErrorFinder.register_rule(type='sync_comp_for') | |
1152 | class _CompForRule(_CheckAssignmentRule): | |
1153 | message = "asynchronous comprehension outside of an asynchronous function" | |
1154 | ||
1155 | def is_issue(self, node): | |
1156 | expr_list = node.children[1] | |
1157 | if expr_list.type != 'expr_list': # Already handled. | |
1158 | self._check_assignment(expr_list) | |
1159 | ||
1160 | return node.parent.children[0] == 'async' \ | |
1161 | and not self._normalizer.context.is_async_funcdef() | |
1162 | ||
1163 | ||
1164 | @ErrorFinder.register_rule(type='expr_stmt') | |
1165 | class _ExprStmtRule(_CheckAssignmentRule): | |
1166 | message = "illegal expression for augmented assignment" | |
1167 | extended_message = "'{target}' is an " + message | |
1168 | ||
1169 | def is_issue(self, node): | |
1170 | augassign = node.children[1] | |
1171 | is_aug_assign = augassign != '=' and augassign.type != 'annassign' | |
1172 | ||
1173 | if self._normalizer.version <= (3, 8) or not is_aug_assign: | |
1174 | for before_equal in node.children[:-2:2]: | |
1175 | self._check_assignment(before_equal, is_aug_assign=is_aug_assign) | |
1176 | ||
1177 | if is_aug_assign: | |
1178 | target = _remove_parens(node.children[0]) | |
1179 | # a, a[b], a.b | |
1180 | ||
1181 | if target.type == "name" or ( | |
1182 | target.type in ("atom_expr", "power") | |
1183 | and target.children[1].type == "trailer" | |
1184 | and target.children[-1].children[0] != "(" | |
1185 | ): | |
1186 | return False | |
1187 | ||
1188 | if self._normalizer.version <= (3, 8): | |
1189 | return True | |
1190 | else: | |
1191 | self.add_issue( | |
1192 | node, | |
1193 | message=self.extended_message.format( | |
1194 | target=_get_rhs_name(node.children[0], self._normalizer.version) | |
1195 | ), | |
1196 | ) | |
1197 | ||
1198 | ||
1199 | @ErrorFinder.register_rule(type='with_item') | |
1200 | class _WithItemRule(_CheckAssignmentRule): | |
1201 | def is_issue(self, with_item): | |
1202 | self._check_assignment(with_item.children[2]) | |
1203 | ||
1204 | ||
1205 | @ErrorFinder.register_rule(type='del_stmt') | |
1206 | class _DelStmtRule(_CheckAssignmentRule): | |
1207 | def is_issue(self, del_stmt): | |
1208 | child = del_stmt.children[1] | |
1209 | ||
1210 | if child.type != 'expr_list': # Already handled. | |
1211 | self._check_assignment(child, is_deletion=True) | |
1212 | ||
1213 | ||
1214 | @ErrorFinder.register_rule(type='expr_list') | |
1215 | class _ExprListRule(_CheckAssignmentRule): | |
1216 | def is_issue(self, expr_list): | |
1217 | for expr in expr_list.children[::2]: | |
1218 | self._check_assignment(expr) | |
1219 | ||
1220 | ||
1221 | @ErrorFinder.register_rule(type='for_stmt') | |
1222 | class _ForStmtRule(_CheckAssignmentRule): | |
1223 | def is_issue(self, for_stmt): | |
1224 | # Some of the nodes here are already used, so no else if | |
1225 | expr_list = for_stmt.children[1] | |
1226 | if expr_list.type != 'expr_list': # Already handled. | |
1227 | self._check_assignment(expr_list) | |
1228 | ||
1229 | ||
1230 | @ErrorFinder.register_rule(type='namedexpr_test') | |
1231 | class _NamedExprRule(_CheckAssignmentRule): | |
1232 | # namedexpr_test: test [':=' test] | |
1233 | ||
1234 | def is_issue(self, namedexpr_test): | |
1235 | # assigned name | |
1236 | first = namedexpr_test.children[0] | |
1237 | ||
1238 | def search_namedexpr_in_comp_for(node): | |
1239 | while True: | |
1240 | parent = node.parent | |
1241 | if parent is None: | |
1242 | return parent | |
1243 | if parent.type == 'sync_comp_for' and parent.children[3] == node: | |
1244 | return parent | |
1245 | node = parent | |
1246 | ||
1247 | if search_namedexpr_in_comp_for(namedexpr_test): | |
1248 | # [i+1 for i in (i := range(5))] | |
1249 | # [i+1 for i in (j := range(5))] | |
1250 | # [i+1 for i in (lambda: (j := range(5)))()] | |
1251 | message = 'assignment expression cannot be used in a comprehension iterable expression' | |
1252 | self.add_issue(namedexpr_test, message=message) | |
1253 | ||
1254 | # defined names | |
1255 | exprlist = list() | |
1256 | ||
1257 | def process_comp_for(comp_for): | |
1258 | if comp_for.type == 'sync_comp_for': | |
1259 | comp = comp_for | |
1260 | elif comp_for.type == 'comp_for': | |
1261 | comp = comp_for.children[1] | |
1262 | exprlist.extend(_get_for_stmt_definition_exprs(comp)) | |
1263 | ||
1264 | def search_all_comp_ancestors(node): | |
1265 | has_ancestors = False | |
1266 | while True: | |
1267 | node = node.search_ancestor('testlist_comp', 'dictorsetmaker') | |
1268 | if node is None: | |
1269 | break | |
1270 | for child in node.children: | |
1271 | if child.type in _COMP_FOR_TYPES: | |
1272 | process_comp_for(child) | |
1273 | has_ancestors = True | |
1274 | break | |
1275 | return has_ancestors | |
1276 | ||
1277 | # check assignment expressions in comprehensions | |
1278 | search_all = search_all_comp_ancestors(namedexpr_test) | |
1279 | if search_all: | |
1280 | if self._normalizer.context.node.type == 'classdef': | |
1281 | message = 'assignment expression within a comprehension ' \ | |
1282 | 'cannot be used in a class body' | |
1283 | self.add_issue(namedexpr_test, message=message) | |
1284 | ||
1285 | namelist = [expr.value for expr in exprlist if expr.type == 'name'] | |
1286 | if first.type == 'name' and first.value in namelist: | |
1287 | # [i := 0 for i, j in range(5)] | |
1288 | # [[(i := i) for j in range(5)] for i in range(5)] | |
1289 | # [i for i, j in range(5) if True or (i := 1)] | |
1290 | # [False and (i := 0) for i, j in range(5)] | |
1291 | message = 'assignment expression cannot rebind ' \ | |
1292 | 'comprehension iteration variable %r' % first.value | |
1293 | self.add_issue(namedexpr_test, message=message) | |
1294 | ||
1295 | self._check_assignment(first, is_namedexpr=True) |