Skip to content

Commit

Permalink
Handle case where a class is defined in TYPE_CHECKING guard
Browse files Browse the repository at this point in the history
  • Loading branch information
zenlyj committed Oct 6, 2024
1 parent 95337f3 commit b04eb21
Show file tree
Hide file tree
Showing 5 changed files with 31 additions and 20 deletions.
42 changes: 25 additions & 17 deletions pylint/checkers/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,8 @@ def _uncertain_nodes_if_tests(
name = other_node.name
elif isinstance(other_node, (nodes.Import, nodes.ImportFrom)):
name = node.name
elif isinstance(other_node, nodes.ClassDef):
name = other_node.name
else:
continue

Expand Down Expand Up @@ -1263,7 +1265,7 @@ def __init__(self, linter: PyLinter) -> None:
tuple[nodes.ExceptHandler, nodes.AssignName]
] = []
"""This is a queue, last in first out."""
self._evaluated_type_checking_scopes: dict[
self._reported_type_checking_usage_scopes: dict[
str, list[nodes.LocalsDictNodeNG]
] = {}
self._postponed_evaluation_enabled = False
Expand Down Expand Up @@ -1767,7 +1769,7 @@ def _check_consumer(
# Mark for consumption any nodes added to consumed_uncertain by
# get_next_to_consume() because they might not have executed.
nodes_to_consume = current_consumer.consumed_uncertain[node.name]
nodes_to_consume = self._filter_type_checking_import_from_consumption(
nodes_to_consume = self._filter_type_checking_definitions_from_consumption(
node, nodes_to_consume, is_reported
)
return (
Expand Down Expand Up @@ -1943,8 +1945,10 @@ def _report_unfound_name_definition(
node: nodes.NodeNG,
current_consumer: NamesConsumer,
) -> bool:
"""Reports used-before-assignment when all name definition nodes
get filtered out by NamesConsumer.
"""Reports used-before-assignment error when all name definition nodes
are filtered out by NamesConsumer.
Returns True if an error is reported; otherwise, returns False.
"""
if (
self._postponed_evaluation_enabled
Expand All @@ -1956,8 +1960,8 @@ def _report_unfound_name_definition(
if self._is_variable_annotation_in_function(node):
return False
if (
node.name in self._evaluated_type_checking_scopes
and node.scope() in self._evaluated_type_checking_scopes[node.name]
node.name in self._reported_type_checking_usage_scopes
and node.scope() in self._reported_type_checking_usage_scopes[node.name]
):
return False

Expand All @@ -1981,32 +1985,36 @@ def _report_unfound_name_definition(

return True

def _filter_type_checking_import_from_consumption(
def _filter_type_checking_definitions_from_consumption(
self,
node: nodes.NodeNG,
nodes_to_consume: list[nodes.NodeNG],
is_reported: bool,
) -> list[nodes.NodeNG]:
"""Do not consume type-checking import node as used-before-assignment
may invoke in different scopes.
"""Filters out type-checking definition nodes (e.g. imports, class definitions)
from consumption, as they may trigger used-before-assignment in a separate
context.
If a used-before-assignment error is reported for the usage of a
type-checking definition, the method tracks the scope of that usage for
future evaluation.
"""
type_checking_import = next(
type_checking_definition = next(
(
n
for n in nodes_to_consume
if isinstance(n, (nodes.Import, nodes.ImportFrom))
if isinstance(n, (nodes.Import, nodes.ImportFrom, nodes.ClassDef))
and in_type_checking_block(n)
),
None,
)
# If used-before-assignment reported for usage of type checking import
# keep track of its scope
if type_checking_import and is_reported:
self._evaluated_type_checking_scopes.setdefault(node.name, []).append(

if type_checking_definition and is_reported:
self._reported_type_checking_usage_scopes.setdefault(node.name, []).append(
node.scope()
)
nodes_to_consume = [n for n in nodes_to_consume if n != type_checking_import]
return nodes_to_consume

return [n for n in nodes_to_consume if n != type_checking_definition]

@utils.only_required_for_messages("no-name-in-module")
def visit_import(self, node: nodes.Import) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ class X:
pass

def h():
return X() # FALSE NEGATIVE
return X() # [used-before-assignment]

def i() -> X:
return X() # FALSE NEGATIVE
return X() # [used-before-assignment]

if TYPE_CHECKING:
from mod import Y
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
used-before-assignment:10:6:10:9::Using variable 'var' before assignment:INFERENCE
used-before-assignment:20:11:20:19:f:Using variable 'datetime' before assignment:INFERENCE
used-before-assignment:23:11:23:19:g:Using variable 'datetime' before assignment:INFERENCE
used-before-assignment:30:11:30:12:h:Using variable 'X' before assignment:INFERENCE
used-before-assignment:33:11:33:12:i:Using variable 'X' before assignment:INFERENCE
2 changes: 1 addition & 1 deletion tests/functional/u/used/used_before_assignment_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def defined_in_elif_branch(self) -> calendar.Calendar: # [possibly-used-before-
def defined_in_else_branch(self) -> urlopen:
print(zoneinfo) # [used-before-assignment]
print(pprint())
print(collections())
print(collections()) # [used-before-assignment]
return urlopen

def defined_in_nested_if_else(self) -> heapq: # [possibly-used-before-assignment]
Expand Down
1 change: 1 addition & 0 deletions tests/functional/u/used/used_before_assignment_typing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ used-before-assignment:153:20:153:28:VariableAnnotationsGuardedByTypeChecking:Us
possibly-used-before-assignment:170:40:170:48:TypeCheckingMultiBranch.defined_in_elif_branch:Possibly using variable 'calendar' before assignment:INFERENCE
possibly-used-before-assignment:171:14:171:20:TypeCheckingMultiBranch.defined_in_elif_branch:Possibly using variable 'bisect' before assignment:INFERENCE
used-before-assignment:175:14:175:22:TypeCheckingMultiBranch.defined_in_else_branch:Using variable 'zoneinfo' before assignment:INFERENCE
used-before-assignment:177:14:177:25:TypeCheckingMultiBranch.defined_in_else_branch:Using variable 'collections' before assignment:INFERENCE
possibly-used-before-assignment:180:43:180:48:TypeCheckingMultiBranch.defined_in_nested_if_else:Possibly using variable 'heapq' before assignment:INFERENCE
used-before-assignment:184:39:184:44:TypeCheckingMultiBranch.defined_in_try_except:Using variable 'array' before assignment:INFERENCE
used-before-assignment:185:14:185:19:TypeCheckingMultiBranch.defined_in_try_except:Using variable 'types' before assignment:INFERENCE
Expand Down

0 comments on commit b04eb21

Please sign in to comment.