diff --git a/pyflakes/checker.py b/pyflakes/checker.py index d8f093a1..a0708222 100644 --- a/pyflakes/checker.py +++ b/pyflakes/checker.py @@ -922,6 +922,39 @@ def handleDoctests(self, node): self.popScope() self.scopeStack = saved_stack + def handleAnnotation(self, annotation, node): + if isinstance(annotation, ast.Str): + # Defer handling forward annotation. + def handleForwardAnnotation(): + try: + tree = ast.parse(annotation.s) + except SyntaxError: + self.report( + messages.ForwardAnnotationSyntaxError, + node, + annotation.s, + ) + return + + body = tree.body + if len(body) != 1 or not isinstance(body[0], ast.Expr): + self.report( + messages.ForwardAnnotationSyntaxError, + node, + annotation.s, + ) + return + + parsed_annotation = tree.body[0].value + for descendant in ast.walk(parsed_annotation): + ast.copy_location(descendant, annotation) + + self.handleNode(parsed_annotation, node) + + self.deferFunction(handleForwardAnnotation) + else: + self.handleNode(annotation, node) + def ignore(self, node): pass @@ -1160,9 +1193,11 @@ def addArgs(arglist): if arg in args[:idx]: self.report(messages.DuplicateArgument, node, arg) - for child in annotations + defaults: - if child: - self.handleNode(child, node) + for annotation in annotations: + self.handleAnnotation(annotation, node) + + for default in defaults: + self.handleNode(default, node) def runFunction(): @@ -1375,7 +1410,7 @@ def ANNASSIGN(self, node): # Otherwise it's not really ast.Store and shouldn't silence # UndefinedLocal warnings. self.handleNode(node.target, node) - self.handleNode(node.annotation, node) + self.handleAnnotation(node.annotation, node) if node.value: # If the assignment has value, handle the *value* now. self.handleNode(node.value, node) diff --git a/pyflakes/messages.py b/pyflakes/messages.py index 9e9406c3..670f95f6 100644 --- a/pyflakes/messages.py +++ b/pyflakes/messages.py @@ -231,3 +231,11 @@ class AssertTuple(Message): Assertion test is a tuple, which are always True. """ message = 'assertion is always true, perhaps remove parentheses?' + + +class ForwardAnnotationSyntaxError(Message): + message = 'syntax error in forward annotation %r' + + def __init__(self, filename, loc, annotation): + Message.__init__(self, filename, loc) + self.message_args = (annotation,) diff --git a/pyflakes/test/test_other.py b/pyflakes/test/test_other.py index ba052f18..14f213c4 100644 --- a/pyflakes/test/test_other.py +++ b/pyflakes/test/test_other.py @@ -1890,3 +1890,77 @@ def f(): class C: foo: not_a_real_type = None ''', m.UndefinedName) + self.flakes(''' + from foo import Bar + bar: Bar + ''') + self.flakes(''' + from foo import Bar + bar: 'Bar' + ''') + self.flakes(''' + import foo + bar: foo.Bar + ''') + self.flakes(''' + import foo + bar: 'foo.Bar' + ''') + self.flakes(''' + from foo import Bar + def f(bar: Bar): pass + ''') + self.flakes(''' + from foo import Bar + def f(bar: 'Bar'): pass + ''') + self.flakes(''' + from foo import Bar + def f(bar) -> Bar: return bar + ''') + self.flakes(''' + from foo import Bar + def f(bar) -> 'Bar': return bar + ''') + self.flakes(''' + bar: 'Bar' + ''', m.UndefinedName) + self.flakes(''' + bar: 'foo.Bar' + ''', m.UndefinedName) + self.flakes(''' + from foo import Bar + bar: str + ''', m.UnusedImport) + self.flakes(''' + from foo import Bar + def f(bar: str): pass + ''', m.UnusedImport) + self.flakes(''' + def f(a: A) -> A: pass + class A: pass + ''', m.UndefinedName, m.UndefinedName) + self.flakes(''' + def f(a: 'A') -> 'A': return a + class A: pass + ''') + self.flakes(''' + a: A + class A: pass + ''', m.UndefinedName) + self.flakes(''' + a: 'A' + class A: pass + ''') + self.flakes(''' + a: 'A B' + ''', m.ForwardAnnotationSyntaxError) + self.flakes(''' + a: 'A; B' + ''', m.ForwardAnnotationSyntaxError) + self.flakes(''' + a: '1 + 2' + ''') + self.flakes(''' + a: 'a: "A"' + ''', m.ForwardAnnotationSyntaxError)