Repository: pylint-dev/pylint
Issue category: Bug Report

Issue:
[uba] Fix new false negative from #10034
```
def nonlocal_in_outer_frame_ok(callback, condition_a, condition_b):
    def outer():
        nonlocal callback
        def inner():
            if condition_a:
                def inner2():
                    callback()  # possibly-used-before-assignment?
                inner2()
            else:
                if condition_b:
                    def callback():
                        pass
        inner()
    outer()
```
we should probably raise a message here?

_Originally posted by @zenlyj in https://github.com/pylint-dev/pylint/pull/10034#issuecomment-2453421927_


Hints:

Patch:
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


Test patch:
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