mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-22 20:52:23 +02:00
96 lines
3.2 KiB
Python
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
|