Skip to content

Commit dc44856

Browse files
fix: avoid false positive when module-level names match class-level names
- Add scope comparision to avoid module-level constants to be incorrectly classified as variables when a class-level attribute with the same name exists Closes #10719
1 parent 4f0716a commit dc44856

File tree

6 files changed

+80
-13
lines changed

6 files changed

+80
-13
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fixed false positive for ``invalid-name`` where module-level constants were incorrectly classified as variables when a class-level attribute with the same name exists.
2+
3+
Closes #10719

pylint/checkers/utils.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1843,28 +1843,48 @@ def is_sys_guard(node: nodes.If) -> bool:
18431843
return False
18441844

18451845

1846+
def _is_node_in_same_scope(
1847+
candidate: nodes.NodeNG, node_scope: nodes.LocalsDictNodeNG
1848+
) -> bool:
1849+
if isinstance(candidate, (nodes.ClassDef, nodes.FunctionDef)):
1850+
return bool(
1851+
candidate.parent is not None and candidate.parent.scope() == node_scope
1852+
)
1853+
return bool(candidate.scope() == node_scope)
1854+
1855+
18461856
def is_reassigned_before_current(node: nodes.NodeNG, varname: str) -> bool:
18471857
"""Check if the given variable name is reassigned in the same scope before the
18481858
current node.
18491859
"""
1850-
return any(
1851-
a.name == varname and a.lineno < node.lineno
1852-
for a in node.scope().nodes_of_class(
1853-
(nodes.AssignName, nodes.ClassDef, nodes.FunctionDef)
1854-
)
1855-
)
1860+
node_scope = node.scope()
1861+
node_lineno = node.lineno
1862+
if node_lineno is None:
1863+
return False
1864+
for a in node_scope.nodes_of_class(
1865+
(nodes.AssignName, nodes.ClassDef, nodes.FunctionDef)
1866+
):
1867+
if a.name == varname and a.lineno is not None and a.lineno < node_lineno:
1868+
if _is_node_in_same_scope(a, node_scope):
1869+
return True
1870+
return False
18561871

18571872

18581873
def is_reassigned_after_current(node: nodes.NodeNG, varname: str) -> bool:
18591874
"""Check if the given variable name is reassigned in the same scope after the
18601875
current node.
18611876
"""
1862-
return any(
1863-
a.name == varname and a.lineno > node.lineno
1864-
for a in node.scope().nodes_of_class(
1865-
(nodes.AssignName, nodes.ClassDef, nodes.FunctionDef)
1866-
)
1867-
)
1877+
node_scope = node.scope()
1878+
node_lineno = node.lineno
1879+
if node_lineno is None:
1880+
return False
1881+
for a in node_scope.nodes_of_class(
1882+
(nodes.AssignName, nodes.ClassDef, nodes.FunctionDef)
1883+
):
1884+
if a.name == varname and a.lineno is not None and a.lineno > node_lineno:
1885+
if _is_node_in_same_scope(a, node_scope):
1886+
return True
1887+
return False
18681888

18691889

18701890
def is_deleted_after_current(node: nodes.NodeNG, varname: str) -> bool:

tests/checkers/unittest_utils.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,3 +520,32 @@ def test_is_typing_member() -> None:
520520
)
521521
assert not utils.is_typing_member(code[0], ("Literal",))
522522
assert not utils.is_typing_member(code[1], ("Literal",))
523+
524+
525+
def test_is_reassigned_after_current_requires_isinstance_check() -> None:
526+
tree = astroid.parse(
527+
"""
528+
CONSTANT = 1
529+
530+
def global_function_assign():
531+
global CONSTANT
532+
def CONSTANT():
533+
pass
534+
CONSTANT()
535+
"""
536+
)
537+
func = tree.body[1]
538+
global_stmt = func.body[0]
539+
nested_func = func.body[1]
540+
541+
assert isinstance(global_stmt, nodes.Global)
542+
assert isinstance(nested_func, nodes.FunctionDef)
543+
544+
node_scope = global_stmt.scope()
545+
546+
assert nested_func.scope() == nested_func
547+
assert nested_func.scope() != node_scope
548+
549+
assert nested_func.parent.scope() == node_scope
550+
551+
assert utils.is_reassigned_after_current(global_stmt, "CONSTANT") is True
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Test module-level constants with class attribute same name
2+
Regression test for #10719.
3+
"""
4+
# pylint: disable=missing-docstring, too-few-public-methods, redefined-builtin
5+
6+
7+
class Theme:
8+
INPUT = ">>> "
9+
10+
11+
INPUT = Theme()
12+
input = Theme()
13+
OUTPUT = Theme()
14+
output = Theme()

tests/functional/n/no/no_dummy_redefined.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Make sure warnings about redefinitions do not trigger for dummy variables."""
2+
# pylint: disable=invalid-name
23

34

45
_, INTERESTING = 'a=b'.split('=')
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
redefined-outer-name:11:4:11:9:clobbering:Redefining name 'value' from outer scope (line 6):UNDEFINED
1+
redefined-outer-name:12:4:12:9:clobbering:Redefining name 'value' from outer scope (line 7):UNDEFINED

0 commit comments

Comments
 (0)