Skip to content

Commit 86d91b6

Browse files
committed
Handle functions defined under type-checking
1 parent 5feface commit 86d91b6

File tree

3 files changed

+45
-21
lines changed

3 files changed

+45
-21
lines changed

pylint/checkers/variables.py

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,20 @@ def _assigned_locally(name_node: nodes.Name) -> bool:
303303
)
304304

305305

306+
def _is_before(node: nodes.NodeNG, reference_node: nodes.NodeNG) -> bool:
307+
"""
308+
Returns True if node appears before reference_node, False otherwise.
309+
"""
310+
if node.lineno < reference_node.lineno:
311+
return True
312+
if (
313+
node.lineno == reference_node.lineno
314+
and node.col_offset < reference_node.col_offset
315+
):
316+
return True
317+
return False
318+
319+
306320
def _has_locals_call_after_node(stmt: nodes.NodeNG, scope: nodes.FunctionDef) -> bool:
307321
skip_nodes = (
308322
nodes.FunctionDef,
@@ -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, is_nonlocal: bool
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
@@ -542,6 +558,12 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None:
542558
parent_node = node.parent
543559
found_nodes = self.to_consume.get(name)
544560
node_statement = node.statement()
561+
562+
# Filter out all nodes that define `node`, as it is a nonlocal
563+
if is_nonlocal:
564+
self.consumed_uncertain[node.name] += found_nodes if found_nodes else []
565+
return []
566+
545567
if (
546568
found_nodes
547569
and isinstance(parent_node, nodes.Assign)
@@ -561,13 +583,6 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None:
561583
):
562584
found_nodes = None
563585

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-
571586
# And no comprehension is under the node's frame
572587
if VariablesChecker._comprehension_between_frame_and_node(node):
573588
return found_nodes
@@ -723,7 +738,7 @@ def _uncertain_nodes_if_tests(
723738
name = other_node.name
724739
elif isinstance(other_node, (nodes.Import, nodes.ImportFrom)):
725740
name = node.name
726-
elif isinstance(other_node, nodes.ClassDef):
741+
elif isinstance(other_node, (nodes.FunctionDef, nodes.ClassDef)):
727742
name = other_node.name
728743
else:
729744
continue
@@ -1268,6 +1283,7 @@ def __init__(self, linter: PyLinter) -> None:
12681283
self._reported_type_checking_usage_scopes: dict[
12691284
str, list[nodes.LocalsDictNodeNG]
12701285
] = {}
1286+
self._nonlocal_nodes_stack: list[list[nodes.Nonlocal]] = []
12711287
self._postponed_evaluation_enabled = False
12721288

12731289
@utils.only_required_for_messages(
@@ -1434,6 +1450,9 @@ def leave_setcomp(self, _: nodes.SetComp) -> None:
14341450
def visit_functiondef(self, node: nodes.FunctionDef) -> None:
14351451
"""Visit function: update consumption analysis variable and check locals."""
14361452
self._to_consume.append(NamesConsumer(node, "function"))
1453+
self._nonlocal_nodes_stack.append(
1454+
[n for n in node.body if isinstance(n, nodes.Nonlocal)]
1455+
)
14371456
if not (
14381457
self.linter.is_message_enabled("redefined-outer-name")
14391458
or self.linter.is_message_enabled("redefined-builtin")
@@ -1483,6 +1502,7 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None:
14831502
def leave_functiondef(self, node: nodes.FunctionDef) -> None:
14841503
"""Leave function: check function's locals are consumed."""
14851504
self._check_metaclasses(node)
1505+
self._nonlocal_nodes_stack.pop()
14861506

14871507
if node.type_comment_returns:
14881508
self._store_type_annotation_node(node.type_comment_returns)
@@ -1761,7 +1781,9 @@ def _check_consumer(
17611781
self._check_late_binding_closure(node)
17621782
return (VariableVisitConsumerAction.RETURN, None)
17631783

1764-
found_nodes = current_consumer.get_next_to_consume(node)
1784+
found_nodes = current_consumer.get_next_to_consume(
1785+
node, self._is_nonlocal(node)
1786+
)
17651787
if found_nodes is None:
17661788
return (VariableVisitConsumerAction.CONTINUE, None)
17671789
if not found_nodes:
@@ -1940,6 +1962,13 @@ def _check_consumer(
19401962

19411963
return (VariableVisitConsumerAction.RETURN, found_nodes)
19421964

1965+
def _is_nonlocal(self, node: nodes.Name) -> bool:
1966+
return any(
1967+
node.name in nonlocal_node.names and _is_before(nonlocal_node, node)
1968+
for nonlocal_scope in self._nonlocal_nodes_stack
1969+
for nonlocal_node in nonlocal_scope
1970+
)
1971+
19431972
def _report_unfound_name_definition(
19441973
self,
19451974
node: nodes.NodeNG,
@@ -1964,6 +1993,8 @@ def _report_unfound_name_definition(
19641993
and node.scope() in self._reported_type_checking_usage_scopes[node.name]
19651994
):
19661995
return False
1996+
if self._is_nonlocal(node):
1997+
return False
19671998

19681999
confidence = HIGH
19692000
if node.name in current_consumer.names_under_always_false_test:
@@ -2291,19 +2322,11 @@ def _is_variable_violation(
22912322
# x = b if (b := True) else False
22922323
maybe_before_assign = False
22932324
elif (
2294-
isinstance( # pylint: disable=too-many-boolean-expressions
2295-
defnode, nodes.NamedExpr
2296-
)
2325+
isinstance(defnode, nodes.NamedExpr)
22972326
and frame is defframe
22982327
and defframe.parent_of(stmt)
22992328
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-
)
2329+
and _is_before(defnode, node)
23072330
):
23082331
# Relation of a name to the same name in a named expression
23092332
# Could be used before assignment if self-referencing:

tests/functional/u/used/used_before_assignment_typing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def defined_in_elif_branch(self) -> calendar.Calendar: # [possibly-used-before-
173173

174174
def defined_in_else_branch(self) -> urlopen:
175175
print(zoneinfo) # [used-before-assignment]
176-
print(pprint())
176+
print(pprint()) # [used-before-assignment]
177177
print(collections()) # [used-before-assignment]
178178
return urlopen
179179

tests/functional/u/used/used_before_assignment_typing.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ used-before-assignment:153:20:153:28:VariableAnnotationsGuardedByTypeChecking:Us
66
possibly-used-before-assignment:170:40:170:48:TypeCheckingMultiBranch.defined_in_elif_branch:Possibly using variable 'calendar' before assignment:INFERENCE
77
possibly-used-before-assignment:171:14:171:20:TypeCheckingMultiBranch.defined_in_elif_branch:Possibly using variable 'bisect' before assignment:INFERENCE
88
used-before-assignment:175:14:175:22:TypeCheckingMultiBranch.defined_in_else_branch:Using variable 'zoneinfo' before assignment:INFERENCE
9+
used-before-assignment:176:14:176:20:TypeCheckingMultiBranch.defined_in_else_branch:Using variable 'pprint' before assignment:INFERENCE
910
used-before-assignment:177:14:177:25:TypeCheckingMultiBranch.defined_in_else_branch:Using variable 'collections' before assignment:INFERENCE
1011
possibly-used-before-assignment:180:43:180:48:TypeCheckingMultiBranch.defined_in_nested_if_else:Possibly using variable 'heapq' before assignment:INFERENCE
1112
used-before-assignment:184:39:184:44:TypeCheckingMultiBranch.defined_in_try_except:Using variable 'array' before assignment:INFERENCE

0 commit comments

Comments
 (0)