Skip to content

Commit a750069

Browse files
jacobtylerwallsPierre-SassoulasDanielNoord
authored
Add optional check redefined-loop-name (#5649)
* Also change message type for inner loops overwriting the outer loop variable from `redefined-outer-name` to `redefined-loop-name` * Update redefined-outer-name description * Add coverage and fix preexisting false positive When the check for inner loops redefining outer loop variables was redefined-outer-name, it assumed enclosing for-loops were in the same namespace. Co-authored-by: Pierre Sassoulas <[email protected]> Co-authored-by: Daniël van Noord <[email protected]>
1 parent 1dcb90a commit a750069

21 files changed

+209
-55
lines changed

ChangeLog

+10
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,16 @@ Release date: TBA
181181

182182
Closes #578
183183

184+
* Added optional extension ``redefined-loop-name`` to emit messages when a loop variable
185+
is redefined in the loop body.
186+
187+
Closes #5072
188+
189+
* Changed message type from ``redefined-outer-name`` to ``redefined-loop-name``
190+
(optional extension) for redefinitions of outer loop variables by inner loops.
191+
192+
Closes #5608
193+
184194
* The ``ignore-mixin-members`` option has been deprecated. You should now use the new
185195
``ignored-checks-for-mixins`` option.
186196

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
for name in names:
2+
name = name.lower() # [redefined-loop-name]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
for name in names:
2+
lowercased_name = name.lower()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[MASTER]
2+
load-plugins=pylint.extensions.redefined_loop_name,

doc/whatsnew/2.14.rst

+10
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ New checkers
4646

4747
Closes #4525
4848

49+
* Added optional extension ``redefined-loop-name`` to emit messages when a loop variable
50+
is redefined in the loop body.
51+
52+
Closes #5072
53+
4954
* Added new message called ``duplicate-value`` which identifies duplicate values inside sets.
5055

5156
Closes #5880
@@ -233,3 +238,8 @@ Other Changes
233238
attribute to itself without any prior assignment.
234239

235240
Closes #1555
241+
242+
* Changed message type from ``redefined-outer-name`` to ``redefined-loop-name``
243+
(optional extension) for redefinitions of outer loop variables by inner loops.
244+
245+
Closes #5608

pylint/checkers/utils.py

+8
Original file line numberDiff line numberDiff line change
@@ -1742,3 +1742,11 @@ def in_type_checking_block(node: nodes.NodeNG) -> bool:
17421742
and ancestor.test.as_string() in TYPING_TYPE_CHECKS_GUARDS
17431743
for ancestor in node.node_ancestors()
17441744
)
1745+
1746+
1747+
@lru_cache()
1748+
def in_for_else_branch(parent: nodes.NodeNG, stmt: nodes.Statement) -> bool:
1749+
"""Returns True if stmt is inside the else branch for a parent For stmt."""
1750+
return isinstance(parent, nodes.For) and any(
1751+
else_stmt.parent_of(stmt) or else_stmt == stmt for else_stmt in parent.orelse
1752+
)

pylint/checkers/variables.py

+3-35
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,6 @@ def _is_from_future_import(stmt, name):
132132
return None
133133

134134

135-
def in_for_else_branch(parent, stmt):
136-
"""Returns True if stmt in inside the else branch for a parent For stmt."""
137-
return isinstance(parent, nodes.For) and any(
138-
else_stmt.parent_of(stmt) or else_stmt == stmt for else_stmt in parent.orelse
139-
)
140-
141-
142135
@lru_cache(maxsize=1000)
143136
def overridden_method(klass, name):
144137
"""Get overridden method if any."""
@@ -448,7 +441,7 @@ def _has_locals_call_after_node(stmt, scope):
448441
"W0621": (
449442
"Redefining name %r from outer scope (line %s)",
450443
"redefined-outer-name",
451-
"Used when a variable's name hides a name defined in the outer scope.",
444+
"Used when a variable's name hides a name defined in an outer scope or except handler.",
452445
),
453446
"W0622": (
454447
"Redefining built-in %r",
@@ -961,7 +954,7 @@ class VariablesChecker(BaseChecker):
961954
Checks for
962955
* unused variables / imports
963956
* undefined variables
964-
* redefinition of variable from builtins or from an outer scope
957+
* redefinition of variable from builtins or from an outer scope or except handler
965958
* use of variable before assignment
966959
* __all__ consistency
967960
* self/cls assignment
@@ -1062,7 +1055,6 @@ def __init__(self, linter=None):
10621055
super().__init__(linter)
10631056
self._to_consume: list[NamesConsumer] = []
10641057
self._checking_mod_attr = None
1065-
self._loop_variables = []
10661058
self._type_annotation_names = []
10671059
self._except_handler_names_queue: list[
10681060
tuple[nodes.ExceptHandler, nodes.AssignName]
@@ -1079,31 +1071,7 @@ def open(self) -> None:
10791071
"undefined-loop-variable"
10801072
)
10811073

1082-
@utils.only_required_for_messages("redefined-outer-name")
1083-
def visit_for(self, node: nodes.For) -> None:
1084-
assigned_to = [a.name for a in node.target.nodes_of_class(nodes.AssignName)]
1085-
1086-
# Only check variables that are used
1087-
dummy_rgx = self.linter.config.dummy_variables_rgx
1088-
assigned_to = [var for var in assigned_to if not dummy_rgx.match(var)]
1089-
1090-
for variable in assigned_to:
1091-
for outer_for, outer_variables in self._loop_variables:
1092-
if variable in outer_variables and not in_for_else_branch(
1093-
outer_for, node
1094-
):
1095-
self.add_message(
1096-
"redefined-outer-name",
1097-
args=(variable, outer_for.fromlineno),
1098-
node=node,
1099-
)
1100-
break
1101-
1102-
self._loop_variables.append((node, assigned_to))
1103-
1104-
@utils.only_required_for_messages("redefined-outer-name")
11051074
def leave_for(self, node: nodes.For) -> None:
1106-
self._loop_variables.pop()
11071075
self._store_type_annotation_names(node)
11081076

11091077
def visit_module(self, node: nodes.Module) -> None:
@@ -2231,7 +2199,7 @@ def _loopvar_name(self, node: astroid.Name) -> None:
22312199
for i, stmt in enumerate(astmts[1:]):
22322200
if astmts[i].statement(future=True).parent_of(
22332201
stmt
2234-
) and not in_for_else_branch(astmts[i].statement(future=True), stmt):
2202+
) and not utils.in_for_else_branch(astmts[i].statement(future=True), stmt):
22352203
continue
22362204
_astmts.append(stmt)
22372205
astmts = _astmts
+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
2+
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
3+
# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
4+
5+
"""Optional checker to warn when loop variables are overwritten in the loop's body."""
6+
7+
from __future__ import annotations
8+
9+
from astroid import nodes
10+
11+
from pylint import checkers
12+
from pylint.checkers import utils
13+
from pylint.interfaces import HIGH
14+
from pylint.lint import PyLinter
15+
16+
17+
class RedefinedLoopName(checkers.BaseChecker):
18+
19+
name = "redefined-loop-name"
20+
21+
msgs = {
22+
"W2901": (
23+
"Redefining %r from loop (line %s)",
24+
"redefined-loop-name",
25+
"Used when a loop variable is overwritten in the loop body.",
26+
),
27+
}
28+
29+
def __init__(self, linter: PyLinter) -> None:
30+
super().__init__(linter)
31+
self._loop_variables: list[
32+
tuple[nodes.For, list[str], nodes.LocalsDictNodeNG]
33+
] = []
34+
35+
@utils.only_required_for_messages("redefined-loop-name")
36+
def visit_assignname(self, node: nodes.AssignName) -> None:
37+
assign_type = node.assign_type()
38+
if not isinstance(assign_type, (nodes.Assign, nodes.AugAssign)):
39+
return
40+
node_scope = node.scope()
41+
for outer_for, outer_variables, outer_for_scope in self._loop_variables:
42+
if node_scope is not outer_for_scope:
43+
continue
44+
if node.name in outer_variables and not utils.in_for_else_branch(
45+
outer_for, node
46+
):
47+
self.add_message(
48+
"redefined-loop-name",
49+
args=(node.name, outer_for.fromlineno),
50+
node=node,
51+
confidence=HIGH,
52+
)
53+
break
54+
55+
@utils.only_required_for_messages("redefined-loop-name")
56+
def visit_for(self, node: nodes.For) -> None:
57+
assigned_to = [a.name for a in node.target.nodes_of_class(nodes.AssignName)]
58+
# Only check variables that are used
59+
assigned_to = [
60+
var
61+
for var in assigned_to
62+
if not self.linter.config.dummy_variables_rgx.match(var)
63+
]
64+
65+
node_scope = node.scope()
66+
for variable in assigned_to:
67+
for outer_for, outer_variables, outer_for_scope in self._loop_variables:
68+
if node_scope is not outer_for_scope:
69+
continue
70+
if variable in outer_variables and not utils.in_for_else_branch(
71+
outer_for, node
72+
):
73+
self.add_message(
74+
"redefined-loop-name",
75+
args=(variable, outer_for.fromlineno),
76+
node=node,
77+
confidence=HIGH,
78+
)
79+
break
80+
81+
self._loop_variables.append((node, assigned_to, node.scope()))
82+
83+
@utils.only_required_for_messages("redefined-loop-name")
84+
def leave_for(self, node: nodes.For) -> None: # pylint: disable=unused-argument
85+
self._loop_variables.pop()
86+
87+
88+
def register(linter: PyLinter) -> None:
89+
linter.register_checker(RedefinedLoopName(linter))

tests/functional/c/cellvar_escaping_loop.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def good_case9():
7272

7373

7474
def good_case10():
75-
"""Ignore when a loop variable is showdowed by an inner function"""
75+
"""Ignore when a loop variable is shadowed by an inner function"""
7676
lst = []
7777
for i in range(10): # pylint: disable=unused-variable
7878
def func():
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Tests for redefinitions of loop variables inside the loop body.
2+
3+
See: https://github.com/PyCQA/pylint/issues/5608
4+
"""
5+
# pylint: disable=invalid-name
6+
7+
lines = ["1\t", "2\t"]
8+
for line in lines:
9+
line = line.strip() # [redefined-loop-name]
10+
11+
for i in range(8):
12+
for j in range(8):
13+
for i in range(j): # [redefined-loop-name]
14+
j = i # [redefined-loop-name]
15+
print(i, j)
16+
17+
for i in range(8):
18+
for j in range(8):
19+
for k in range(j):
20+
k = (i, j) # [redefined-loop-name]
21+
print(i, j, k)
22+
23+
lines = [(1, "1\t"), (2, "2\t")]
24+
for i, line in lines:
25+
line = line.strip() # [redefined-loop-name]
26+
27+
line = "no warning"
28+
29+
for i in range(10):
30+
i += 1 # [redefined-loop-name]
31+
32+
33+
def outer():
34+
"""No redefined-loop-name for variables in nested scopes"""
35+
for i1 in range(5):
36+
def inner():
37+
"""No warning, because i has a new scope"""
38+
for i1 in range(3):
39+
print(i1)
40+
print(i1)
41+
inner()
42+
43+
44+
def outer2():
45+
"""Similar, but with an assignment instead of homonymous loop variables"""
46+
for i1 in range(5):
47+
def inner():
48+
"""No warning, because i has a new scope"""
49+
for _ in range(3):
50+
i1 = 0
51+
print(i1)
52+
print(i1)
53+
inner()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[MASTER]
2+
load-plugins=pylint.extensions.redefined_loop_name,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
redefined-loop-name:9:4:9:8::Redefining 'line' from loop (line 8):HIGH
2+
redefined-loop-name:13:8:15:23::Redefining 'i' from loop (line 11):HIGH
3+
redefined-loop-name:14:12:14:13::Redefining 'j' from loop (line 12):HIGH
4+
redefined-loop-name:20:12:20:13::Redefining 'k' from loop (line 19):HIGH
5+
redefined-loop-name:25:4:25:8::Redefining 'line' from loop (line 24):HIGH
6+
redefined-loop-name:30:4:30:5::Redefining 'i' from loop (line 29):HIGH

tests/functional/r/reused_outer_loop_variable.py renamed to tests/functional/ext/redefined_loop_name/reused_outer_loop_variable.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,22 @@
44

55
# Simple nested loop
66
for i in range(10):
7-
for i in range(10): #[redefined-outer-name]
7+
for i in range(10): #[redefined-loop-name]
88
print(i)
99

1010
# When outer loop unpacks a tuple
1111
for i, i_again in enumerate(range(10)):
12-
for i in range(10): #[redefined-outer-name]
12+
for i in range(10): #[redefined-loop-name]
1313
print(i, i_again)
1414

1515
# When inner loop unpacks a tuple
1616
for i in range(10):
17-
for i, i_again in range(10): #[redefined-outer-name]
17+
for i, i_again in range(10): #[redefined-loop-name]
1818
print(i, i_again)
1919

2020
# With nested tuple unpacks
2121
for (a, (b, c)) in [(1, (2, 3))]:
22-
for i, a in range(10): #[redefined-outer-name]
22+
for i, a in range(10): #[redefined-loop-name]
2323
print(i, a, b, c)
2424

2525
# Ignores when in else
@@ -35,3 +35,8 @@
3535
for _ in range(10):
3636
for _ in range(10):
3737
print("Hello")
38+
39+
# Unpacking
40+
for i, *j in [(1, 2, 3, 4)]:
41+
for j in range(i): # [redefined-loop-name]
42+
print(j)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[MASTER]
2+
load-plugins=pylint.extensions.redefined_loop_name,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
redefined-loop-name:7:4:8:16::Redefining 'i' from loop (line 6):HIGH
2+
redefined-loop-name:12:4:13:25::Redefining 'i' from loop (line 11):HIGH
3+
redefined-loop-name:17:4:18:25::Redefining 'i' from loop (line 16):HIGH
4+
redefined-loop-name:22:4:23:25::Redefining 'a' from loop (line 21):HIGH
5+
redefined-loop-name:41:4:42:16::Redefining 'j' from loop (line 40):HIGH

tests/functional/r/redefine_loop.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
"""Test case for variable redefined in inner loop."""
1+
"""No message is emitted for variables overwritten in inner loops by default.
2+
See redefined-loop-name optional checker."""
23
for item in range(0, 5):
34
print("hello")
4-
for item in range(5, 10): #[redefined-outer-name]
5+
for item in range(5, 10):
56
print(item)
67
print("yay")
78
print(item)

tests/functional/r/redefine_loop.txt

-1
This file was deleted.

tests/functional/r/reused_outer_loop_variable.txt

-4
This file was deleted.

tests/functional/r/reused_outer_loop_variable_py3.py

-5
This file was deleted.

tests/functional/r/reused_outer_loop_variable_py3.txt

-1
This file was deleted.

tests/functional/t/too/too_many_nested_blocks.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def my_function():
99
while True:
1010
try:
1111
if True:
12-
i += 1
12+
print(i)
1313
except IOError:
1414
pass
1515

@@ -18,7 +18,7 @@ def my_function():
1818
if i == 2:
1919
while True:
2020
try:
21-
i += 1
21+
print(i)
2222
except IOError:
2323
pass
2424

0 commit comments

Comments
 (0)