|
1 | 1 | from dataclasses import dataclass |
2 | 2 |
|
3 | 3 | from mypy.nodes import ( |
| 4 | + AssignmentStmt, |
| 5 | + Block, |
4 | 6 | CallExpr, |
5 | 7 | DictionaryComprehension, |
6 | 8 | Expression, |
7 | 9 | ForStmt, |
8 | 10 | GeneratorExpr, |
| 11 | + MypyFile, |
9 | 12 | NameExpr, |
10 | 13 | Node, |
| 14 | + Statement, |
11 | 15 | TupleExpr, |
12 | 16 | ) |
13 | 17 |
|
14 | 18 | from refurb.checks.common import ( |
| 19 | + ReadCountVisitor, |
| 20 | + check_block_like, |
15 | 21 | check_for_loop_like, |
16 | 22 | get_mypy_type, |
17 | 23 | is_name_unused_in_contexts, |
@@ -57,11 +63,81 @@ class ErrorInfo(Error): |
57 | 63 | categories = ("builtin",) |
58 | 64 |
|
59 | 65 |
|
| 66 | +def _assigns_name(stmt: Statement, name: NameExpr) -> bool: |
| 67 | + """Check if a statement assigns to the given name.""" |
| 68 | + match stmt: |
| 69 | + case ForStmt(index=index): |
| 70 | + return _node_contains_name(index, name) |
| 71 | + case AssignmentStmt(lvalues=lvalues): |
| 72 | + return any(_node_contains_name(lv, name) for lv in lvalues) |
| 73 | + return False |
| 74 | + |
| 75 | + |
| 76 | +def _node_contains_name(node: Node, name: NameExpr) -> bool: |
| 77 | + """Check if a node contains a NameExpr with the same fullname.""" |
| 78 | + visitor = ReadCountVisitor(name) |
| 79 | + visitor.accept(node) |
| 80 | + return bool(visitor.was_read) |
| 81 | + |
| 82 | + |
| 83 | +def _is_name_read_after_loop(name: NameExpr, remaining_stmts: list[Statement]) -> bool: |
| 84 | + """Check if name is read in statements after the loop, stopping at reassignment.""" |
| 85 | + for stmt in remaining_stmts: |
| 86 | + if _assigns_name(stmt, name): |
| 87 | + # Name is reassigned — reads after this point don't count |
| 88 | + # as uses of the loop variable. But check the RHS first. |
| 89 | + rhs: Expression | None = None |
| 90 | + if isinstance(stmt, AssignmentStmt): |
| 91 | + rhs = stmt.rvalue |
| 92 | + elif isinstance(stmt, ForStmt): |
| 93 | + rhs = stmt.expr |
| 94 | + |
| 95 | + if rhs is not None: |
| 96 | + visitor = ReadCountVisitor(name) |
| 97 | + visitor.accept(rhs) |
| 98 | + if visitor.was_read: |
| 99 | + return True |
| 100 | + |
| 101 | + return False |
| 102 | + |
| 103 | + # Check if name is read in this statement |
| 104 | + visitor = ReadCountVisitor(name) |
| 105 | + visitor.accept(stmt) |
| 106 | + if visitor.was_read: |
| 107 | + return True |
| 108 | + |
| 109 | + return False |
| 110 | + |
| 111 | + |
60 | 112 | def check( |
61 | | - node: ForStmt | GeneratorExpr | DictionaryComprehension, |
| 113 | + node: Block | MypyFile | GeneratorExpr | DictionaryComprehension, |
62 | 114 | errors: list[Error], |
63 | 115 | ) -> None: |
64 | | - check_for_loop_like(check_enumerate_call, node, errors) |
| 116 | + match node: |
| 117 | + case GeneratorExpr() | DictionaryComprehension(): |
| 118 | + check_for_loop_like(check_enumerate_call, node, errors) |
| 119 | + |
| 120 | + case Block() | MypyFile(): |
| 121 | + check_block_like(check_stmts_for_enumerate, node, errors) |
| 122 | + |
| 123 | + |
| 124 | +def check_stmts_for_enumerate(stmts: list[Statement], errors: list[Error]) -> None: |
| 125 | + for i, stmt in enumerate(stmts): |
| 126 | + if not isinstance(stmt, ForStmt): |
| 127 | + continue |
| 128 | + |
| 129 | + match stmt.index, stmt.expr: |
| 130 | + case ( |
| 131 | + TupleExpr(items=[NameExpr() as index, NameExpr() as value]), |
| 132 | + CallExpr( |
| 133 | + callee=NameExpr(fullname="builtins.enumerate"), |
| 134 | + args=[enumerate_arg], |
| 135 | + ), |
| 136 | + ) if is_subclass(get_mypy_type(enumerate_arg), "typing.Sequence"): |
| 137 | + remaining = stmts[i + 1 :] |
| 138 | + check_unused_index_or_value_in_block( |
| 139 | + index, value, stmt.body, remaining, errors, enumerate_arg |
| 140 | + ) |
65 | 141 |
|
66 | 142 |
|
67 | 143 | def check_enumerate_call( |
@@ -94,3 +170,23 @@ def check_unused_index_or_value( |
94 | 170 | msg = f"Value is unused, use `for {stringify(index)} in range(len({stringify(enumerate_arg)}))` instead" # noqa: E501 |
95 | 171 |
|
96 | 172 | errors.append(ErrorInfo.from_node(value, msg)) |
| 173 | + |
| 174 | + |
| 175 | +def check_unused_index_or_value_in_block( # noqa: PLR0913, PLR0917 |
| 176 | + index: NameExpr, |
| 177 | + value: NameExpr, |
| 178 | + body: Node, |
| 179 | + remaining_stmts: list[Statement], |
| 180 | + errors: list[Error], |
| 181 | + enumerate_arg: Expression, |
| 182 | +) -> None: |
| 183 | + index_unused_in_body = is_name_unused_in_contexts(index, [body]) |
| 184 | + value_unused_in_body = is_name_unused_in_contexts(value, [body]) |
| 185 | + |
| 186 | + if index_unused_in_body and not _is_name_read_after_loop(index, remaining_stmts): |
| 187 | + msg = f"Index is unused, use `for {stringify(value)} in {stringify(enumerate_arg)}` instead" # noqa: E501 |
| 188 | + errors.append(ErrorInfo.from_node(index, msg)) |
| 189 | + |
| 190 | + if value_unused_in_body and not _is_name_read_after_loop(value, remaining_stmts): |
| 191 | + msg = f"Value is unused, use `for {stringify(index)} in range(len({stringify(enumerate_arg)}))` instead" # noqa: E501 |
| 192 | + errors.append(ErrorInfo.from_node(value, msg)) |
0 commit comments