Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/whatsnew/fragments/10028.false_negative
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix false negative for `used-before-assignment` when a function is defined inside a `TYPE_CHECKING` guard block and used later.

Closes #10028
60 changes: 41 additions & 19 deletions pylint/checkers/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,31 @@ def _assigned_locally(name_node: nodes.Name) -> bool:
)


def _is_before(node: nodes.NodeNG, reference_node: nodes.NodeNG) -> bool:
"""Checks if node appears before reference_node."""
if node.lineno < reference_node.lineno:
return True
if (
node.lineno == reference_node.lineno
and node.col_offset < reference_node.col_offset
):
return True
return False


def _is_nonlocal_name(node: nodes.Name, frame: nodes.LocalsDictNodeNG) -> bool:
"""Checks if name node has a nonlocal declaration in the given frame."""
if not isinstance(frame, nodes.FunctionDef):
return False

return any(
isinstance(stmt, nodes.Nonlocal)
and node.name in stmt.names
and _is_before(stmt, node)
for stmt in frame.body
)


def _has_locals_call_after_node(stmt: nodes.NodeNG, scope: nodes.FunctionDef) -> bool:
skip_nodes = (
nodes.FunctionDef,
Expand Down Expand Up @@ -562,10 +587,7 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None:
found_nodes = None

# Before filtering, check that this node's name is not a nonlocal
if any(
isinstance(child, nodes.Nonlocal) and node.name in child.names
for child in node.frame().get_children()
):
if _is_nonlocal_name(node, node.frame()):
return found_nodes

# And no comprehension is under the node's frame
Expand Down Expand Up @@ -723,7 +745,7 @@ 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):
elif isinstance(other_node, (nodes.FunctionDef, nodes.ClassDef)):
name = other_node.name
else:
continue
Expand Down Expand Up @@ -1959,6 +1981,8 @@ def _report_unfound_name_definition(
return False
if self._is_variable_annotation_in_function(node):
return False
if self._has_nonlocal_binding(node):
return False
if (
node.name in self._reported_type_checking_usage_scopes
and node.scope() in self._reported_type_checking_usage_scopes[node.name]
Expand Down Expand Up @@ -2215,10 +2239,7 @@ def _is_variable_violation(
)
# check if we have a nonlocal
elif node.name in defframe.locals:
maybe_before_assign = not any(
isinstance(child, nodes.Nonlocal) and node.name in child.names
for child in defframe.get_children()
)
maybe_before_assign = not _is_nonlocal_name(node, defframe)

if (
base_scope_type == "lambda"
Expand Down Expand Up @@ -2291,19 +2312,11 @@ def _is_variable_violation(
# x = b if (b := True) else False
maybe_before_assign = False
elif (
isinstance( # pylint: disable=too-many-boolean-expressions
defnode, nodes.NamedExpr
)
isinstance(defnode, nodes.NamedExpr)
and frame is defframe
and defframe.parent_of(stmt)
and stmt is defstmt
and (
(
defnode.lineno == node.lineno
and defnode.col_offset < node.col_offset
)
or (defnode.lineno < node.lineno)
)
and _is_before(defnode, node)
):
# Relation of a name to the same name in a named expression
# Could be used before assignment if self-referencing:
Expand Down Expand Up @@ -2358,6 +2371,15 @@ 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."""
frame = node.frame()
while frame:
if _is_nonlocal_name(node, frame):
return True
frame = frame.parent.frame() if frame.parent else None
return False

@staticmethod
def _is_only_type_assignment(
node: nodes.Name,
Expand Down
27 changes: 27 additions & 0 deletions tests/functional/u/used/used_before_assignment_nonlocal.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,30 @@ def inner():
print(args)
inner()
print(args)


def nonlocal_in_outer_frame_fail():
"""Nonlocal declared in outer frame, bad usage and assignment in inner frame."""
num = 1
def outer():
nonlocal num
def inner():
print(num) # [used-before-assignment]
num = 2
inner()
outer()


def nonlocal_in_outer_frame_ok(callback, condition_a, condition_b):
"""Nonlocal declared in outer frame, usage and definition in different frames."""
def outer():
nonlocal callback
if condition_a:
def inner():
callback() # should not emit possibly-used-before-assignment
inner()
else:
if condition_b:
def callback():
pass
outer()
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ used-before-assignment:33:44:33:53:test_fail4:Using variable 'undefined' before
used-before-assignment:39:18:39:28:test_fail5:Using variable 'undefined1' before assignment:HIGH
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
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 @@ -173,7 +173,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(pprint()) # [used-before-assignment]
print(collections()) # [used-before-assignment]
return urlopen

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:176:14:176:20:TypeCheckingMultiBranch.defined_in_else_branch:Using variable 'pprint' 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
Expand Down
Loading