diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index e55968cf38..7a63798d91 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -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 @@ -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 @@ -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 diff --git a/tests/functional/u/used/used_before_assignment_nonlocal.py b/tests/functional/u/used/used_before_assignment_nonlocal.py index 4dc8bbf943..a3d8ca6517 100644 --- a/tests/functional/u/used/used_before_assignment_nonlocal.py +++ b/tests/functional/u/used/used_before_assignment_nonlocal.py @@ -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: @@ -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() diff --git a/tests/functional/u/used/used_before_assignment_nonlocal.txt b/tests/functional/u/used/used_before_assignment_nonlocal.txt index 887985fda2..d48443f375 100644 --- a/tests/functional/u/used/used_before_assignment_nonlocal.txt +++ b/tests/functional/u/used/used_before_assignment_nonlocal.txt @@ -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