Files
openswarm/linter/checks/vulture.py
T

96 lines
3.2 KiB
Python

"""Vulture dead-code detection runner.
Class-body findings (fields, methods inside a class) are filtered out here
and handled separately by checks/classes.py which understands Pydantic.
"""
from __future__ import annotations
import ast
import re
import shutil
import subprocess
from functools import lru_cache
from pathlib import Path
from . import is_excepted, is_lintignored
CONFIG_DIR = Path(__file__).resolve().parent.parent / "config"
@lru_cache(maxsize=64)
def _class_line_ranges(filepath: str) -> list[tuple[int, int]]:
"""Return (start, end) line ranges for all class bodies in *filepath*."""
try:
tree = ast.parse(Path(filepath).read_text())
except (OSError, SyntaxError):
return []
ranges: list[tuple[int, int]] = []
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
end = max(getattr(n, "lineno", node.lineno) for n in ast.walk(node))
ranges.append((node.lineno, end))
return ranges
def _is_inside_class(filepath: str, lineno: int) -> bool:
"""True when *lineno* is strictly inside a class body.
The class declaration line itself (``class Foo:``) is *not* considered
inside, so vulture's "unused class" findings still pass through.
"""
return any(start < lineno <= end for start, end in _class_line_ranges(filepath))
def run_vulture(
root: Path, min_confidence: int, error_threshold: int,
exceptions: dict[str, list[str]],
ignores: dict[Path, set[str]] | None = None,
) -> list[str]:
"""Run vulture on the Python backend and return errors."""
vulture_bin = root / "backend" / ".venv" / "bin" / "vulture"
if not vulture_bin.exists():
found = shutil.which("vulture")
if not found:
return []
vulture_bin = Path(found)
whitelist = CONFIG_DIR / "vulture_whitelist.py"
targets = ["backend"]
if (root / "debug.py").exists():
targets.append("debug.py")
cmd = [str(vulture_bin), *targets]
if whitelist.exists():
cmd.append(str(whitelist))
cmd.extend([
"--min-confidence", str(min_confidence),
"--exclude", ".venv,__pycache__,data,uv-bin",
"--ignore-decorators", "@*.router.*,@*.websocket,@app.*,@pytest.fixture,@pytest.fixture*",
"--ignore-names", "cls",
])
try:
result = subprocess.run(
cmd, capture_output=True, text=True, cwd=str(root), timeout=30,
)
except (OSError, subprocess.TimeoutExpired):
return []
errors: list[str] = []
for line in result.stdout.strip().splitlines():
m = re.match(r"^(.+):(\d+): (.+)$", line)
if not m:
continue
filepath, lineno, message = m.groups()
if is_excepted(filepath, "vulture", exceptions):
continue
if ignores and is_lintignored(root / filepath, root, "vulture", ignores):
continue
if _is_inside_class(str(root / filepath), int(lineno)):
continue
conf = re.search(r"\((\d+)% confidence\)", message)
confidence = int(conf.group(1)) if conf else 0
severity = "error" if confidence >= error_threshold else "warning"
errors.append(f"{filepath}:{lineno}:1: {severity}: [vulture] {message}")
return errors