Skip to content

Commit 78666e4

Browse files
[used-before-assignment] Fix FP for inner function return type (#10275)
1 parent 512c8be commit 78666e4

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
@@ -2282,20 +2282,38 @@ def _is_variable_violation(
22822282
elif isinstance(defframe, nodes.ClassDef) and isinstance(
22832283
frame, nodes.FunctionDef
22842284
):
2285-
# Special rule for function return annotations,
2286-
# using a name defined earlier in the class containing the function.
2287-
if node is frame.returns and defframe.parent_of(frame.returns):
2288-
annotation_return = True
2289-
if frame.returns.name in defframe.locals:
2290-
definition = defframe.locals[node.name][0]
2291-
if definition.lineno is None or definition.lineno < frame.lineno:
2292-
# Detect class assignments with a name defined earlier in the
2293-
# class. In this case, no warning should be raised.
2294-
maybe_before_assign = False
2285+
# Special rules for function return annotations.
2286+
if node is frame.returns:
2287+
# Using a name defined earlier in the class containing the function.
2288+
if defframe.parent_of(frame.returns):
2289+
annotation_return = True
2290+
if frame.returns.name in defframe.locals:
2291+
definition = defframe.locals[node.name][0]
2292+
# no warning raised if a name was defined earlier in the class
2293+
maybe_before_assign = (
2294+
definition.lineno is not None
2295+
and definition.lineno >= frame.lineno
2296+
)
22952297
else:
22962298
maybe_before_assign = True
2297-
else:
2298-
maybe_before_assign = True
2299+
# Using a name defined in the module if this is a nested function.
2300+
elif (
2301+
# defframe is the class containing the function.
2302+
# It shouldn't be nested: expect its parent to be a module.
2303+
(defframe_parent := next(defframe.node_ancestors()))
2304+
and isinstance(defframe_parent, nodes.Module)
2305+
# frame is the function inside the class.
2306+
and (frame_ancestors := tuple(frame.node_ancestors()))
2307+
# Does that function have any functions as ancestors?
2308+
and any(
2309+
isinstance(ancestor, nodes.FunctionDef)
2310+
for ancestor in frame_ancestors
2311+
)
2312+
# And is its last ancestor the same module as the class's?
2313+
and frame_ancestors[-1] is defframe_parent
2314+
):
2315+
annotation_return = True
2316+
maybe_before_assign = False
22992317
if isinstance(node.parent, nodes.Arguments):
23002318
maybe_before_assign = stmt.fromlineno <= defstmt.fromlineno
23012319
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)