From 4a2407d7aa0834c37c9fe97e2db37fa8d01caa5b Mon Sep 17 00:00:00 2001 From: Danny Sepler Date: Fri, 16 Sep 2022 00:00:27 -0400 Subject: Detect unused annotations in functions (#668) * Detect unused annotations in functions * Rebase correctly, use snake case, simplify conditions for `unused_annotations` Co-authored-by: Danny Sepler --- pyflakes/checker.py | 26 ++++++++++++++++++++++---- pyflakes/messages.py | 12 ++++++++++++ pyflakes/test/test_type_annotations.py | 13 +++++++++---- 3 files changed, 43 insertions(+), 8 deletions(-) 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. 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 -- cgit v1.2.1