diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b38281c..9ba2687a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # 2.9 (unreleased) * Use exit code 3 when dead code is found (whosayn, #319). +* Simplify decorator names that are too hard to parse to "@" (Llandy3d and Jendrik Seipp, #284). # 2.8 (2023-08-10) diff --git a/README.md b/README.md index 4c892e72..acfff5e6 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,9 @@ starting with `foo` and the names `bar` and `baz`. Additionally, the `--ignore-decorators` option can be used to ignore functions decorated with the given decorator. This is helpful for example in Flask projects, where you can use `--ignore-decorators "@app.route"` to ignore all -functions with the `@app.route` decorator. +functions with the `@app.route` decorator. Note that Vulture simplifies +decorators it cannot parse: `@foo.bar(x, y)` becomes "@foo.bar" and +`@foo.bar(x, y).baz` becomes "@" internally. We recommend using whitelists instead of `--ignore-names` or `--ignore-decorators` whenever possible, since whitelists are diff --git a/tests/test_utils.py b/tests/test_utils.py index 44801fca..c75527a3 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,7 @@ import ast import os import pathlib +import sys import pytest @@ -119,3 +120,40 @@ class Foo: pass """ check_decorator_names(code, ["@foo", "@bar.yz"]) + + +def test_get_decorator_name_end_function_call(): + code = """\ +@foo.bar(x, y, z) +def bar(): + pass +""" + check_decorator_names(code, ["@foo.bar"]) + + +@pytest.mark.skipif( + sys.version_info < (3, 9), reason="requires Python 3.9 or higher" +) +@pytest.mark.parametrize( + "decorated", + [ + ("def foo():"), + ("async def foo():"), + ("class Foo:"), + ], +) +def test_get_decorator_name_multiple_callables(decorated): + decorated = f"{decorated}\n pass" + code = f"""\ +@foo +@bar.prop +@z.func("hi").bar().k.foo +@k("hello").doo("world").x +@k.hello("world") +@foo[2] +{decorated} +""" + check_decorator_names( + code, + ["@foo", "@bar.prop", "@", "@", "@k.hello", "@"], + ) diff --git a/vulture/utils.py b/vulture/utils.py index 2578d868..4cec77fe 100644 --- a/vulture/utils.py +++ b/vulture/utils.py @@ -64,11 +64,14 @@ def format_path(path): def get_decorator_name(decorator): if isinstance(decorator, ast.Call): decorator = decorator.func - parts = [] - while isinstance(decorator, ast.Attribute): - parts.append(decorator.attr) - decorator = decorator.value - parts.append(decorator.id) + try: + parts = [] + while isinstance(decorator, ast.Attribute): + parts.append(decorator.attr) + decorator = decorator.value + parts.append(decorator.id) + except AttributeError: + parts = [] return "@" + ".".join(reversed(parts))