Skip to content

Commit ac4f03a

Browse files
committed
Enhance nonlocal check to handle nesting
1 parent 5341893 commit ac4f03a

File tree

1 file changed

+77
-19
lines changed

1 file changed

+77
-19
lines changed

pylint/checkers/variables.py

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
306318
def _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

Comments
 (0)