Skip to content

Commit 544464a

Browse files
[used-before-assignment] Fix FP for inner function return type (#10275) (#10285)
(cherry picked from commit 78666e4) Co-authored-by: Jacob Walls <[email protected]>
1 parent f28d768 commit 544464a

File tree

3 files changed

+50
-13
lines changed

3 files changed

+50
-13
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix a false positive for `used-before-assignment` when an inner function's return type
2+
annotation is a class defined at module scope.
3+
4+
Closes #9391

pylint/checkers/variables.py

+30-12
Original file line numberDiff line numberDiff line change
@@ -2297,20 +2297,38 @@ def _is_variable_violation(
22972297
elif isinstance(defframe, nodes.ClassDef) and isinstance(
22982298
frame, nodes.FunctionDef
22992299
):
2300-
# Special rule for function return annotations,
2301-
# using a name defined earlier in the class containing the function.
2302-
if node is frame.returns and defframe.parent_of(frame.returns):
2303-
annotation_return = True
2304-
if frame.returns.name in defframe.locals:
2305-
definition = defframe.locals[node.name][0]
2306-
if definition.lineno is None or definition.lineno < frame.lineno:
2307-
# Detect class assignments with a name defined earlier in the
2308-
# class. In this case, no warning should be raised.
2309-
maybe_before_assign = False
2300+
# Special rules for function return annotations.
2301+
if node is frame.returns:
2302+
# Using a name defined earlier in the class containing the function.
2303+
if defframe.parent_of(frame.returns):
2304+
annotation_return = True
2305+
if frame.returns.name in defframe.locals:
2306+
definition = defframe.locals[node.name][0]
2307+
# no warning raised if a name was defined earlier in the class
2308+
maybe_before_assign = (
2309+
definition.lineno is not None
2310+
and definition.lineno >= frame.lineno
2311+
)
23102312
else:
23112313
maybe_before_assign = True
2312-
else:
2313-
maybe_before_assign = True
2314+
# Using a name defined in the module if this is a nested function.
2315+
elif (
2316+
# defframe is the class containing the function.
2317+
# It shouldn't be nested: expect its parent to be a module.
2318+
(defframe_parent := next(defframe.node_ancestors()))
2319+
and isinstance(defframe_parent, nodes.Module)
2320+
# frame is the function inside the class.
2321+
and (frame_ancestors := tuple(frame.node_ancestors()))
2322+
# Does that function have any functions as ancestors?
2323+
and any(
2324+
isinstance(ancestor, nodes.FunctionDef)
2325+
for ancestor in frame_ancestors
2326+
)
2327+
# And is its last ancestor the same module as the class's?
2328+
and frame_ancestors[-1] is defframe_parent
2329+
):
2330+
annotation_return = True
2331+
maybe_before_assign = False
23142332
if isinstance(node.parent, nodes.Arguments):
23152333
maybe_before_assign = stmt.fromlineno <= defstmt.fromlineno
23162334
elif is_recursive_klass:

tests/functional/u/used/used_before_assignment_typing.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# pylint: disable=missing-function-docstring,ungrouped-imports,invalid-name
33

44

5-
from typing import List, Optional, TYPE_CHECKING
5+
from typing import List, NamedTuple, Optional, TYPE_CHECKING
66

77
if TYPE_CHECKING:
88
if True: # pylint: disable=using-constant-test
@@ -196,3 +196,18 @@ def defined_in_loops(self) -> json: # [used-before-assignment]
196196
def defined_in_with(self) -> base64: # [used-before-assignment]
197197
print(binascii) # [used-before-assignment]
198198
return base64
199+
200+
201+
def outer() -> None:
202+
def inner() -> MyNamedTuple:
203+
return MyNamedTuple(1)
204+
205+
print(inner())
206+
207+
208+
class MyNamedTuple(NamedTuple):
209+
"""Note: current false negative if outer() called before this declaration."""
210+
field: int
211+
212+
213+
outer()

0 commit comments

Comments
 (0)