Skip to content

Commit

Permalink
Detect unused annotations in functions (#668)
Browse files Browse the repository at this point in the history
* Detect unused annotations in functions

* Rebase correctly, use snake case, simplify conditions for `unused_annotations`

Co-authored-by: Danny Sepler <dsepler@flatiron.com>
  • Loading branch information
dannysepler and Danny Sepler committed Sep 16, 2022
1 parent 4dcd92e commit 4a2407d
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 8 deletions.
26 changes: 22 additions & 4 deletions pyflakes/checker.py
Expand Up @@ -588,7 +588,7 @@ def __init__(self):
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.
"""
Expand All @@ -600,6 +600,14 @@ def unusedAssignments(self):
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
Expand Down Expand Up @@ -1156,6 +1164,7 @@ def handleNodeLoad(self, node):

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):
Expand Down Expand Up @@ -2084,13 +2093,22 @@ def runFunction():

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()

Expand Down
12 changes: 12 additions & 0 deletions pyflakes/messages.py
Expand Up @@ -156,6 +156,18 @@ def __init__(self, filename, loc, names):
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.
Expand Down
13 changes: 9 additions & 4 deletions pyflakes/test/test_type_annotations.py
Expand Up @@ -174,7 +174,7 @@ class C:
def f():
name: str
age: int
''')
''', m.UnusedAnnotation, m.UnusedAnnotation)
self.flakes('''
def f():
name: str = 'Bob'
Expand All @@ -190,7 +190,7 @@ def f():
from typing import Any
def f():
a: Any
''')
''', m.UnusedAnnotation)
self.flakes('''
foo: not_a_real_type
''', m.UndefinedName)
Expand Down Expand Up @@ -356,18 +356,23 @@ def test_unused_annotation(self):
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():
x: int
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
Expand Down

0 comments on commit 4a2407d

Please sign in to comment.