Skip to content

Commit a18a27f

Browse files
authored
Fix used-before-assignment false negative for nonlocals (#10075)
(unreleased)
1 parent 0687c85 commit a18a27f

File tree

3 files changed

+51
-7
lines changed

3 files changed

+51
-7
lines changed

pylint/checkers/variables.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1968,7 +1968,7 @@ def _check_consumer(
19681968

19691969
def _report_unfound_name_definition(
19701970
self,
1971-
node: nodes.NodeNG,
1971+
node: nodes.Name,
19721972
current_consumer: NamesConsumer,
19731973
) -> bool:
19741974
"""Reports used-before-assignment error when all name definition nodes
@@ -1985,7 +1985,9 @@ def _report_unfound_name_definition(
19851985
return False
19861986
if self._is_variable_annotation_in_function(node):
19871987
return False
1988-
if self._has_nonlocal_binding(node):
1988+
if self._has_nonlocal_in_enclosing_frame(
1989+
node, current_consumer.consumed_uncertain.get(node.name, [])
1990+
):
19891991
return False
19901992
if (
19911993
node.name in self._reported_type_checking_usage_scopes
@@ -2375,11 +2377,21 @@ def _maybe_used_and_assigned_at_once(defstmt: _base_nodes.Statement) -> bool:
23752377
def _is_builtin(self, name: str) -> bool:
23762378
return name in self.linter.config.additional_builtins or utils.is_builtin(name)
23772379

2378-
def _has_nonlocal_binding(self, node: nodes.Name) -> bool:
2379-
"""Checks if name node has a nonlocal binding in any enclosing frame."""
2380+
def _has_nonlocal_in_enclosing_frame(
2381+
self, node: nodes.Name, uncertain_definitions: list[nodes.NodeNG]
2382+
) -> bool:
2383+
"""Check if there is a nonlocal declaration in the nearest frame that encloses
2384+
both usage and definitions.
2385+
"""
2386+
defining_frames = {definition.frame() for definition in uncertain_definitions}
23802387
frame = node.frame()
2381-
while frame:
2382-
if _is_nonlocal_name(node, frame):
2388+
is_enclosing_frame = False
2389+
while frame and not is_enclosing_frame:
2390+
is_enclosing_frame = all(
2391+
(frame is defining_frame) or frame.parent_of(defining_frame)
2392+
for defining_frame in defining_frames
2393+
)
2394+
if is_enclosing_frame and _is_nonlocal_name(node, frame):
23832395
return True
23842396
frame = frame.parent.frame() if frame.parent else None
23852397
return False

tests/functional/u/used/used_before_assignment_nonlocal.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ def inner():
121121

122122

123123
def nonlocal_in_outer_frame_ok(callback, condition_a, condition_b):
124-
"""Nonlocal declared in outer frame, usage and definition in different frames."""
124+
"""Nonlocal declared in outer frame, usage and definition in different frames,
125+
both enclosed in outer frame.
126+
"""
125127
def outer():
126128
nonlocal callback
127129
if condition_a:
@@ -133,3 +135,31 @@ def inner():
133135
def callback():
134136
pass
135137
outer()
138+
139+
140+
def nonlocal_in_distant_outer_frame_fail(callback, condition_a, condition_b):
141+
"""Nonlocal declared in outer frame, both usage and definition immediately enclosed
142+
in intermediate frame.
143+
"""
144+
def outer():
145+
nonlocal callback
146+
def intermediate():
147+
if condition_a:
148+
def inner():
149+
callback() # [possibly-used-before-assignment]
150+
inner()
151+
else:
152+
if condition_b:
153+
def callback():
154+
pass
155+
intermediate()
156+
outer()
157+
158+
159+
def nonlocal_after_bad_usage_fail():
160+
"""Nonlocal declared after used-before-assignment."""
161+
num = 1
162+
def inner():
163+
num = num + 1 # [used-before-assignment]
164+
nonlocal num
165+
inner()

tests/functional/u/used/used_before_assignment_nonlocal.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ used-before-assignment:39:18:39:28:test_fail5:Using variable 'undefined1' before
77
used-before-assignment:90:10:90:18:type_annotation_never_gets_value_despite_nonlocal:Using variable 'some_num' before assignment:HIGH
88
used-before-assignment:96:14:96:18:inner_function_lacks_access_to_outer_args.inner:Using variable 'args' before assignment:HIGH
99
used-before-assignment:117:18:117:21:nonlocal_in_outer_frame_fail.outer.inner:Using variable 'num' before assignment:HIGH
10+
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
11+
used-before-assignment:163:14:163:17:nonlocal_after_bad_usage_fail.inner:Using variable 'num' before assignment:HIGH

0 commit comments

Comments
 (0)