Skip to content
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

Fix used-before-assignment false negative for nonlocals #10075

Merged
merged 2 commits into from
Nov 9, 2024
Merged
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
24 changes: 18 additions & 6 deletions pylint/checkers/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -1968,7 +1968,7 @@ def _check_consumer(

def _report_unfound_name_definition(
self,
node: nodes.NodeNG,
node: nodes.Name,
current_consumer: NamesConsumer,
) -> bool:
"""Reports used-before-assignment error when all name definition nodes
Expand All @@ -1985,7 +1985,9 @@ def _report_unfound_name_definition(
return False
if self._is_variable_annotation_in_function(node):
return False
if self._has_nonlocal_binding(node):
if self._has_nonlocal_in_enclosing_frame(
node, current_consumer.consumed_uncertain.get(node.name, [])
):
return False
if (
node.name in self._reported_type_checking_usage_scopes
Expand Down Expand Up @@ -2375,11 +2377,21 @@ def _maybe_used_and_assigned_at_once(defstmt: _base_nodes.Statement) -> bool:
def _is_builtin(self, name: str) -> bool:
return name in self.linter.config.additional_builtins or utils.is_builtin(name)

def _has_nonlocal_binding(self, node: nodes.Name) -> bool:
"""Checks if name node has a nonlocal binding in any enclosing frame."""
def _has_nonlocal_in_enclosing_frame(
self, node: nodes.Name, uncertain_definitions: list[nodes.NodeNG]
) -> bool:
"""Check if there is a nonlocal declaration in the nearest frame that encloses
both usage and definitions.
"""
defining_frames = {definition.frame() for definition in uncertain_definitions}
frame = node.frame()
while frame:
if _is_nonlocal_name(node, frame):
is_enclosing_frame = False
while frame and not is_enclosing_frame:
is_enclosing_frame = all(
(frame is defining_frame) or frame.parent_of(defining_frame)
for defining_frame in defining_frames
)
if is_enclosing_frame and _is_nonlocal_name(node, frame):
return True
frame = frame.parent.frame() if frame.parent else None
return False
Expand Down
32 changes: 31 additions & 1 deletion tests/functional/u/used/used_before_assignment_nonlocal.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ def inner():


def nonlocal_in_outer_frame_ok(callback, condition_a, condition_b):
"""Nonlocal declared in outer frame, usage and definition in different frames."""
"""Nonlocal declared in outer frame, usage and definition in different frames,
both enclosed in outer frame.
"""
def outer():
nonlocal callback
if condition_a:
Expand All @@ -133,3 +135,31 @@ def inner():
def callback():
pass
outer()


def nonlocal_in_distant_outer_frame_fail(callback, condition_a, condition_b):
"""Nonlocal declared in outer frame, both usage and definition immediately enclosed
in intermediate frame.
"""
def outer():
nonlocal callback
def intermediate():
if condition_a:
def inner():
callback() # [possibly-used-before-assignment]
inner()
else:
if condition_b:
def callback():
pass
intermediate()
outer()


def nonlocal_after_bad_usage_fail():
"""Nonlocal declared after used-before-assignment."""
num = 1
def inner():
num = num + 1 # [used-before-assignment]
nonlocal num
inner()
2 changes: 2 additions & 0 deletions tests/functional/u/used/used_before_assignment_nonlocal.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ used-before-assignment:39:18:39:28:test_fail5:Using variable 'undefined1' before
used-before-assignment:90:10:90:18:type_annotation_never_gets_value_despite_nonlocal:Using variable 'some_num' before assignment:HIGH
used-before-assignment:96:14:96:18:inner_function_lacks_access_to_outer_args.inner:Using variable 'args' before assignment:HIGH
used-before-assignment:117:18:117:21:nonlocal_in_outer_frame_fail.outer.inner:Using variable 'num' before assignment:HIGH
possibly-used-before-assignment:149:20:149:28:nonlocal_in_distant_outer_frame_fail.outer.intermediate.inner:Possibly using variable 'callback' before assignment:CONTROL_FLOW
used-before-assignment:163:14:163:17:nonlocal_after_bad_usage_fail.inner:Using variable 'num' before assignment:HIGH
Loading