Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect unused functions and classes in closures #485

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions pyflakes/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,16 +568,15 @@ def __init__(self):
self.returnValue = None # First non-empty return
self.isGenerator = False # Detect a generator

def unusedAssignments(self):
def unusedBindings(self):
"""
Return a generator for the assignments which have not been used.
"""
for name, binding in self.items():
if (not binding.used and
name != '_' and # see issue #202
name not in self.globals and
not self.usesLocals and
isinstance(binding, Assignment)):
not self.usesLocals):
yield name, binding


Expand Down Expand Up @@ -1839,13 +1838,24 @@ def runFunction():

self.handleChildren(node, omit=['decorator_list', 'returns'])

def checkUnusedAssignments():
def checkUnusedBindings():
"""
Check to see if any assignments have not been used.
"""
for name, binding in self.scope.unusedAssignments():
self.report(messages.UnusedVariable, binding.source, name)
self.deferAssignment(checkUnusedAssignments)
for name, binding in self.scope.unusedBindings():
if isinstance(binding, Assignment):
self.report(messages.UnusedVariable, binding.source, name)
elif (
isinstance(binding, ClassDefinition)
and not binding.source.decorator_list
):
self.report(messages.UnusedClass, binding.source, name)
elif (
isinstance(binding, FunctionDefinition)
and not binding.source.decorator_list
):
self.report(messages.UnusedFunction, binding.source, name)
self.deferAssignment(checkUnusedBindings)

if PY2:
def checkReturnWithArgumentInsideGenerator():
Expand Down
22 changes: 22 additions & 0 deletions pyflakes/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,28 @@ def __init__(self, filename, loc, names):
self.message_args = (names,)


class UnusedFunction(Message):
"""
Indicates that a function has been defined but not actually used.
"""
message = 'local function %r is defined but never used'

def __init__(self, filename, loc, names):
Message.__init__(self, filename, loc)
self.message_args = (names,)


class UnusedClass(Message):
"""
Indicates that a class has been defined but not actually used.
"""
message = 'local class %r is defined but never used'

def __init__(self, filename, loc, names):
Message.__init__(self, filename, loc)
self.message_args = (names,)


class ReturnWithArgsInsideGenerator(Message):
"""
Indicates a return statement with arguments inside a generator.
Expand Down
6 changes: 6 additions & 0 deletions pyflakes/test/test_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ def bar():
def baz():
def fu():
pass
return fu
return baz
''', m.RedefinedWhileUnused, m.UnusedImport)

def test_redefinedInNestedFunctionTwice(self):
Expand All @@ -316,6 +318,8 @@ def bar():
def baz():
def fu():
pass
return fu
return baz
''',
m.RedefinedWhileUnused, m.RedefinedWhileUnused,
m.UnusedImport, m.UnusedImport)
Expand Down Expand Up @@ -729,6 +733,7 @@ def a():
def b():
fu
import fu
return b
''')

def test_nestedClassAndFunctionScope(self):
Expand All @@ -738,6 +743,7 @@ def a():
class b:
def c(self):
print(fu)
return b
''')

def test_importStar(self):
Expand Down
83 changes: 83 additions & 0 deletions pyflakes/test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,7 @@ def barMaker():
def bar():
def baz():
return foo
return baz
return bar
''')

Expand Down Expand Up @@ -1766,6 +1767,88 @@ def test_assign_expr(self):
''')


class TestUnusedFunction(TestCase):
"""
Tests for warning about unused functions.
"""

def test_unusedFunction(self):
"""
Warn when a function inside a function is defined but never used.
"""
self.flakes('''
def a():
def b():
pass
''', m.UnusedFunction)

def test_unusedUnderscoreFunction(self):
"""
Don't warn when the magic "_" (underscore) name is unused.
See issue #202.
"""
self.flakes('''
def a():
def _():
pass
''')

def test_usedDecoratedFunction(self):
"""
Don't warn when the function is decorated because decorators can do
anything, like copy it into global state.
"""
self.flakes('''
from somewhere import decorator

def a():
@decorator
def b():
pass
''')


class TestUnusedClass(TestCase):
"""
Tests for warning about unused classes.
"""

def test_unusedClass(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test could be moved to a new PR and UnusedClass introduced in a non-controversial manner.

"""
Warn when a class inside a function is defined but never used.
"""
self.flakes('''
def a():
class B:
pass
''', m.UnusedClass)

def test_unusedUnderscoreClass(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test can be moved to a new PR and will pass as-is.

"""
Don't warn when the magic "_" (underscore) name is unused.
See issue #202.
"""
self.flakes('''
def a():
class _:
pass
''')

def test_usedDecoratedClass(self):
"""
Don't warn when the class is decorated because decorators can do
anything, like copy it into global state.
"""
self.flakes('''
from somewhere import decorator

def a():
@decorator
class B:
pass
''')


class TestStringFormatting(TestCase):

@skipIf(version_info < (3, 6), 'new in Python 3.6')
Expand Down
2 changes: 2 additions & 0 deletions pyflakes/test/test_type_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,13 @@ class C:
def f():
class C:
foo: not_a_real_type
return C
''', m.UndefinedName)
self.flakes('''
def f():
class C:
foo: not_a_real_type = None
return C
''', m.UndefinedName)
self.flakes('''
from foo import Bar
Expand Down
13 changes: 8 additions & 5 deletions pyflakes/test/test_undefined_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ def test_globalFromNestedScope(self):
def b():
def c():
a
return c
''')

def test_laterRedefinedGlobalFromNestedScope(self):
Expand Down Expand Up @@ -482,6 +483,7 @@ def fun2():
a
a = 2
return a
return fun2
''', m.UndefinedLocal)

def test_intermediateClassScopeIgnored(self):
Expand All @@ -499,7 +501,7 @@ def h(self):
a = x
x = None
print(x, a)
print(x)
print(x, g)
''', m.UndefinedLocal)

def test_doubleNestingReportsClosestName(self):
Expand All @@ -518,8 +520,8 @@ def c():
x
x = 3
return x
return x
return x
return x, c
return x, b
''', m.UndefinedLocal).messages[0]

# _DoctestMixin.flakes adds two lines preceding the code above.
Expand All @@ -539,7 +541,7 @@ def fun2():
a
a = 1
return a
return a
return a, fun2
''', m.UndefinedLocal)

def test_undefinedAugmentedAssignment(self):
Expand Down Expand Up @@ -580,7 +582,7 @@ def f():
class C:
bar = foo
foo = 456
return foo
return foo, C
f()
''', m.UndefinedName)

Expand Down Expand Up @@ -678,6 +680,7 @@ def func(a: note1, *args: note2,
def func():
d = e = 42
def func(a: {1, d}) -> (lambda c: e): pass
return func
''')

@skipIf(version_info < (3,), 'new in Python 3')
Expand Down