Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions doc/whatsnew/fragments/10719.false_positive
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
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.

Closes #10719
46 changes: 34 additions & 12 deletions pylint/checkers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1843,28 +1843,50 @@ def is_sys_guard(node: nodes.If) -> bool:
return False


def _is_node_in_same_scope(
candidate: nodes.NodeNG, node_scope: nodes.LocalsDictNodeNG
) -> bool:
if isinstance(candidate, (nodes.ClassDef, nodes.FunctionDef)):
return candidate.parent is not None and candidate.parent.scope() is node_scope
return candidate.scope() is node_scope


def _is_reassigned_relative_to_current(
node: nodes.NodeNG, varname: str, before: bool
) -> bool:
"""Check if the given variable name is reassigned in the same scope relative to
the current node.
"""
node_scope = node.scope()
node_lineno = node.lineno
if node_lineno is None:
return False
for a in node_scope.nodes_of_class(
(nodes.AssignName, nodes.ClassDef, nodes.FunctionDef)
):
if a.name == varname and a.lineno is not None:
if before:
if a.lineno < node_lineno:
if _is_node_in_same_scope(a, node_scope):
return True
elif a.lineno > node_lineno:
if _is_node_in_same_scope(a, node_scope):
return True
return False


def is_reassigned_before_current(node: nodes.NodeNG, varname: str) -> bool:
"""Check if the given variable name is reassigned in the same scope before the
current node.
"""
return any(
a.name == varname and a.lineno < node.lineno
for a in node.scope().nodes_of_class(
(nodes.AssignName, nodes.ClassDef, nodes.FunctionDef)
)
)
return _is_reassigned_relative_to_current(node, varname, before=True)


def is_reassigned_after_current(node: nodes.NodeNG, varname: str) -> bool:
"""Check if the given variable name is reassigned in the same scope after the
current node.
"""
return any(
a.name == varname and a.lineno > node.lineno
for a in node.scope().nodes_of_class(
(nodes.AssignName, nodes.ClassDef, nodes.FunctionDef)
)
)
return _is_reassigned_relative_to_current(node, varname, before=False)


def is_deleted_after_current(node: nodes.NodeNG, varname: str) -> bool:
Expand Down
29 changes: 29 additions & 0 deletions tests/checkers/unittest_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,3 +520,32 @@ def test_is_typing_member() -> None:
)
assert not utils.is_typing_member(code[0], ("Literal",))
assert not utils.is_typing_member(code[1], ("Literal",))


def test_is_reassigned_after_current_requires_isinstance_check() -> None:
tree = astroid.parse(
"""
CONSTANT = 1

def global_function_assign():
global CONSTANT
def CONSTANT():
pass
CONSTANT()
"""
)
func = tree.body[1]
global_stmt = func.body[0]
nested_func = func.body[1]

assert isinstance(global_stmt, nodes.Global)
assert isinstance(nested_func, nodes.FunctionDef)

node_scope = global_stmt.scope()

assert nested_func.scope() == nested_func
assert nested_func.scope() != node_scope

assert nested_func.parent.scope() == node_scope

assert utils.is_reassigned_after_current(global_stmt, "CONSTANT") is True
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,15 @@ def A(): # [invalid-name]

from typing import Annotated
IntWithAnnotation = Annotated[int, "anything"]


# Regression test for #10719: module-level constants should not be incorrectly
# classified as variables when a class-level attribute with the same name exists.
class Theme:
INPUT = ">>> "


INPUT = Theme()
input = Theme() # pylint: disable=redefined-builtin
OUTPUT = Theme()
output = Theme()
1 change: 1 addition & 0 deletions tests/functional/n/no/no_dummy_redefined.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Make sure warnings about redefinitions do not trigger for dummy variables."""
# pylint: disable=invalid-name


_, INTERESTING = 'a=b'.split('=')
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/n/no/no_dummy_redefined.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
redefined-outer-name:11:4:11:9:clobbering:Redefining name 'value' from outer scope (line 6):UNDEFINED
redefined-outer-name:12:4:12:9:clobbering:Redefining name 'value' from outer scope (line 7):UNDEFINED