Skip to content

Commit 5fc37cb

Browse files
authored
Fix undefined name in annotations (#535)
* Fix undefined name in annotations Variable annotations without a value don't create a name, but they still can be used as variable annotation themselves as long as the annotation is quoted or "from __future__ import annotations" is used. The implementation introduces a new binding "Annotation" for these kinds of variable annotations. This can potentially be used for further annotation-related checks in the future. Annotation handling is extended to be able to detect both forms of postponed annotations (quoted and future import) during checks. Fixes #486 * Fix wrong tuple syntax * Refactor annotation handling * Introduce AnnotationState "enum" * Pass AnnotationState to _enter_annotation() instead of a "string" flag * Add a test for unused annotations * Add a few more tests for unused variables
1 parent cdb0606 commit 5fc37cb

File tree

2 files changed

+84
-13
lines changed

2 files changed

+84
-13
lines changed

pyflakes/checker.py

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ def getAlternatives(n):
7979
LOOP_TYPES = (ast.While, ast.For)
8080
FUNCTION_TYPES = (ast.FunctionDef,)
8181

82+
if PY36_PLUS:
83+
ANNASSIGN_TYPES = (ast.AnnAssign,)
84+
else:
85+
ANNASSIGN_TYPES = ()
8286

8387
if PY38_PLUS:
8488
def _is_singleton(node): # type: (ast.AST) -> bool
@@ -528,6 +532,16 @@ class Assignment(Binding):
528532
"""
529533

530534

535+
class Annotation(Binding):
536+
"""
537+
Represents binding a name to a type without an associated value.
538+
539+
As long as this name is not assigned a value in another binding, it is considered
540+
undefined for most purposes. One notable exception is using the name as a type
541+
annotation.
542+
"""
543+
544+
531545
class FunctionDefinition(Definition):
532546
pass
533547

@@ -732,6 +746,12 @@ def is_typing_overload(value, scope_stack):
732746
)
733747

734748

749+
class AnnotationState:
750+
NONE = 0
751+
STRING = 1
752+
BARE = 2
753+
754+
735755
def in_annotation(func):
736756
@functools.wraps(func)
737757
def in_annotation_func(self, *args, **kwargs):
@@ -740,6 +760,14 @@ def in_annotation_func(self, *args, **kwargs):
740760
return in_annotation_func
741761

742762

763+
def in_string_annotation(func):
764+
@functools.wraps(func)
765+
def in_annotation_func(self, *args, **kwargs):
766+
with self._enter_annotation(AnnotationState.STRING):
767+
return func(self, *args, **kwargs)
768+
return in_annotation_func
769+
770+
743771
def make_tokens(code):
744772
# PY3: tokenize.tokenize requires readline of bytes
745773
if not isinstance(code, bytes):
@@ -826,7 +854,7 @@ class Checker(object):
826854
nodeDepth = 0
827855
offset = None
828856
traceTree = False
829-
_in_annotation = False
857+
_in_annotation = AnnotationState.NONE
830858
_in_typing_literal = False
831859
_in_deferred = False
832860

@@ -1146,8 +1174,11 @@ def handleNodeLoad(self, node):
11461174
# iteration
11471175
continue
11481176

1149-
if (name == 'print' and
1150-
isinstance(scope.get(name, None), Builtin)):
1177+
binding = scope.get(name, None)
1178+
if isinstance(binding, Annotation) and not self._in_postponed_annotation:
1179+
continue
1180+
1181+
if name == 'print' and isinstance(binding, Builtin):
11511182
parent = self.getParent(node)
11521183
if (isinstance(parent, ast.BinOp) and
11531184
isinstance(parent.op, ast.RShift)):
@@ -1222,7 +1253,9 @@ def handleNodeStore(self, node):
12221253
break
12231254

12241255
parent_stmt = self.getParent(node)
1225-
if isinstance(parent_stmt, (FOR_TYPES, ast.comprehension)) or (
1256+
if isinstance(parent_stmt, ANNASSIGN_TYPES) and parent_stmt.value is None:
1257+
binding = Annotation(name, node)
1258+
elif isinstance(parent_stmt, (FOR_TYPES, ast.comprehension)) or (
12261259
parent_stmt != node._pyflakes_parent and
12271260
not self.isLiteralTupleUnpacking(parent_stmt)):
12281261
binding = Binding(name, node)
@@ -1265,13 +1298,20 @@ def on_conditional_branch():
12651298
self.report(messages.UndefinedName, node, name)
12661299

12671300
@contextlib.contextmanager
1268-
def _enter_annotation(self):
1269-
orig, self._in_annotation = self._in_annotation, True
1301+
def _enter_annotation(self, ann_type=AnnotationState.BARE):
1302+
orig, self._in_annotation = self._in_annotation, ann_type
12701303
try:
12711304
yield
12721305
finally:
12731306
self._in_annotation = orig
12741307

1308+
@property
1309+
def _in_postponed_annotation(self):
1310+
return (
1311+
self._in_annotation == AnnotationState.STRING or
1312+
self.annotationsFutureEnabled
1313+
)
1314+
12751315
def _handle_type_comments(self, node):
12761316
for (lineno, col_offset), comment in self._type_comments.get(node, ()):
12771317
comment = comment.split(':', 1)[1].strip()
@@ -1399,7 +1439,7 @@ def handleDoctests(self, node):
13991439
self.popScope()
14001440
self.scopeStack = saved_stack
14011441

1402-
@in_annotation
1442+
@in_string_annotation
14031443
def handleStringAnnotation(self, s, node, ref_lineno, ref_col_offset, err):
14041444
try:
14051445
tree = ast.parse(s)
@@ -1611,7 +1651,7 @@ def CALL(self, node):
16111651
len(node.args) >= 1 and
16121652
isinstance(node.args[0], ast.Str)
16131653
):
1614-
with self._enter_annotation():
1654+
with self._enter_annotation(AnnotationState.STRING):
16151655
self.handleNode(node.args[0], node)
16161656

16171657
self.handleChildren(node)
@@ -2224,11 +2264,7 @@ def EXCEPTHANDLER(self, node):
22242264
self.scope[node.name] = prev_definition
22252265

22262266
def ANNASSIGN(self, node):
2227-
if node.value:
2228-
# Only bind the *targets* if the assignment has a value.
2229-
# Otherwise it's not really ast.Store and shouldn't silence
2230-
# UndefinedLocal warnings.
2231-
self.handleNode(node.target, node)
2267+
self.handleNode(node.target, node)
22322268
self.handleAnnotation(node.annotation, node)
22332269
if node.value:
22342270
# If the assignment has value, handle the *value* now.

pyflakes/test/test_type_annotations.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,14 @@ class A: pass
263263
class A: pass
264264
''')
265265
self.flakes('''
266+
T: object
267+
def f(t: T): pass
268+
''', m.UndefinedName)
269+
self.flakes('''
270+
T: object
271+
def g(t: 'T'): pass
272+
''')
273+
self.flakes('''
266274
a: 'A B'
267275
''', m.ForwardAnnotationSyntaxError)
268276
self.flakes('''
@@ -275,6 +283,26 @@ class A: pass
275283
a: 'a: "A"'
276284
''', m.ForwardAnnotationSyntaxError)
277285

286+
@skipIf(version_info < (3, 6), 'new in Python 3.6')
287+
def test_unused_annotation(self):
288+
# Unused annotations are fine in module and class scope
289+
self.flakes('''
290+
x: int
291+
class Cls:
292+
y: int
293+
''')
294+
# TODO: this should print a UnusedVariable message
295+
self.flakes('''
296+
def f():
297+
x: int
298+
''')
299+
# This should only print one UnusedVariable message
300+
self.flakes('''
301+
def f():
302+
x: int
303+
x = 3
304+
''', m.UnusedVariable)
305+
278306
@skipIf(version_info < (3, 5), 'new in Python 3.5')
279307
def test_annotated_async_def(self):
280308
self.flakes('''
@@ -300,6 +328,13 @@ class A:
300328
class B: pass
301329
''', m.UndefinedName)
302330

331+
self.flakes('''
332+
from __future__ import annotations
333+
T: object
334+
def f(t: T): pass
335+
def g(t: 'T'): pass
336+
''')
337+
303338
def test_typeCommentsMarkImportsAsUsed(self):
304339
self.flakes("""
305340
from mod import A, B, C, D, E, F, G

0 commit comments

Comments
 (0)