@@ -303,6 +303,18 @@ def _assigned_locally(name_node: nodes.Name) -> bool:
303303 )
304304
305305
306+ def _is_before (node : nodes .NodeNG , reference_node : nodes .NodeNG ) -> bool :
307+ """Returns True if node appears before reference_node, False otherwise."""
308+ if node .lineno < reference_node .lineno :
309+ return True
310+ if (
311+ node .lineno == reference_node .lineno
312+ and node .col_offset < reference_node .col_offset
313+ ):
314+ return True
315+ return False
316+
317+
306318def _has_locals_call_after_node (stmt : nodes .NodeNG , scope : nodes .FunctionDef ) -> bool :
307319 skip_nodes = (
308320 nodes .FunctionDef ,
@@ -503,6 +515,8 @@ def __init__(self, node: nodes.NodeNG, scope_type: str):
503515 self .names_under_always_false_test : set [str ] = set ()
504516 self .names_defined_under_one_branch_only : set [str ] = set ()
505517
518+ self .defined_by_nonlocal = False
519+
506520 def __repr__ (self ) -> str :
507521 _to_consumes = [f"{ k } ->{ v } " for k , v in self .to_consume .items ()]
508522 _consumed = [f"{ k } ->{ v } " for k , v in self .consumed .items ()]
@@ -531,7 +545,9 @@ def mark_as_consumed(self, name: str, consumed_nodes: list[nodes.NodeNG]) -> Non
531545 else :
532546 del self .to_consume [name ]
533547
534- def get_next_to_consume (self , node : nodes .Name ) -> list [nodes .NodeNG ] | None :
548+ def get_next_to_consume (
549+ self , node : nodes .Name , visited_nonlocal_nodes : list [list [nodes .Nonlocal ]]
550+ ) -> list [nodes .NodeNG ] | None :
535551 """Return a list of the nodes that define `node` from this scope.
536552
537553 If it is uncertain whether a node will be consumed, such as for statements in
@@ -561,13 +577,6 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None:
561577 ):
562578 found_nodes = None
563579
564- # Before filtering, check that this node's name is not a nonlocal
565- if any (
566- isinstance (child , nodes .Nonlocal ) and node .name in child .names
567- for child in node .frame ().get_children ()
568- ):
569- return found_nodes
570-
571580 # And no comprehension is under the node's frame
572581 if VariablesChecker ._comprehension_between_frame_and_node (node ):
573582 return found_nodes
@@ -581,6 +590,18 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None:
581590 or n .statement ().parent_of (node )
582591 ]
583592
593+ # Filter out definitions that are shadowed by an existing nonlocal definition
594+ if found_nodes :
595+ uncertain_nodes = self ._uncertain_nodes_nonlocal (
596+ found_nodes , node , visited_nonlocal_nodes
597+ )
598+ self .consumed_uncertain [node .name ] += uncertain_nodes
599+ uncertain_nodes_set = set (uncertain_nodes )
600+ found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set ]
601+ if not found_nodes :
602+ self .defined_by_nonlocal = True
603+ return []
604+
584605 # Filter out assignments guarded by always false conditions
585606 if found_nodes :
586607 uncertain_nodes = self ._uncertain_nodes_if_tests (found_nodes , node )
@@ -624,6 +645,42 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None:
624645
625646 return found_nodes
626647
648+ def _uncertain_nodes_nonlocal (
649+ self ,
650+ found_nodes : list [nodes .NodeNg ],
651+ node : nodes .Name ,
652+ visited_nonlocal_nodes : list [list [nodes .Nonlocal ]],
653+ ) -> list [nodes .NodeNg ]:
654+ """Return found nodes that can be excluded for checks for cases
655+ where `node` is already defined by a nonlocal statement.
656+ """
657+ uncertain_nodes = []
658+ node_frame = node .frame ()
659+
660+ for found_node in found_nodes :
661+ # Determine the frame for the current found node
662+ found_node_frame = (
663+ found_node .parent .frame ()
664+ if isinstance (found_node , nodes .FunctionDef )
665+ else found_node .frame ()
666+ )
667+
668+ # If both nodes in same frame, check for nonlocal definition in their frame
669+ # Else, check for nonlocal definitions in visited frames
670+ if any (
671+ node .name in nonlocal_node .names and _is_before (nonlocal_node , node )
672+ for scope in visited_nonlocal_nodes
673+ if (found_node_frame != node_frame )
674+ or (
675+ found_node_frame == node_frame
676+ and (scope and scope [0 ].frame () == node_frame )
677+ )
678+ for nonlocal_node in scope
679+ ):
680+ uncertain_nodes .append (found_node )
681+
682+ return uncertain_nodes
683+
627684 def _inferred_to_define_name_raise_or_return (
628685 self ,
629686 name : str ,
@@ -1268,6 +1325,7 @@ def __init__(self, linter: PyLinter) -> None:
12681325 self ._reported_type_checking_usage_scopes : dict [
12691326 str , list [nodes .LocalsDictNodeNG ]
12701327 ] = {}
1328+ self ._visited_nonlocal_nodes : list [list [nodes .Nonlocal ]] = []
12711329 self ._postponed_evaluation_enabled = False
12721330
12731331 @utils .only_required_for_messages (
@@ -1434,6 +1492,9 @@ def leave_setcomp(self, _: nodes.SetComp) -> None:
14341492 def visit_functiondef (self , node : nodes .FunctionDef ) -> None :
14351493 """Visit function: update consumption analysis variable and check locals."""
14361494 self ._to_consume .append (NamesConsumer (node , "function" ))
1495+ self ._visited_nonlocal_nodes .append (
1496+ [n for n in node .body if isinstance (n , nodes .Nonlocal )]
1497+ )
14371498 if not (
14381499 self .linter .is_message_enabled ("redefined-outer-name" )
14391500 or self .linter .is_message_enabled ("redefined-builtin" )
@@ -1483,6 +1544,7 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None:
14831544 def leave_functiondef (self , node : nodes .FunctionDef ) -> None :
14841545 """Leave function: check function's locals are consumed."""
14851546 self ._check_metaclasses (node )
1547+ self ._visited_nonlocal_nodes .pop ()
14861548
14871549 if node .type_comment_returns :
14881550 self ._store_type_annotation_node (node .type_comment_returns )
@@ -1761,7 +1823,9 @@ def _check_consumer(
17611823 self ._check_late_binding_closure (node )
17621824 return (VariableVisitConsumerAction .RETURN , None )
17631825
1764- found_nodes = current_consumer .get_next_to_consume (node )
1826+ found_nodes = current_consumer .get_next_to_consume (
1827+ node , self ._visited_nonlocal_nodes
1828+ )
17651829 if found_nodes is None :
17661830 return (VariableVisitConsumerAction .CONTINUE , None )
17671831 if not found_nodes :
@@ -1964,6 +2028,8 @@ def _report_unfound_name_definition(
19642028 and node .scope () in self ._reported_type_checking_usage_scopes [node .name ]
19652029 ):
19662030 return False
2031+ if current_consumer .defined_by_nonlocal :
2032+ return False
19672033
19682034 confidence = HIGH
19692035 if node .name in current_consumer .names_under_always_false_test :
@@ -2291,19 +2357,11 @@ def _is_variable_violation(
22912357 # x = b if (b := True) else False
22922358 maybe_before_assign = False
22932359 elif (
2294- isinstance ( # pylint: disable=too-many-boolean-expressions
2295- defnode , nodes .NamedExpr
2296- )
2360+ isinstance (defnode , nodes .NamedExpr )
22972361 and frame is defframe
22982362 and defframe .parent_of (stmt )
22992363 and stmt is defstmt
2300- and (
2301- (
2302- defnode .lineno == node .lineno
2303- and defnode .col_offset < node .col_offset
2304- )
2305- or (defnode .lineno < node .lineno )
2306- )
2364+ and _is_before (defnode , node )
23072365 ):
23082366 # Relation of a name to the same name in a named expression
23092367 # Could be used before assignment if self-referencing:
0 commit comments