Repository: PyCQA/pyflakes
Pull request #73 (2016-06-14 14:44:33)

Ignore underscore redefinition
https://github.com/kevinw/pyflakes/issues/30


sigmavirus24: So that's because windows uses `\r\n` for newlines whereas most other operating systems just use `\n`. I'm not sure what the best way to test that is.

I'm a little uncertain we want to accept this pull request for one reason: `_` is often used for the `i18n` translation function, so people might do

```py
from .i18n import _


print(_("Some text to be translated based on locale."))
```

In that case, we probably do not want to silently ignore `_` being undefined in a function. I just don't know what case we want to prefer here. =/

@bitglue do you have any preferences?
ambv: I agree with **both** of you! IMO, let's ignore redefinitions of `_` *unless it was imported (including imported `as`).

Rationale: `_` is a common throwaway variable. Doing three consecutive for-loops in Python 2 using the `_` name should not raise warnings. However, you never import a thing under a name if you don't intend to use it somehow. In this case, if anything overwrites our imported name (including by a for-loop or other tuple-unpacking throwaway variable), it should definitely raise a warning.
jayvdb: If we are going to ignore redefinitions(assignments) of `_`, we should also check that `_` is not loaded.

The complication is that in doctest, `_` is magically assigned and is loaded in subsequent lines of code.
sigmavirus24: @jayvdb I think that's what @ambv and I are talking about, no?

> The complication is that in doctest, _ is magically assigned and is loaded in subsequent lines of code.

Could you provide a link or example? I don't understand what you're talking about here.
jayvdb: ```python
>>> 1
1
>>> _ + 1
2
```
eight04: What does the building error mean?
asmeurer: I think it's still fine to use `for _ in range(10)` or some similar in a doctest. Even though it gets assigned automatically it's still a throwaway variable of sorts. 
sigmavirus24: > What does the building error mean?

@eight04 that's not your fault. IIRC @jayvdb added the AppVeyor dependency on PyPy3, so we can let them fix it. (For posterity, it seems setuptools no longer supports Py 3.2-like runtimes.)
jayvdb: As requested, https://github.com/PyCQA/pyflakes/pull/83 fixes Appveyor builds.

Patch:
diff --git a/.appveyor.yml b/.appveyor.yml
index 149be37..41b1dbb 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -1,5 +1,7 @@
 # To activate, change the Appveyor settings to use `.appveyor.yml`.
 install:
+  - ps: "[Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12"
+
   - python -m pip install --upgrade virtualenv pip setuptools tox
 
   # Fetch the three main PyPy releases
@@ -30,13 +32,11 @@ install:
 
   # pypy3-2.4.0 and pypy-2.6.1 are manually bootstrapped and tested
   - ps: (New-Object Net.WebClient).DownloadFile('https://bootstrap.pypa.io/get-pip.py', "$env:appveyor_build_folder\get-pip.py")
-  - git clone https://github.com/pypa/setuptools/
-  - cd setuptools
-  - C:\pypy3-2.4.0-win32\pypy3 bootstrap.py
-  - C:\pypy3-2.4.0-win32\pypy3 setup.py install
-  - C:\pypy-2.6.1-win32\pypy bootstrap.py
-  - C:\pypy-2.6.1-win32\pypy setup.py install
-  - cd ..
+  # errors are ignored due to https://github.com/pypa/pip/issues/2669#issuecomment-136405390
+  # NOTE: If and when a new version of PyPy3 is released for Windows that
+  #       supports anything newer than Python 3.2, remove the setuptools pin.
+  - ps: C:\pypy3-2.4.0-win32\pypy3 "$env:appveyor_build_folder\get-pip.py"; C:\pypy3-2.4.0-win32\pypy3 -m pip install -U --force-reinstall pip==8.1.2 "setuptools<30"; echo "ignore error"
+  - ps: C:\pypy-2.6.1-win32\pypy "$env:appveyor_build_folder\get-pip.py"
 
 build: off
 
diff --git a/.travis.yml b/.travis.yml
index 0b07a39..780b117 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,11 +1,12 @@
 language: python
+cache: pip
 python:
   - 2.6
   - 2.7
-  - 3.2
   - 3.3
   - 3.4
   - 3.5
+  - 3.6
   - nightly
   - pypy
   - pypy-5.3
diff --git a/NEWS.txt b/NEWS.txt
index d54109b..3e9b8c1 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -1,3 +1,17 @@
+UNRELEASED
+  - Check for unused exception binding in `except:` block
+
+1.6.0 (2017-08-03)
+  - Process function scope variable annotations for used names
+  - Find Python files without extensions by their shebang
+
+1.5.0 (2017-01-09)
+  - Enable support for PEP 526 annotated assignments
+
+1.4.0 (2016-12-30):
+  - Change formatting of ImportStarMessage to be consistent with other errors
+  - Support PEP 498 "f-strings"
+
 1.3.0 (2016-09-01):
   - Fix PyPy2 Windows IntegrationTests
   - Check for duplicate dictionary keys
diff --git a/README.rst b/README.rst
index f0a43c8..45f0ab8 100644
--- a/README.rst
+++ b/README.rst
@@ -9,7 +9,7 @@ parsing the source file, not importing it, so it is safe to use on
 modules with side effects.  It's also much faster.
 
 It is `available on PyPI <https://pypi.python.org/pypi/pyflakes>`_
-and it supports all active versions of Python from 2.5 to 3.5.
+and it supports all active versions of Python from 2.5 to 3.6.
 
 
 
@@ -61,7 +61,7 @@ Share your feedback and ideas: `subscribe to the mailing-list
 Contributing
 ------------
 
-Issues are tracked on `Launchpad <https://bugs.launchpad.net/pyflakes>`_.
+Issues are tracked on `GitHub <https://github.com/PyCQA/pyflakes/issues>`_.
 
 Patches may be submitted via a `GitHub pull request`_ or via the mailing list
 if you prefer. If you are comfortable doing so, please `rebase your changes`_
@@ -72,7 +72,7 @@ rebase your commits for you.
 
 All changes should include tests and pass flake8_.
 
-.. image:: https://api.travis-ci.org/PyCQA/pyflakes.svg
+.. image:: https://api.travis-ci.org/PyCQA/pyflakes.svg?branch=master
    :target: https://travis-ci.org/PyCQA/pyflakes
    :alt: Build status
 
@@ -82,3 +82,8 @@ All changes should include tests and pass flake8_.
 .. _Pychecker: http://pychecker.sourceforge.net/
 .. _`rebase your changes`: https://git-scm.com/book/en/v2/Git-Branching-Rebasing
 .. _`GitHub pull request`: https://github.com/PyCQA/pyflakes/pulls
+
+Changelog
+---------
+
+Please see `NEWS.txt <NEWS.txt>`_.
diff --git a/pyflakes/__init__.py b/pyflakes/__init__.py
index 19b4f1d..bcd8d54 100644
--- a/pyflakes/__init__.py
+++ b/pyflakes/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '1.3.0'
+__version__ = '1.6.0'
diff --git a/pyflakes/api.py b/pyflakes/api.py
index a535bff..49ee38d 100644
--- a/pyflakes/api.py
+++ b/pyflakes/api.py
@@ -5,6 +5,7 @@ from __future__ import with_statement
 
 import sys
 import os
+import re
 import _ast
 
 from pyflakes import checker, __version__
@@ -13,6 +14,9 @@ from pyflakes import reporter as modReporter
 __all__ = ['check', 'checkPath', 'checkRecursive', 'iterSourceCode', 'main']
 
 
+PYTHON_SHEBANG_REGEX = re.compile(br'^#!.*\bpython[23w]?\b\s*$')
+
+
 def check(codeString, filename, reporter=None):
     """
     Check the Python source given by C{codeString} for flakes.
@@ -108,6 +112,25 @@ def checkPath(filename, reporter=None):
     return check(codestr, filename, reporter)
 
 
+def isPythonFile(filename):
+    """Return True if filename points to a Python file."""
+    if filename.endswith('.py'):
+        return True
+
+    max_bytes = 128
+
+    try:
+        with open(filename, 'rb') as f:
+            text = f.read(max_bytes)
+            if not text:
+                return False
+    except IOError:
+        return False
+
+    first_line = text.splitlines()[0]
+    return PYTHON_SHEBANG_REGEX.match(first_line)
+
+
 def iterSourceCode(paths):
     """
     Iterate over all Python source files in C{paths}.
@@ -120,8 +143,9 @@ def iterSourceCode(paths):
         if os.path.isdir(path):
             for dirpath, dirnames, filenames in os.walk(path):
                 for filename in filenames:
-                    if filename.endswith('.py'):
-                        yield os.path.join(dirpath, filename)
+                    full_path = os.path.join(dirpath, filename)
+                    if isPythonFile(full_path):
+                        yield full_path
         else:
             yield path
 
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index caa9db5..baef833 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -42,6 +42,9 @@ else:
     def getNodeType(node_class):
         return node_class.__name__.upper()
 
+    # Silence `pyflakes` from reporting `undefined name 'unicode'` in Python 3.
+    unicode = str
+
 # Python >= 3.3 uses ast.Try instead of (ast.TryExcept + ast.TryFinally)
 if PY32:
     def getAlternatives(n):
@@ -676,8 +679,9 @@ class Checker(object):
                     self.report(messages.RedefinedInListComp,
                                 node, value.name, existing.source)
                 elif not existing.used and value.redefines(existing):
-                    self.report(messages.RedefinedWhileUnused,
-                                node, value.name, existing.source)
+                    if value.name != '_' or isinstance(existing, Importation):
+                        self.report(messages.RedefinedWhileUnused,
+                                    node, value.name, existing.source)
 
             elif isinstance(existing, Importation) and value.redefines(existing):
                 existing.redefined.append(node)
@@ -870,7 +874,19 @@ class Checker(object):
 
     def handleDoctests(self, node):
         try:
-            (docstring, node_lineno) = self.getDocstring(node.body[0])
+            if hasattr(node, 'docstring'):
+                docstring = node.docstring
+
+                # This is just a reasonable guess. In Python 3.7, docstrings no
+                # longer have line numbers associated with them. This will be
+                # incorrect if there are empty lines between the beginning
+                # of the function and the docstring.
+                node_lineno = node.lineno
+                if hasattr(node, 'args'):
+                    node_lineno = max([node_lineno] +
+                                      [arg.lineno for arg in node.args.args])
+            else:
+                (docstring, node_lineno) = self.getDocstring(node.body[0])
             examples = docstring and self._getDoctestExamples(docstring)
         except (ValueError, IndexError):
             # e.g. line 6 of the docstring for <string> has inconsistent
@@ -907,6 +923,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
 
@@ -1145,9 +1194,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():
 
@@ -1307,31 +1358,60 @@ class Checker(object):
             self.handleChildren(node)
             return
 
-        # 3.x: the name of the exception, which is not a Name node, but
-        # a simple string, creates a local that is only bound within the scope
-        # of the except: block.
+        # If the name already exists in the scope, modify state of existing
+        # binding.
+        if node.name in self.scope:
+            self.handleNodeStore(node)
+
+        # 3.x: the name of the exception, which is not a Name node, but a
+        # simple string, creates a local that is only bound within the scope of
+        # the except: block. As such, temporarily remove the existing binding
+        # to more accurately determine if the name is used in the except:
+        # block.
 
         for scope in self.scopeStack[::-1]:
-            if node.name in scope:
-                is_name_previously_defined = True
+            try:
+                binding = scope.pop(node.name)
+            except KeyError:
+                pass
+            else:
+                prev_definition = scope, binding
                 break
         else:
-            is_name_previously_defined = False
+            prev_definition = None
 
         self.handleNodeStore(node)
         self.handleChildren(node)
-        if not is_name_previously_defined:
-            # See discussion on https://github.com/PyCQA/pyflakes/pull/59
 
-            # We're removing the local name since it's being unbound
-            # after leaving the except: block and it's always unbound
-            # if the except: block is never entered. This will cause an
-            # "undefined name" error raised if the checked code tries to
-            # use the name afterwards.
-            #
-            # Unless it's been removed already. Then do nothing.
+        # See discussion on https://github.com/PyCQA/pyflakes/pull/59
 
-            try:
-                del self.scope[node.name]
-            except KeyError:
-                pass
+        # We're removing the local name since it's being unbound after leaving
+        # the except: block and it's always unbound if the except: block is
+        # never entered. This will cause an "undefined name" error raised if
+        # the checked code tries to use the name afterwards.
+        #
+        # Unless it's been removed already. Then do nothing.
+
+        try:
+            binding = self.scope.pop(node.name)
+        except KeyError:
+            pass
+        else:
+            if not binding.used:
+                self.report(messages.UnusedVariable, node, node.name)
+
+        # Restore.
+        if prev_definition:
+            scope, binding = prev_definition
+            scope[node.name] = binding
+
+    def ANNASSIGN(self, node):
+        if node.value:
+            # Only bind the *targets* if the assignment has a value.
+            # Otherwise it's not really ast.Store and shouldn't silence
+            # UndefinedLocal warnings.
+            self.handleNode(node.target, 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,)
diff --git a/setup.cfg b/setup.cfg
index 5e40900..ed8a958 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,2 +1,5 @@
-[wheel]
+[bdist_wheel]
 universal = 1
+
+[metadata]
+license_file = LICENSE
diff --git a/tox.ini b/tox.ini
index 870ed43..737e334 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,7 @@
 [tox]
 skip_missing_interpreters = True
 envlist =
-    py26,py27,py32,py33,py34,py35,pypy,pypy3
+    py26,py27,py33,py34,py35,py36,pypy,pypy3
 
 [testenv]
 deps =


Test patch:
diff --git a/pyflakes/test/test_api.py b/pyflakes/test/test_api.py
index 77ee33c..8aa6df9 100644
--- a/pyflakes/test/test_api.py
+++ b/pyflakes/test/test_api.py
@@ -187,6 +187,36 @@ class TestIterSourceCode(TestCase):
             sorted(iterSourceCode([self.tempdir])),
             sorted([apath, bpath, cpath]))
 
+    def test_shebang(self):
+        """
+        Find Python files that don't end with `.py`, but contain a Python
+        shebang.
+        """
+        python = os.path.join(self.tempdir, 'a')
+        with open(python, 'w') as fd:
+            fd.write('#!/usr/bin/env python\n')
+
+        self.makeEmptyFile('b')
+
+        with open(os.path.join(self.tempdir, 'c'), 'w') as fd:
+            fd.write('hello\nworld\n')
+
+        python2 = os.path.join(self.tempdir, 'd')
+        with open(python2, 'w') as fd:
+            fd.write('#!/usr/bin/env python2\n')
+
+        python3 = os.path.join(self.tempdir, 'e')
+        with open(python3, 'w') as fd:
+            fd.write('#!/usr/bin/env python3\n')
+
+        pythonw = os.path.join(self.tempdir, 'f')
+        with open(pythonw, 'w') as fd:
+            fd.write('#!/usr/bin/env pythonw\n')
+
+        self.assertEqual(
+            sorted(iterSourceCode([self.tempdir])),
+            sorted([python, python2, python3, pythonw]))
+
     def test_multipleDirectories(self):
         """
         L{iterSourceCode} can be given multiple directories.  It will recurse
@@ -484,6 +514,7 @@ foo(bar=baz, bax)
 foo(bar=baz, bax)
 %s""" % (sourcePath, column, message, last_line)])
 
+    @skipIf(PYPY, 'Output in PyPy varies highly, dependending on version')
     def test_invalidEscape(self):
         """
         The invalid escape syntax raises ValueError in Python 2
@@ -493,13 +524,6 @@ foo(bar=baz, bax)
         sourcePath = self.makeTempFile(r"foo = '\xyz'")
         if ver < (3,):
             decoding_error = "%s: problem decoding source\n" % (sourcePath,)
-        elif PYPY:
-            # pypy3 only
-            decoding_error = """\
-%s:1:6: %s: ('unicodeescape', b'\\\\xyz', 0, 2, 'truncated \\\\xXX escape')
-foo = '\\xyz'
-     ^
-""" % (sourcePath, 'UnicodeDecodeError')
         else:
             last_line = '      ^\n' if ERROR_HAS_LAST_LINE else ''
             # Column has been "fixed" since 3.2.4 and 3.3.1
@@ -518,6 +542,9 @@ foo = '\\xyz'
         If the source file is not readable, this is reported on standard
         error.
         """
+        if os.getuid() == 0:
+            self.skipTest('root user can access all files regardless of '
+                          'permissions')
         sourcePath = self.makeTempFile('')
         os.chmod(sourcePath, 0)
         count, errors = self.getErrors(sourcePath)
diff --git a/pyflakes/test/test_imports.py b/pyflakes/test/test_imports.py
index 3e3be88..4723954 100644
--- a/pyflakes/test/test_imports.py
+++ b/pyflakes/test/test_imports.py
@@ -1,4 +1,3 @@
-
 from sys import version_info
 
 from pyflakes import messages as m
@@ -571,11 +570,15 @@ class Test(TestCase):
 
     def test_redefinedByExcept(self):
         as_exc = ', ' if version_info < (2, 6) else ' as '
+        expected = [m.RedefinedWhileUnused]
+        if version_info >= (3,):
+            # The exc variable is unused inside the exception handler.
+            expected.append(m.UnusedVariable)
         self.flakes('''
         import fu
         try: pass
         except Exception%sfu: pass
-        ''' % as_exc, m.RedefinedWhileUnused)
+        ''' % as_exc, *expected)
 
     def test_usedInRaise(self):
         self.flakes('''
diff --git a/pyflakes/test/test_other.py b/pyflakes/test/test_other.py
index 109bc66..5bae7a0 100644
--- a/pyflakes/test/test_other.py
+++ b/pyflakes/test/test_other.py
@@ -151,6 +151,25 @@ class Test(TestCase):
         def a(): pass
         ''', m.RedefinedWhileUnused)
 
+    def test_redefinedUnderscoreFunction(self):
+        """
+        Test that shadowing a function definition named with underscore doesn't
+        raise anything.
+        """
+        self.flakes('''
+        def _(): pass
+        def _(): pass
+        ''')
+
+    def test_redefinedUnderscoreImportation(self):
+        """
+        Test that shadowing an underscore importation raises a warning.
+        """
+        self.flakes('''
+        from .i18n import _
+        def _(): pass
+        ''', m.RedefinedWhileUnused)
+
     def test_redefinedClassFunction(self):
         """
         Test that shadowing a function definition in a class suite with another
@@ -1597,6 +1616,22 @@ class TestUnusedAssignment(TestCase):
             except Exception%se: e
         ''' % as_exc)
 
+    @skipIf(version_info < (3,),
+            "In Python 2 exception names stay bound after the exception handler")
+    def test_exceptionUnusedInExcept(self):
+        self.flakes('''
+        try: pass
+        except Exception as e: pass
+        ''', m.UnusedVariable)
+
+    def test_exceptionUnusedInExceptInFunction(self):
+        as_exc = ', ' if version_info < (2, 6) else ' as '
+        self.flakes('''
+        def download_review():
+            try: pass
+            except Exception%se: pass
+        ''' % as_exc, m.UnusedVariable)
+
     def test_exceptWithoutNameInFunction(self):
         """
         Don't issue false warning when an unnamed exception is used.
@@ -1808,3 +1843,143 @@ class TestAsyncStatements(TestCase):
         mom = 'mom'
         f'{hi} {mom}'
         ''')
+
+    @skipIf(version_info < (3, 6), 'new in Python 3.6')
+    def test_variable_annotations(self):
+        self.flakes('''
+        name: str
+        age: int
+        ''')
+        self.flakes('''
+        name: str = 'Bob'
+        age: int = 18
+        ''')
+        self.flakes('''
+        class C:
+            name: str
+            age: int
+        ''')
+        self.flakes('''
+        class C:
+            name: str = 'Bob'
+            age: int = 18
+        ''')
+        self.flakes('''
+        def f():
+            name: str
+            age: int
+        ''')
+        self.flakes('''
+        def f():
+            name: str = 'Bob'
+            age: int = 18
+            foo: not_a_real_type = None
+        ''', m.UnusedVariable, m.UnusedVariable, m.UnusedVariable, m.UndefinedName)
+        self.flakes('''
+        def f():
+            name: str
+            print(name)
+        ''', m.UndefinedName)
+        self.flakes('''
+        from typing import Any
+        def f():
+            a: Any
+        ''')
+        self.flakes('''
+        foo: not_a_real_type
+        ''', m.UndefinedName)
+        self.flakes('''
+        foo: not_a_real_type = None
+        ''', m.UndefinedName)
+        self.flakes('''
+        class C:
+            foo: not_a_real_type
+        ''', m.UndefinedName)
+        self.flakes('''
+        class C:
+            foo: not_a_real_type = None
+        ''', m.UndefinedName)
+        self.flakes('''
+        def f():
+            class C:
+                foo: not_a_real_type
+        ''', m.UndefinedName)
+        self.flakes('''
+        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)
diff --git a/pyflakes/test/test_undefined_names.py b/pyflakes/test/test_undefined_names.py
index 1464d8e..3d19210 100644
--- a/pyflakes/test/test_undefined_names.py
+++ b/pyflakes/test/test_undefined_names.py
@@ -25,15 +25,16 @@ class Test(TestCase):
     @skipIf(version_info < (3,),
             'in Python 2 exception names stay bound after the except: block')
     def test_undefinedExceptionName(self):
-        """Exception names can't be used after the except: block."""
+        """Exception names can't be used after the except: block.
+
+        The exc variable is unused inside the exception handler."""
         self.flakes('''
         try:
             raise ValueError('ve')
         except ValueError as exc:
             pass
         exc
-        ''',
-                    m.UndefinedName)
+        ''', m.UndefinedName, m.UnusedVariable)
 
     def test_namesDeclaredInExceptBlocks(self):
         """Locals declared in except: blocks can be used after the block.
@@ -71,7 +72,8 @@ class Test(TestCase):
         """Exception names are unbound after the `except:` block.
 
         Last line will raise UnboundLocalError on Python 3 but would print out
-        've' on Python 2."""
+        've' on Python 2. The exc variable is unused inside the exception
+        handler."""
         self.flakes('''
         try:
             raise ValueError('ve')
@@ -79,14 +81,15 @@ class Test(TestCase):
             pass
         print(exc)
         exc = 'Original value'
-        ''',
-                    m.UndefinedName)
+        ''', m.UndefinedName, m.UnusedVariable)
 
     def test_undefinedExceptionNameObscuringLocalVariableFalsePositive1(self):
         """Exception names obscure locals, can't be used after. Unless.
 
         Last line will never raise UnboundLocalError because it's only
         entered if no exception was raised."""
+        # The exc variable is unused inside the exception handler.
+        expected = [] if version_info < (3,) else [m.UnusedVariable]
         self.flakes('''
         exc = 'Original value'
         try:
@@ -95,7 +98,7 @@ class Test(TestCase):
             print('exception logged')
             raise
         exc
-        ''')
+        ''', *expected)
 
     def test_delExceptionInExcept(self):
         """The exception name can be deleted in the except: block."""
@@ -111,6 +114,8 @@ class Test(TestCase):
 
         Last line will never raise UnboundLocalError because `error` is
         only falsy if the `except:` block has not been entered."""
+        # The exc variable is unused inside the exception handler.
+        expected = [] if version_info < (3,) else [m.UnusedVariable]
         self.flakes('''
         exc = 'Original value'
         error = None
@@ -122,7 +127,7 @@ class Test(TestCase):
             print(error)
         else:
             exc
-        ''')
+        ''', *expected)
 
     @skip('error reporting disabled due to false positives below')
     def test_undefinedExceptionNameObscuringGlobalVariable(self):
@@ -168,6 +173,8 @@ class Test(TestCase):
 
         Last line will never raise NameError because it's only entered
         if no exception was raised."""
+        # The exc variable is unused inside the exception handler.
+        expected = [] if version_info < (3,) else [m.UnusedVariable]
         self.flakes('''
         exc = 'Original value'
         def func():
@@ -178,13 +185,15 @@ class Test(TestCase):
                 print('exception logged')
                 raise
             exc
-        ''')
+        ''', *expected)
 
     def test_undefinedExceptionNameObscuringGlobalVariableFalsePositive2(self):
         """Exception names obscure globals, can't be used after. Unless.
 
         Last line will never raise NameError because `error` is only
         falsy if the `except:` block has not been entered."""
+        # The exc variable is unused inside the exception handler.
+        expected = [] if version_info < (3,) else [m.UnusedVariable]
         self.flakes('''
         exc = 'Original value'
         def func():
@@ -198,7 +207,7 @@ class Test(TestCase):
                 print(error)
             else:
                 exc
-        ''')
+        ''', *expected)
 
     def test_functionsNeedGlobalScope(self):
         self.flakes('''