Repository: PyCQA/pyflakes (python)

Issue:
Is an unused variable annotation able to be detected?
Given this sample file:

```python
def foo():
    a = 10
    b: str
    return 1


foo()
```

It correctly gives an error about the variable `a`: `test.py:2:5 local variable 'a' is assigned to but never used`

It would be nice if an error could be given about `b` as well since it is also never used. I'm not sure if this is a bug report or a feature request, but I just wasn't sure if this was possible.

Pull request description:
Detect unused annotations in functions
Closes #546

Patch:
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index 56fc3ca..89c9d0a 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -588,7 +588,7 @@ class FunctionScope(Scope):
         self.returnValue = None     # First non-empty return
         self.isGenerator = False    # Detect a generator
 
-    def unusedAssignments(self):
+    def unused_assignments(self):
         """
         Return a generator for the assignments which have not been used.
         """
@@ -600,6 +600,14 @@ class FunctionScope(Scope):
                     isinstance(binding, Assignment)):
                 yield name, binding
 
+    def unused_annotations(self):
+        """
+        Return a generator for the annotations which have not been used.
+        """
+        for name, binding in self.items():
+            if not binding.used and isinstance(binding, Annotation):
+                yield name, binding
+
 
 class GeneratorScope(Scope):
     pass
@@ -1156,6 +1164,7 @@ class Checker:
 
             binding = scope.get(name, None)
             if isinstance(binding, Annotation) and not self._in_postponed_annotation:
+                scope[name].used = True
                 continue
 
             if name == 'print' and isinstance(binding, Builtin):
@@ -2084,13 +2093,22 @@ class Checker:
 
             self.handleChildren(node, omit=['decorator_list', 'returns'])
 
-            def checkUnusedAssignments():
+            def check_unused_assignments():
                 """
                 Check to see if any assignments have not been used.
                 """
-                for name, binding in self.scope.unusedAssignments():
+                for name, binding in self.scope.unused_assignments():
                     self.report(messages.UnusedVariable, binding.source, name)
-            self.deferAssignment(checkUnusedAssignments)
+
+            def check_unused_annotations():
+                """
+                Check to see if any annotations have not been used.
+                """
+                for name, binding in self.scope.unused_annotations():
+                    self.report(messages.UnusedAnnotation, binding.source, name)
+
+            self.deferAssignment(check_unused_assignments)
+            self.deferAssignment(check_unused_annotations)
 
             self.popScope()
 
diff --git a/pyflakes/messages.py b/pyflakes/messages.py
index 37c4432..c2246cf 100644
--- a/pyflakes/messages.py
+++ b/pyflakes/messages.py
@@ -156,6 +156,18 @@ class UnusedVariable(Message):
         self.message_args = (names,)
 
 
+class UnusedAnnotation(Message):
+    """
+    Indicates that a variable has been explicitly annotated to but not actually
+    used.
+    """
+    message = 'local variable %r is annotated but never used'
+
+    def __init__(self, filename, loc, names):
+        Message.__init__(self, filename, loc)
+        self.message_args = (names,)
+
+
 class ReturnOutsideFunction(Message):
     """
     Indicates a return statement outside of a function/method.


Test patch:
diff --git a/pyflakes/test/test_type_annotations.py b/pyflakes/test/test_type_annotations.py
index d881205..2ad9f45 100644
--- a/pyflakes/test/test_type_annotations.py
+++ b/pyflakes/test/test_type_annotations.py
@@ -174,7 +174,7 @@ class TestTypeAnnotations(TestCase):
         def f():
             name: str
             age: int
-        ''')
+        ''', m.UnusedAnnotation, m.UnusedAnnotation)
         self.flakes('''
         def f():
             name: str = 'Bob'
@@ -190,7 +190,7 @@ class TestTypeAnnotations(TestCase):
         from typing import Any
         def f():
             a: Any
-        ''')
+        ''', m.UnusedAnnotation)
         self.flakes('''
         foo: not_a_real_type
         ''', m.UndefinedName)
@@ -356,11 +356,10 @@ class TestTypeAnnotations(TestCase):
         class Cls:
             y: int
         ''')
-        # TODO: this should print a UnusedVariable message
         self.flakes('''
         def f():
             x: int
-        ''')
+        ''', m.UnusedAnnotation)
         # This should only print one UnusedVariable message
         self.flakes('''
         def f():
@@ -368,6 +367,12 @@ class TestTypeAnnotations(TestCase):
             x = 3
         ''', m.UnusedVariable)
 
+    def test_unassigned_annotation_is_undefined(self):
+        self.flakes('''
+        name: str
+        print(name)
+        ''', m.UndefinedName)
+
     def test_annotated_async_def(self):
         self.flakes('''
         class c: pass