Skip to content

Commit

Permalink
Get the recursion feature to work for class/instance methods as well.…
Browse files Browse the repository at this point in the history
… Also, disable the feature by default, and add a `--recursion` flag for if the user wants to do it.
  • Loading branch information
johndoknjas committed Oct 23, 2024
1 parent 2d063dc commit d202b0a
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 63 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# next (unreleased)

* Add an option to mark most functions only called recursively as unused (John Doknjas, #374).

# 2.13 (2024-10-02)

* Add support for Python 3.13 (Jendrik Seipp, #369).
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@ function arguments, e.g., `def foo(x, _y)`.

Raise the minimum [confidence value](#types-of-unused-code) with the `--min-confidence` flag.

#### Verbose output

For more verbose output, use the `--verbose` (or `-v`) flag.

#### Not counting recursion

It's possible that a function is only called by itself. The `--recursion` (or `-r`) flag will get
vulture to mark most such functions as unused. Note that this will have some performance cost.

#### Unreachable code

If Vulture complains about code like `if False:`, you can use a Boolean
Expand Down
5 changes: 5 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,8 @@ def check_unreachable(v, lineno, size, name):
@pytest.fixture
def v():
return core.Vulture(verbose=True)


@pytest.fixture
def v_rec():
return core.Vulture(verbose=True, recursion=True)
167 changes: 125 additions & 42 deletions tests/test_recursion.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,104 @@
from . import check, v
from . import check, v, v_rec

assert v # Silence pyflakes.
assert v
assert v_rec


def test_recursion1(v):
v.scan(
"""\
def test_recursion1(v, v_rec):
code = """\
def Rec():
Rec()
"""
)
v_rec.scan(code)
check(v_rec.defined_funcs, ["Rec"])
check(v_rec.unused_funcs, ["Rec"])
v.scan(code)
check(v.defined_funcs, ["Rec"])
check(v.unused_funcs, ["Rec"])

check(v.unused_funcs, [])

def test_recursion2(v):
v.scan(
"""\
def Rec():
Rec()

def test_recursion2(v, v_rec):
code = """\
class MyClass:
def __init__(self):
pass
def Rec():
Rec() # calls global Rec()
def inst(self):
self.inst()
def inst2(self):
self.inst2()
def Rec2():
MyClass.Rec2()
def main():
main()
@classmethod
def Rec3():
MyClass.Rec3()
@staticmethod
def Rec4():
MyClass.Rec4()
def inst2():
o = MyClass()
o.inst2()
def Rec3():
Rec3()
def Rec4():
Rec4()
"""
)
check(v.defined_funcs, ["Rec", "Rec", "main"])
check(v.unused_funcs, ["main"])
v_rec.scan(code)
check(v_rec.defined_funcs, ["Rec2", "inst2", "Rec3", "Rec4"])
check(v_rec.unused_funcs, ["Rec2", "Rec3", "Rec4"])
check(v_rec.defined_methods, ["inst", "inst2", "Rec3", "Rec4"])
check(v_rec.unused_methods, ["inst", "Rec3", "Rec4"])
v.scan(code)
check(v.defined_funcs, ["Rec2", "inst2", "Rec3", "Rec4"])
check(v.unused_funcs, [])
check(v.defined_methods, ["inst", "inst2", "Rec3", "Rec4"])
check(v.unused_methods, [])


def test_recursion3(v):
v.scan(
"""\
def test_recursion3(v, v_rec):
code = """\
class MyClass:
def __init__(self):
pass
@classmethod
def Rec():
pass
MyClass.Rec()
def Rec():
MyClass.Rec()
def aa():
aa()
"""
v_rec.scan(code)
check(
v_rec.defined_funcs,
[
"aa",
],
)
check(v.defined_funcs, ["Rec", "Rec"])
check(v_rec.defined_methods, ["Rec"])
check(v_rec.unused_funcs, ["aa"])
check(v_rec.unused_methods, ["Rec"])
v.scan(code)
check(
v.defined_funcs,
[
"aa",
],
)
check(v.defined_methods, ["Rec"])
check(v.unused_funcs, [])
# MyClass.Rec() is not treated as a recursive call. So, MyClass.Rec is marked as used, causing Rec to also
# be marked as used (in Vulture's current behaviour) since they share the same name.
check(v.unused_methods, [])


def test_recursion4(v):
v.scan(
"""\
def test_recursion4(v, v_rec):
code = """\
def Rec():
Rec()
Expand All @@ -68,23 +109,65 @@ def __init__(self):
def Rec():
pass
"""
)
v_rec.scan(code)
check(v_rec.defined_funcs, ["Rec", "Rec"])
check(v_rec.unused_funcs, ["Rec", "Rec"])
v.scan(code)
check(v.defined_funcs, ["Rec", "Rec"])
check(v.unused_funcs, ["Rec", "Rec"])
check(v.unused_funcs, [])


def test_recursion5(v):
v.scan(
"""\
def test_recursion5(v, v_rec):
code = """\
def rec():
if (5 > 4):
rec()
if 5 > 4:
if 5 > 4:
rec()
def outer():
def inner():
outer() # these calls aren't considered for recursion
# the following calls are within a function within a function, so they
# are disregarded from recursion candidacy (to keep things simple)
outer()
inner()
"""
)
v_rec.scan(code)
check(v_rec.defined_funcs, ["rec", "outer", "inner"])
check(v_rec.unused_funcs, ["rec"])
v.scan(code)
check(v.defined_funcs, ["rec", "outer", "inner"])
check(v.unused_funcs, ["rec"])
check(v.unused_funcs, [])


def test_recursion6(v, v_rec):
code = """\
def rec(num: int):
if num > 4:
x = 1 + (rec ((num + num) / 3) / 2)
return x
"""
v_rec.scan(code)
check(v_rec.defined_funcs, ["rec"])
check(v_rec.unused_funcs, ["rec"])
v.scan(code)
check(v.defined_funcs, ["rec"])
check(v.unused_funcs, [])


def test_recursion7(v, v_rec):
code = """\
def rec(num: int):
for i in (1, num):
rec(i)
rec(2)
"""
v_rec.scan(code)
check(v_rec.defined_funcs, ["rec"])
check(v_rec.unused_funcs, [])
check(v_rec.defined_vars, ["num", "i"])
check(v_rec.unused_vars, [])
v.scan(code)
check(v.defined_funcs, ["rec"])
check(v.unused_funcs, [])
check(v.defined_vars, ["num", "i"])
check(v.unused_vars, [])
4 changes: 4 additions & 0 deletions vulture/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"make_whitelist": False,
"sort_by_size": False,
"verbose": False,
"recursion": False,
}


Expand Down Expand Up @@ -169,6 +170,9 @@ def csv(exclude):
"-v", "--verbose", action="store_true", default=missing
)
parser.add_argument("--version", action="version", version=version)
parser.add_argument(
"-r", "--recursion", action="store_true", default=missing
)
namespace = parser.parse_args(args)
cli_args = {
key: value
Expand Down
20 changes: 16 additions & 4 deletions vulture/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,14 @@ class Vulture(ast.NodeVisitor):
"""Find dead code."""

def __init__(
self, verbose=False, ignore_names=None, ignore_decorators=None
self,
verbose=False,
ignore_names=None,
ignore_decorators=None,
recursion=False,
):
self.verbose = verbose
self.recursion = recursion

def get_list(typ):
return utils.LoggingList(typ, self.verbose)
Expand Down Expand Up @@ -250,7 +255,8 @@ def handle_syntax_error(e):
)
self.exit_code = ExitCode.InvalidInput
else:
utils.add_parent_info(node)
if self.recursion:
utils.add_parents(node)
# When parsing type comments, visiting can throw a SyntaxError:
try:
self.visit(node)
Expand Down Expand Up @@ -509,7 +515,9 @@ def visit_AsyncFunctionDef(self, node):
def visit_Attribute(self, node):
if isinstance(node.ctx, ast.Store):
self._define(self.defined_attrs, node.attr, node)
elif isinstance(node.ctx, ast.Load):
elif isinstance(node.ctx, ast.Load) and not getattr(
node, "recursive", None
):
self.used_names.add(node.attr)

def visit_BinOp(self, node):
Expand Down Expand Up @@ -548,6 +556,9 @@ def visit_Call(self, node):
):
self._handle_new_format_string(node.func.value.value)

if self.recursion and isinstance(node.func, (ast.Name, ast.Attribute)):
node.func.recursive = utils.recursive_call(node.func)

def _handle_new_format_string(self, s):
def is_identifier(name):
return bool(re.match(r"[a-zA-Z_][a-zA-Z0-9_]*", name))
Expand Down Expand Up @@ -643,7 +654,7 @@ def visit_Name(self, node):
if (
isinstance(node.ctx, (ast.Load, ast.Del))
and node.id not in IGNORED_VARIABLE_NAMES
and not utils.top_lvl_recursive_call(node)
and not getattr(node, "recursive", None)
):
self.used_names.add(node.id)
elif isinstance(node.ctx, (ast.Param, ast.Store)):
Expand Down Expand Up @@ -734,6 +745,7 @@ def main():
verbose=config["verbose"],
ignore_names=config["ignore_names"],
ignore_decorators=config["ignore_decorators"],
recursion=config["recursion"],
)
vulture.scavenge(config["paths"], exclude=config["exclude"])
sys.exit(
Expand Down
Loading

0 comments on commit d202b0a

Please sign in to comment.