Repository: PyCQA/pyflakes

Issue:
Typing.TYPE_CHECKING imports raise F401
```
$ pip install  flake8

```

```
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from my_module.test_class import MyClass

object: 'MyClass' = None
```
Flake 8 gives the following error in this scenario:

`my_module\test_class.py:4:5: F401 'my_module.test_class.MyClass' imported but unused`

My current work around is this:

```
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from my_module.test_class import MyClass  # noqa

object: 'MyClass' = None
```

Any better solutions? Couldwe somehow automatically ignore all F401 errors within the `if TYPE_CHECKING:` block?

Discussion:
taion: Hold on, this breaks on http://mypy.readthedocs.io/en/latest/kinds_of_types.html#class-name-forward-references.
taion: This is ready to go now.
taion: Sorry for the repeated updates. This is now fixed.
myint: Thanks!

I was testing this against the CPython standard library and it seemed to crash when run against one of the test files ([`test_functools.py`](https://github.com/python/cpython/blob/bdb8315c21825487b54852ff0511fb4881ea2181/Lib/test/test_functools.py)). You can reproduce it by doing the above or more specifically:

```
$ wget https://raw.githubusercontent.com/python/cpython/bdb8315c21825487b54852ff0511fb4881ea2181/Lib/test/test_functools.py

$ ./py/bin/pyflakes test_functools.py
Traceback (most recent call last):
  File "./py/bin/pyflakes", line 11, in <module>
    load_entry_point('pyflakes==1.6.0', 'console_scripts', 'pyflakes')()
  File "/Users/myint/tmp/pyflakes/py/lib/python3.6/site-packages/pyflakes/api.py", line 208, in main
    warnings = checkRecursive(args, reporter)
  File "/Users/myint/tmp/pyflakes/py/lib/python3.6/site-packages/pyflakes/api.py", line 165, in checkRecursive
    warnings += checkPath(sourcePath, reporter)
  File "/Users/myint/tmp/pyflakes/py/lib/python3.6/site-packages/pyflakes/api.py", line 112, in checkPath
    return check(codestr, filename, reporter)
  File "/Users/myint/tmp/pyflakes/py/lib/python3.6/site-packages/pyflakes/api.py", line 73, in check
    w = checker.Checker(tree, filename)
  File "/Users/myint/tmp/pyflakes/py/lib/python3.6/site-packages/pyflakes/checker.py", line 498, in __init__
    self.runDeferred(self._deferredFunctions)
  File "/Users/myint/tmp/pyflakes/py/lib/python3.6/site-packages/pyflakes/checker.py", line 535, in runDeferred
    handler()
  File "/Users/myint/tmp/pyflakes/py/lib/python3.6/site-packages/pyflakes/checker.py", line 929, in handleForwardAnnotation
    parsed = ast.parse(annotation.s).body[0].value
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/ast.py", line 35, in parse
    return compile(source, filename, mode, PyCF_ONLY_AST)
  File "<unknown>", line 1
    This is a new annotation
                ^
SyntaxError: invalid syntax
```
taion: Ah, my bad. Didn't handle annotations that weren't actually annotations. I've addressed it now and added test cases.

I've also added a new reported error here since there weren't any existing errors that covered this, and I don't think there are any reasons to want illegal annotations in normal code.

If you think this new error is reasonable, could you please advise on whether the naming is appropriate, given that this will be user-facing? The alternative would be to refer to this as a "string literal annotation" rather than a "forward annotation".
taion: Rebased and squashed.

Can you confirm that you're okay with the naming and the semantics around the new error? Something like `a: 'A B'` or `a: 'A; B'` is legal Python, but I think it's vanishing unlikely to actually be intentionally used.

Patch:
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index d8f093a..a070822 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -922,6 +922,39 @@ class Checker(object):
         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 @@ class Checker(object):
                 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 @@ class Checker(object):
             # 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 9e9406c..670f95f 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,)


Test patch:
diff --git a/pyflakes/test/test_other.py b/pyflakes/test/test_other.py
index ba052f1..14f213c 100644
--- a/pyflakes/test/test_other.py
+++ b/pyflakes/test/test_other.py
@@ -1890,3 +1890,77 @@ class TestAsyncStatements(TestCase):
             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)