Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# 2.17 (unreleased)

* Add support for Python 3.15 and drop 3.9 (Hugo van Kemenade, #416).
* Fix false positive for `@override` function parameters (#410).

# 2.16 (2026-03-25)

Expand Down
115 changes: 115 additions & 0 deletions tests/test_scavenging.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,3 +882,118 @@ class Color(Enum):

check(v.unused_classes, [])
check(v.unused_vars, ["BLUE"])


def test_override_typing(v):
v.scan(
"""\
from typing import override

class A():
def my_func(self, a: int, b: int, c: int):
return a + b + c

class B(A):
@override
def my_func(self, a, b, c):
return a - b
"""
)
check(v.defined_vars, ["a", "b", "c"])
check(v.used_names, ["A", "a", "b", "c", "int", "override"])
check(v.unused_vars, [])


def test_override_typing_extensions(v):
v.scan(
"""\
from typing_extensions import override

class A():
def my_func(self, a: int, b: int, c: int):
return a + b + c

class B(A):
@override
def my_func(self, a, b, c):
return a - b
"""
)
check(v.defined_vars, ["a", "b", "c"])
check(v.used_names, ["A", "a", "b", "c", "int", "override"])
check(v.unused_vars, [])


def test_override_async(v):
v.scan(
"""\
from typing import override

class A():
async def my_func(self, a: int, b: int, c: int):
return a + b + c

class B(A):
@override
async def my_func(self, a, b, c):
return a - b
"""
)
check(v.defined_vars, ["a", "b", "c"])
check(v.used_names, ["A", "a", "b", "c", "int", "override"])
check(v.unused_vars, [])


def test_override_nested_function(v):
v.scan(
"""\
from typing import override

class A():
def my_func(self, a: int, b: int, c: int):
return a + b + c

class B(A):
@override
def my_func(self, a, b, c):
def inner(x, y):
return x
return a - b
"""
)
check(v.defined_vars, ["a", "b", "c", "x", "y"])
check(v.used_names, ["A", "a", "b", "c", "int", "override", "x"])
check(v.unused_vars, ["y"])


def test_override_with_args_kwargs(v):
v.scan(
"""\
from typing import override

class A():
def my_func(self, *args, **kwargs):
return args

class B(A):
@override
def my_func(self, *args, **kwargs):
return None
"""
)
check(v.defined_vars, ["args", "kwargs"])
check(v.used_names, ["A", "args", "override"])
check(v.unused_vars, ["kwargs"])


def test_no_override_still_flags(v):
v.scan(
"""\
class A():
def my_func(self, a, b, c):
return a - b
"""
)
check(v.defined_vars, ["a", "b", "c"])
check(v.used_names, ["a", "b"])
check(v.unused_vars, ["c"])
37 changes: 36 additions & 1 deletion vulture/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ def get_list(typ):
self.code = []
self.exit_code = ExitCode.NoDeadCode
self.noqa_lines = {}
self._override_depth = 0

report = partial(
self._define,
Expand Down Expand Up @@ -470,7 +471,8 @@ def _define_variable(self, name, node, confidence=DEFAULT_CONFIDENCE):

def visit_arg(self, node):
"""Function argument"""
self._define_variable(node.arg, node, confidence=100)
if self._override_depth == 0:
self._define_variable(node.arg, node, confidence=100)

def visit_AsyncFunctionDef(self, node):
return self.visit_FunctionDef(node)
Expand Down Expand Up @@ -547,6 +549,25 @@ def _is_locals_call(node):
and not node.keywords
)

@staticmethod
def _has_override_decorator(node):
"""Check if a function has the @override decorator.

Supports:
- @override
- @typing.override
- @typing_extensions.override
"""
for decorator in node.decorator_list:
name = utils.get_decorator_name(decorator)
if name in (
"@override",
"@typing.override",
"@typing_extensions.override",
):
return True
return False

def visit_ClassDef(self, node):
for decorator in node.decorator_list:
if _match(
Expand Down Expand Up @@ -623,9 +644,23 @@ def visit_MatchClass(self, node):
self.used_names.add(kwd_attr)

def visit(self, node):
# For function definitions, save and restore the override depth.
# This ensures inner functions don't inherit the parent's override state.
saved_override_depth = None
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
saved_override_depth = self._override_depth
if self._has_override_decorator(node):
self._override_depth = 1
else:
self._override_depth = 0

# Visit children nodes first to allow recursive reachability analysis.
self.generic_visit(node)

# Restore override depth after visiting children.
if saved_override_depth is not None:
self._override_depth = saved_override_depth

self.reachability.visit(node)

method = "visit_" + node.__class__.__name__
Expand Down
Loading