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

Ignore self calls #374

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# next (unreleased)

* Add an option to mark most functions only called recursively as unused (John Doknjas, #374).
* Add type hints for `get_unused_code` and the fields of the `Item` class (John Doknjas, #361).

# 2.13 (2024-10-02)
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,16 @@ 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,
and requires python >= 3.9.

#### 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)
7 changes: 7 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def test_cli_args():
min_confidence=10,
sort_by_size=True,
verbose=True,
recursion=True,
)
result = _parse_args(
[
Expand All @@ -51,6 +52,7 @@ def test_cli_args():
"--verbose",
"path1",
"path2",
"-r",
]
)
assert isinstance(result, dict)
Expand All @@ -70,6 +72,7 @@ def test_toml_config():
min_confidence=10,
sort_by_size=True,
verbose=True,
recursion=True,
)
data = get_toml_bytes(
dedent(
Expand All @@ -82,6 +85,7 @@ def test_toml_config():
min_confidence = 10
sort_by_size = true
verbose = true
recursion = true
paths = ["path1", "path2"]
"""
)
Expand Down Expand Up @@ -146,6 +150,7 @@ def test_config_merging():
min_confidence = 10
sort_by_size = false
verbose = false
recursion = false
paths = ["toml_path"]
"""
)
Expand All @@ -158,6 +163,7 @@ def test_config_merging():
"--min-confidence=20",
"--sort-by-size",
"--verbose",
"--recursion",
"cli_path",
]
result = make_config(cliargs, toml)
Expand All @@ -171,6 +177,7 @@ def test_config_merging():
min_confidence=20,
sort_by_size=True,
verbose=True,
recursion=True,
)
assert result == expected

Expand Down
207 changes: 207 additions & 0 deletions tests/test_recursion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import sys

from . import check, v, v_rec

assert v
assert v_rec

new_version = sys.version_info.minor >= 9


def test_recursion1(v, v_rec):
code = """\
class MyClass:
def __init__(self):
pass

def inst(self):
self.inst()

def inst2(self):
self.inst2()

def Rec2():
MyClass.Rec2()

@classmethod
def Rec3():
MyClass.Rec3()

@staticmethod
def Rec4():
MyClass.Rec4()

def inst2():
o = MyClass()
o.inst2()

def Rec5():
Rec5()

def Rec6():
Rec6()
"""
v_rec.scan(code)
defined_funcs = ["Rec2", "inst2", "Rec5", "Rec6"]
defined_methods = ["inst", "inst2", "Rec3", "Rec4"]
check(v_rec.defined_funcs, defined_funcs)
check(v_rec.defined_methods, defined_methods)
if new_version:
check(v_rec.unused_funcs, ["Rec2", "Rec5", "Rec6"])
check(v_rec.unused_methods, ["inst", "Rec3", "Rec4"])
else:
check(v_rec.unused_funcs, [])
check(v_rec.unused_methods, [])
v.scan(code)
check(v.defined_funcs, defined_funcs)
check(v.unused_funcs, [])
check(v.defined_methods, defined_methods)
check(v.unused_methods, [])


def test_recursion2(v, v_rec):
code = """\
def Rec():
Rec()

class MyClass:
def __init__(self):
pass

def Rec(self):
pass
"""
defined_funcs = ["Rec"]
defined_methods = ["Rec"]
v_rec.scan(code)
check(v_rec.defined_funcs, defined_funcs)
check(v_rec.defined_methods, defined_methods)
check(v_rec.unused_funcs, defined_funcs if new_version else [])
check(v_rec.unused_methods, defined_methods if new_version else [])
v.scan(code)
check(v.defined_funcs, defined_funcs)
check(v.defined_methods, defined_methods)
check(v.unused_funcs, [])
check(v.unused_methods, [])


def test_recursion3(v, v_rec):
code = """\
def rec():
if 5 > 4:
if 5 > 4:
rec()

def outer():
def inner():
# the following calls are within a function within a function, so they
# are disregarded from recursion candidacy (to keep things simple)
outer()
inner()

class MyClass:
def instMethod(self):
if 5 > 4:
if 5 > 4:
self.instMethod()
def classMethod1():
if 5 > 4:
if 5 > 4:
MyClass.classMethod1()
@classmethod
def classMethod2():
if 5 > 4:
if 5 > 4:
MyClass.classMethod2()
@staticmethod
def classMethod3():
if 5 > 4:
if 5 > 4:
MyClass.classMethod3()
"""
v_rec.scan(code)
defined_funcs = ["rec", "outer", "inner", "classMethod1"]
defined_methods = ["instMethod", "classMethod2", "classMethod3"]
check(v_rec.defined_funcs, defined_funcs)
check(v_rec.defined_methods, defined_methods)
if new_version:
check(v_rec.unused_funcs, ["rec", "classMethod1"])
check(v_rec.unused_methods, defined_methods)
else:
check(v_rec.unused_funcs, [])
check(v_rec.unused_methods, [])
v.scan(code)
check(v.defined_funcs, defined_funcs)
check(v.defined_methods, defined_methods)
check(v.unused_funcs, [])
check(v.unused_methods, [])


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


def test_recursion5(v, v_rec):
code = """\
def rec(num: int):
for i in (1, num):
rec(i)
rec(2)

class myClass:
def instMethod(self, num2):
for i2 in (1, num2):
self.instMethod(i2)
myClass.classMethod1(1)
myClass.classMethod2(1)
myClass.classMethod3(1)
def classMethod1(num3):
for i3 in (1, num3):
myClass.classMethod1(i3)
@classmethod
def classMethod2(num4):
for i4 in (1, num4):
myClass.classMethod2(i4)
@staticmethod
def classMethod3(num5):
for i5 in (1, num5):
myClass.classMethod3(i5)
o = MyClass()
o.instMethod()
"""
defined_funcs = ["rec", "classMethod1"]
defined_methods = ["instMethod", "classMethod2", "classMethod3"]
defined_vars = [
"num",
"i",
"num2",
"i2",
"num3",
"i3",
"num4",
"i4",
"num5",
"i5",
"o",
]
v_rec.scan(code)
v.scan(code)
for v_curr in (v_rec, v):
check(v_curr.defined_funcs, defined_funcs)
check(v_curr.unused_funcs, [])
check(v_curr.defined_methods, defined_methods)
check(v_curr.unused_methods, [])
check(v_curr.defined_vars, defined_vars)
check(v_curr.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: 17 additions & 3 deletions vulture/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,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 and sys.version_info.minor >= 9

def get_list(typ):
return utils.LoggingList(typ, self.verbose)
Expand Down Expand Up @@ -252,7 +257,9 @@ def handle_syntax_error(e):
)
self.exit_code = ExitCode.InvalidInput
else:
# When parsing type comments, visiting can throw SyntaxError.
if self.recursion:
utils.add_parents(node)
# When parsing type comments, visiting can throw a SyntaxError:
try:
self.visit(node)
except SyntaxError as err:
Expand Down Expand Up @@ -513,7 +520,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 @@ -552,6 +561,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 @@ -647,6 +659,7 @@ def visit_Name(self, node):
if (
isinstance(node.ctx, (ast.Load, ast.Del))
and node.id not in IGNORED_VARIABLE_NAMES
and not getattr(node, "recursive", None)
):
self.used_names.add(node.id)
elif isinstance(node.ctx, (ast.Param, ast.Store)):
Expand Down Expand Up @@ -737,6 +750,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
Loading