mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
93 lines
3.5 KiB
Python
93 lines
3.5 KiB
Python
"""Ruff runner: per-file, scope-aware lint that vulture's global name-set can't do.
|
|
|
|
Owns the checks ruff is strictly better at than vulture:
|
|
- F401 unused imports (real per-file scope; honors __all__ / redundant-alias re-exports)
|
|
- F811 redefinition of an unused name
|
|
- F841 unused local variable assignment
|
|
- ARG001/ARG002 unused function/method arguments
|
|
|
|
Vulture is narrowed (see checks/vulture.py) to only emit dead functions, methods,
|
|
classes, and attributes, the whole-program reachability that ruff structurally
|
|
does not attempt. The two are complementary, not redundant.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from . import CheckError, is_excepted, is_lintignored
|
|
|
|
# Generous because the first run after a window reload races the editor's
|
|
# startup load with a cold ruff cache; a tight limit there is exactly what made
|
|
# the check time out and silently report zero.
|
|
_TIMEOUT = 120
|
|
|
|
# Ruff emits ANSI color into its concise output even on a non-tty / NO_COLOR;
|
|
# strip the escapes before parsing so the regex sees plain text.
|
|
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
|
|
|
|
# `path:line:col: CODE message`. The optional `[*]` fixable marker is dropped.
|
|
_LINE_RE = re.compile(r"^(?P<path>.+?):(?P<line>\d+):(?P<col>\d+): (?P<code>[A-Z]+\d+) (?P<msg>.+)$")
|
|
|
|
|
|
def run_ruff(
|
|
root: Path,
|
|
select: str,
|
|
exceptions: dict[str, list[str]],
|
|
ignores: dict[Path, set[str]] | None = None,
|
|
) -> list[str]:
|
|
"""Run ruff on the Python backend and return errors."""
|
|
ruff_bin = root / "backend" / ".venv" / "bin" / "ruff"
|
|
if not ruff_bin.exists():
|
|
found = shutil.which("ruff")
|
|
if not found:
|
|
raise CheckError("ruff executable not found in backend/.venv/bin or PATH")
|
|
ruff_bin = Path(found)
|
|
|
|
targets = ["backend"]
|
|
if (root / "debug.py").exists():
|
|
targets.append("debug.py")
|
|
cmd = [
|
|
str(ruff_bin), "check", *targets,
|
|
"--isolated", # ignore any stray pyproject/ruff.toml so the linter is hermetic
|
|
"--select", select,
|
|
"--output-format", "concise",
|
|
"--no-fix",
|
|
"--exclude", ".venv,__pycache__,data,uv-bin,webapp_template,.runner-venv",
|
|
]
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
cmd, capture_output=True, text=True, cwd=str(root), timeout=_TIMEOUT,
|
|
)
|
|
except subprocess.TimeoutExpired as e:
|
|
raise CheckError(f"timed out after {_TIMEOUT}s (machine under load or cold cache)") from e
|
|
except OSError as e:
|
|
raise CheckError(f"failed to launch ruff ({e})") from e
|
|
|
|
# ruff exits 0 (no findings) or 1 (findings) on success; anything else with
|
|
# no parseable output means ruff itself errored (e.g. could not write its
|
|
# cache) — surface it instead of treating the empty stdout as "clean".
|
|
if result.returncode not in (0, 1) and not result.stdout.strip():
|
|
detail = _ANSI_RE.sub("", result.stderr).strip()[:300] or "no output"
|
|
raise CheckError(f"ruff exited with code {result.returncode}: {detail}")
|
|
|
|
errors: list[str] = []
|
|
for line in result.stdout.strip().splitlines():
|
|
m = _LINE_RE.match(_ANSI_RE.sub("", line).strip())
|
|
if not m:
|
|
continue
|
|
filepath = m.group("path")
|
|
if is_excepted(filepath, "ruff", exceptions):
|
|
continue
|
|
if ignores and is_lintignored(root / filepath, root, "ruff", ignores):
|
|
continue
|
|
errors.append(
|
|
f"{filepath}:{m.group('line')}:{m.group('col')}: "
|
|
f"error: [ruff] {m.group('code')} {m.group('msg')}"
|
|
)
|
|
return errors
|