Skip to content

[optional ext] Emit redefined-loop-name for redefinitions of loop variables in body #5649

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 38 commits into from
May 2, 2022
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a80ad9f
Fix #5608: Emit `redefined-outer-name` for redefinitions of loop vari…
jacobtylerwalls Jan 8, 2022
fa33c2e
Extend solution to AugAssign
jacobtylerwalls Mar 28, 2022
859b01e
Merge branch 'main' into redefined-loop-var
jacobtylerwalls Mar 28, 2022
5929ce7
Update tests
jacobtylerwalls Mar 28, 2022
91e118a
Fix false positive involving functions nested under loops
jacobtylerwalls Mar 29, 2022
5b61d0a
Apply suggestions from code review
jacobtylerwalls Mar 29, 2022
c80336d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 29, 2022
506c194
Update test results
jacobtylerwalls Mar 29, 2022
a67f984
Remove is_message_enabled() call
jacobtylerwalls Mar 29, 2022
e6791e2
Create `redefined-loop-name` message
jacobtylerwalls Mar 30, 2022
05dc2df
Update redefined-outer-name description
jacobtylerwalls Apr 4, 2022
45987bb
Merge branch 'main' into redefined-loop-var
jacobtylerwalls Apr 4, 2022
75f98a9
Make `redefined-loop-name` an optional extension
jacobtylerwalls Apr 4, 2022
7147199
Delete relocated code
jacobtylerwalls Apr 4, 2022
c6e9766
Remove moved message description
jacobtylerwalls Apr 4, 2022
46e0f59
Remove disables
jacobtylerwalls Apr 4, 2022
8378199
Bump message ID
jacobtylerwalls Apr 4, 2022
19962f6
Add examples
jacobtylerwalls Apr 5, 2022
c2b5d4e
Merge branch 'main' into redefined-loop-var
jacobtylerwalls Apr 5, 2022
b5c6aa5
Add pylintrc
jacobtylerwalls Apr 5, 2022
09705bd
Add coverage and fix preexisting false positive
jacobtylerwalls Apr 7, 2022
5970936
Add coverage
jacobtylerwalls Apr 7, 2022
cb3cfbf
Merge branch 'main' into redefined-loop-var
jacobtylerwalls Apr 28, 2022
f15d662
Apply suggestions from code review
jacobtylerwalls Apr 30, 2022
3cc7f0a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 30, 2022
3fb8477
Use self.linter.config and remove test file artifact
jacobtylerwalls Apr 30, 2022
86a272e
Merge branch 'redefined-loop-var' of https://github.com/jacobtylerwal…
jacobtylerwalls Apr 30, 2022
e6e4834
Move to checker utils
jacobtylerwalls Apr 30, 2022
0c23bdf
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 30, 2022
d2b487c
typo
jacobtylerwalls Apr 30, 2022
0f67bc9
Fix typing
jacobtylerwalls May 1, 2022
2824a1e
Better names
jacobtylerwalls May 1, 2022
d506334
Add caching
jacobtylerwalls May 1, 2022
7c13e3a
keep scope() around
jacobtylerwalls May 1, 2022
efc734f
more specific type
jacobtylerwalls May 1, 2022
e21a932
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 1, 2022
3990ee6
Remove now unused list
jacobtylerwalls May 1, 2022
836ef8a
Fix existing typo
jacobtylerwalls May 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,16 @@ Release date: TBA

Closes #578

* Added optional extension ``redefined-loop-name`` to emit messages when a loop variable
is redefined in the loop body.

Closes #5072

* Changed message type from ``redefined-outer-name`` to ``redefined-loop-name``
(optional extension) for redefinitions of outer loop variables by inner loops.

Closes #5608

* The ``ignore-mixin-members`` option has been deprecated. You should now use the new
``ignored-checks-for-mixins`` option.

Expand Down
2 changes: 2 additions & 0 deletions doc/data/messages/r/redefined-loop-name/bad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
for item in names:
item = item.lower() # [redefined-loop-name]
2 changes: 2 additions & 0 deletions doc/data/messages/r/redefined-loop-name/good.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
for item in names:
lowercased = item.lower()
2 changes: 2 additions & 0 deletions doc/data/messages/r/redefined-loop-name/pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[MASTER]
load-plugins=pylint.extensions.redefined_loop_name,
10 changes: 10 additions & 0 deletions doc/whatsnew/2.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ New checkers

Closes #4525

* Added optional extension ``redefined-loop-name`` to emit messages when a loop variable
is redefined in the loop body.

Closes #5072

* Added new message called ``duplicate-value`` which identifies duplicate values inside sets.

Closes #5880
Expand Down Expand Up @@ -226,3 +231,8 @@ Other Changes
attribute to itself without any prior assignment.

Closes #1555

* Changed message type from ``redefined-outer-name`` to ``redefined-loop-name``
(optional extension) for redefinitions of outer loop variables by inner loops.

Closes #5608
28 changes: 2 additions & 26 deletions pylint/checkers/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ def _has_locals_call_after_node(stmt, scope):
"W0621": (
"Redefining name %r from outer scope (line %s)",
"redefined-outer-name",
"Used when a variable's name hides a name defined in the outer scope.",
"Used when a variable's name hides a name defined in an outer scope or except handler.",
),
"W0622": (
"Redefining built-in %r",
Expand Down Expand Up @@ -960,7 +960,7 @@ class VariablesChecker(BaseChecker):
Checks for
* unused variables / imports
* undefined variables
* redefinition of variable from builtins or from an outer scope
* redefinition of variable from builtins or from an outer scope or except handler
* use of variable before assignment
* __all__ consistency
* self/cls assignment
Expand Down Expand Up @@ -1078,31 +1078,7 @@ def open(self) -> None:
"undefined-loop-variable"
)

@utils.only_required_for_messages("redefined-outer-name")
def visit_for(self, node: nodes.For) -> None:
assigned_to = [a.name for a in node.target.nodes_of_class(nodes.AssignName)]

# Only check variables that are used
dummy_rgx = self.linter.config.dummy_variables_rgx
assigned_to = [var for var in assigned_to if not dummy_rgx.match(var)]

for variable in assigned_to:
for outer_for, outer_variables in self._loop_variables:
if variable in outer_variables and not in_for_else_branch(
outer_for, node
):
self.add_message(
"redefined-outer-name",
args=(variable, outer_for.fromlineno),
node=node,
)
break

self._loop_variables.append((node, assigned_to))

@utils.only_required_for_messages("redefined-outer-name")
def leave_for(self, node: nodes.For) -> None:
self._loop_variables.pop()
self._store_type_annotation_names(node)

def visit_module(self, node: nodes.Module) -> None:
Expand Down
85 changes: 85 additions & 0 deletions pylint/extensions/redefined_loop_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt

"""Optional checker to warn when loop variables are overwritten in the loop's body."""

from astroid import nodes

from pylint import checkers, interfaces
from pylint.checkers import utils
from pylint.checkers.variables import in_for_else_branch
from pylint.interfaces import HIGH
from pylint.lint import PyLinter
from pylint.utils import utils as pylint_utils


class RedefinedLoopName(checkers.BaseChecker):

__implements__ = interfaces.IAstroidChecker
name = "redefined-loop-name"

msgs = {
"W2901": (
"Redefining %r from loop (line %s)",
"redefined-loop-name",
"Used when a loop variable is overwritten in the loop body.",
),
}

def __init__(self, linter=None):
super().__init__(linter)
self._loop_variables = []

@utils.check_messages("redefined-loop-name")
def visit_assignname(self, node: nodes.AssignName) -> None:
assign_type = node.assign_type()
if not isinstance(assign_type, (nodes.Assign, nodes.AugAssign)):
return
node_scope = node.scope()
for outer_for, outer_variables in self._loop_variables:
if node_scope is not outer_for.scope():
continue
if node.name in outer_variables and not in_for_else_branch(outer_for, node):
self.add_message(
"redefined-loop-name",
args=(node.name, outer_for.fromlineno),
node=node,
confidence=HIGH,
)
break

@utils.check_messages("redefined-loop-name")
def visit_for(self, node: nodes.For) -> None:
assigned_to = [a.name for a in node.target.nodes_of_class(nodes.AssignName)]
# Only check variables that are used
dummy_rgx = pylint_utils.get_global_option(
self, "dummy-variables-rgx", default=None
)
assigned_to = [var for var in assigned_to if not dummy_rgx.match(var)]

node_scope = node.scope()
for variable in assigned_to:
for outer_for, outer_variables in self._loop_variables:
if node_scope is not outer_for.scope():
continue
if variable in outer_variables and not in_for_else_branch(
outer_for, node
):
self.add_message(
"redefined-loop-name",
args=(variable, outer_for.fromlineno),
node=node,
confidence=HIGH,
)
break

self._loop_variables.append((node, assigned_to))

@utils.check_messages("redefined-loop-name")
def leave_for(self, node: nodes.For) -> None: # pylint: disable=unused-argument
self._loop_variables.pop()


def register(linter: PyLinter) -> None:
linter.register_checker(RedefinedLoopName(linter))
2 changes: 1 addition & 1 deletion tests/functional/c/cellvar_escaping_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def good_case9():


def good_case10():
"""Ignore when a loop variable is showdowed by an inner function"""
"""Ignore when a loop variable is shadowed by an inner function"""
lst = []
for i in range(10): # pylint: disable=unused-variable
def func():
Expand Down
53 changes: 53 additions & 0 deletions tests/functional/ext/redefined_loop_name/redefined_loop_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Tests for redefinitions of loop variables inside the loop body.

See: https://github.com/PyCQA/pylint/issues/5608
"""
# pylint: disable=invalid-name

lines = ["1\t", "2\t"]
for line in lines:
line = line.strip() # [redefined-loop-name]

for i in range(8):
for j in range(8):
for i in range(j): # [redefined-loop-name]
j = i # [redefined-loop-name]
print(i, j)

for i in range(8):
for j in range(8):
for k in range(j):
k = (i, j) # [redefined-loop-name]
print(i, j, k)

lines = [(1, "1\t"), (2, "2\t")]
for i, line in lines:
line = line.strip() # [redefined-loop-name]

line = "no warning"

for i in range(10):
i += 1 # [redefined-loop-name]


def outer():
"""No redefined-loop-name for variables in nested scopes"""
for i1 in range(5):
def inner():
"""No warning, because i has a new scope"""
for i1 in range(3):
print(i1)
print(i1)
inner()


def outer2():
"""Similar, but with an assignment instead of homonymous loop variables"""
for i1 in range(5):
def inner():
"""No warning, because i has a new scope"""
for _ in range(3):
i1 = 0
print(i1)
print(i1)
inner()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[MASTER]
load-plugins=pylint.extensions.redefined_loop_name,
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
redefined-loop-name:9:4:9:8::Redefining 'line' from loop (line 8):HIGH
redefined-loop-name:13:8:15:23::Redefining 'i' from loop (line 11):HIGH
redefined-loop-name:14:12:14:13::Redefining 'j' from loop (line 12):HIGH
redefined-loop-name:20:12:20:13::Redefining 'k' from loop (line 19):HIGH
redefined-loop-name:25:4:25:8::Redefining 'line' from loop (line 24):HIGH
redefined-loop-name:30:4:30:5::Redefining 'i' from loop (line 29):HIGH
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@

# Simple nested loop
for i in range(10):
for i in range(10): #[redefined-outer-name]
for i in range(10): #[redefined-loop-name]
print(i)

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

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

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

# Ignores when in else
Expand All @@ -35,3 +35,8 @@
for _ in range(10):
for _ in range(10):
print("Hello")

# Unpacking
for i, *j in [(1, 2, 3, 4)]:
for j in range(i): # [redefined-loop-name]
print(j)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[MASTER]
load-plugins=pylint.extensions.redefined_loop_name,
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
redefined-loop-name:7:4:8:16::Redefining 'i' from loop (line 6):HIGH
redefined-loop-name:12:4:13:25::Redefining 'i' from loop (line 11):HIGH
redefined-loop-name:17:4:18:25::Redefining 'i' from loop (line 16):HIGH
redefined-loop-name:22:4:23:25::Redefining 'a' from loop (line 21):HIGH
redefined-loop-name:41:4:42:16::Redefining 'j' from loop (line 40):HIGH
5 changes: 3 additions & 2 deletions tests/functional/r/redefine_loop.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Test case for variable redefined in inner loop."""
"""No message is emitted for variables overwritten in inner loops by default.
See redefined-loop-name optional checker."""
for item in range(0, 5):
print("hello")
for item in range(5, 10): #[redefined-outer-name]
for item in range(5, 10):
print(item)
print("yay")
print(item)
Expand Down
1 change: 0 additions & 1 deletion tests/functional/r/redefine_loop.txt

This file was deleted.

6 changes: 6 additions & 0 deletions tests/functional/r/redefined/redefined_loop_name.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
redefined-loop-name:9:4:9:8::Redefining 'line' from loop (line 8):HIGH
redefined-loop-name:13:8:15:23::Redefining 'i' from loop (line 11):UNDEFINED
redefined-loop-name:14:12:14:13::Redefining 'j' from loop (line 12):HIGH
redefined-loop-name:20:12:20:13::Redefining 'k' from loop (line 19):HIGH
redefined-loop-name:25:4:25:8::Redefining 'line' from loop (line 24):HIGH
redefined-loop-name:30:4:30:5::Redefining 'i' from loop (line 29):HIGH
4 changes: 0 additions & 4 deletions tests/functional/r/reused_outer_loop_variable.txt

This file was deleted.

5 changes: 0 additions & 5 deletions tests/functional/r/reused_outer_loop_variable_py3.py

This file was deleted.

1 change: 0 additions & 1 deletion tests/functional/r/reused_outer_loop_variable_py3.txt

This file was deleted.

4 changes: 2 additions & 2 deletions tests/functional/t/too/too_many_nested_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def my_function():
while True:
try:
if True:
i += 1
print(i)
except IOError:
pass

Expand All @@ -18,7 +18,7 @@ def my_function():
if i == 2:
while True:
try:
i += 1
print(i)
except IOError:
pass

Expand Down