Skip to content

Commit

Permalink
Fix used-before-assignment false negative for nonlocals (#10075)
Browse files Browse the repository at this point in the history
(unreleased)
  • Loading branch information
zenlyj authored Nov 9, 2024
1 parent 0687c85 commit a18a27f
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 7 deletions.
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

0 comments on commit a18a27f

Please sign in to comment.