pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
170 lines (169 loc) • 7.43 kB
JavaScript
/**
* Rust clippy runner for dispatch system
*
* Runs `cargo clippy` for Rust files to catch common mistakes.
*/
import { dirname, isAbsolute, join, resolve } from "node:path";
import { findNearestContaining } from "../../path-utils.js";
import { RustClient } from "../../rust-client.js";
import { safeSpawnAsync } from "../../safe-spawn.js";
import { stripAnsi } from "../../sanitize.js";
import { tryLazyInstall } from "./utils/lazy-installer.js";
import { PRIORITY } from "../priorities.js";
import { createCwdCachedProbe } from "./utils/runner-helpers.js";
const rustClient = new RustClient();
// Cached per-cwd `cargo clippy --version` probe (#120). Before this, the
// probe fired on every Rust file save in a project where clippy was already
// installed.
//
// `tryLazyInstall("rust-clippy", cwd)` mutates installed state, so on a
// false initial result the runner needs a way to bust the cache and re-probe
// after install. We can't `delete` a Promise mid-flight, so the safe path
// is: if the cached probe resolves false AND the install just succeeded,
// fall back to a one-shot fresh probe rather than reusing the cached false.
const probeClippy = (cargoExe) => createCwdCachedProbe(async (cwd) => {
const r = await safeSpawnAsync(cargoExe, ["clippy", "--version"], {
timeout: 8000,
cwd,
});
return !r.error && r.status === 0;
});
const clippyProbeByCargo = new Map();
function getClippyProbe(cargoExe) {
const existing = clippyProbeByCargo.get(cargoExe);
if (existing)
return existing;
const created = probeClippy(cargoExe);
clippyProbeByCargo.set(cargoExe, created);
return created;
}
const rustClippyRunner = {
id: "rust-clippy",
appliesTo: ["rust"],
priority: PRIORITY.SPECIALIZED_ANALYSIS,
enabledByDefault: true,
timeoutMs: 90_000,
async run(ctx) {
// Resolve cargo path using platform-aware lookup (handles ~/.cargo/bin on Windows)
const cargoExe = await rustClient.findCargoPathAsync();
if (!cargoExe) {
return { status: "skipped", diagnostics: [], semantic: "none" };
}
const clippyProbe = getClippyProbe(cargoExe);
if (!(await clippyProbe(ctx.cwd))) {
await tryLazyInstall("rust-clippy", ctx.cwd);
// Bust the cwd-keyed cache so the post-install state is observed.
clippyProbeByCargo.set(cargoExe, probeClippy(cargoExe));
if (!(await getClippyProbe(cargoExe)(ctx.cwd))) {
return { status: "skipped", diagnostics: [], semantic: "none" };
}
}
// Find the package root (where Cargo.toml is)
const cargoToml = findCargoToml(ctx.filePath);
if (!cargoToml) {
return { status: "skipped", diagnostics: [], semantic: "none" };
}
// Run cargo clippy on the package
const result = await safeSpawnAsync(cargoExe, ["clippy", "--message-format=json", "-q"], {
timeout: 60000,
cwd: cargoToml.replace("Cargo.toml", ""),
});
const raw = stripAnsi(result.stdout + result.stderr);
if (result.status === 0 && !raw.trim()) {
return { status: "succeeded", diagnostics: [], semantic: "none" };
}
// Parse JSON output. span.file is relative to the package root, so pass
// the cargo dir to resolve diagnostics to absolute paths for filtering.
const cargoDir = cargoToml.replace("Cargo.toml", "");
const allDiagnostics = parseClippyOutput(raw, ctx.filePath, cargoDir);
if (allDiagnostics.length === 0) {
// Non-parseable output
return {
status: "failed",
diagnostics: [],
semantic: "warning",
rawOutput: raw.substring(0, 500),
};
}
// Lint-style policy (#265 B1): `cargo clippy` compiles the whole crate, so
// a crate-mate's pre-existing diagnostic must NOT fail the edited file's
// turn. Filter to the edited file like golangci-lint — if the edited file
// is clean, this turn succeeds even when siblings carry warnings/errors.
const absEdited = resolve(ctx.filePath);
const diagnostics = allDiagnostics.filter((d) => resolve(d.filePath) === absEdited);
const hasErrors = diagnostics.some((d) => d.semantic === "blocking");
return {
status: hasErrors ? "failed" : "succeeded",
diagnostics,
semantic: hasErrors
? "blocking"
: diagnostics.length > 0
? "warning"
: "none",
};
},
};
function findCargoToml(filePath) {
const dir = findNearestContaining(dirname(filePath), ["Cargo.toml"]);
return dir ? join(dir, "Cargo.toml") : undefined;
}
/**
* Find a machine-applicable suggested replacement across the message's spans.
* Clippy emits one diagnostic per warning but can attach the auto-fix to a
* span other than the primary one (e.g. a fix that rewrites a use-site AND
* removes the now-unused import). We treat the diagnostic as fixable if any
* span carries a MachineApplicable suggestion — that's the applicability
* level clippy promises is safe for `cargo clippy --fix` to apply without
* human review.
*/
function findMachineApplicableSpan(spans) {
return spans.find((s) => typeof s.suggested_replacement === "string" &&
s.suggestion_applicability === "MachineApplicable");
}
export function parseClippyOutput(raw, fallbackPath, cargoDir) {
const diagnostics = [];
const lines = raw.split("\n").filter((l) => l.trim());
for (const line of lines) {
try {
const msg = JSON.parse(line);
if (msg.reason !== "compiler-message")
continue;
const message = msg.message;
if (!message)
continue;
// Only include messages for this file or project-wide
const spans = message.spans ?? [];
const span = spans[0];
if (!span)
continue;
const fixableSpan = findMachineApplicableSpan(spans);
// span.file is relative to the package root; resolve to absolute when
// the cargo dir is known so callers can filter by edited file (#265 B1).
const rawFile = span.file || span.file_name;
const filePath = rawFile
? cargoDir && !isAbsolute(rawFile)
? resolve(cargoDir, rawFile)
: rawFile
: fallbackPath;
diagnostics.push({
id: `clippy-${message.code?.code || "unknown"}`,
message: message.message || "Clippy warning",
filePath,
line: span.line_start || 0,
column: span.column_start || 0,
severity: message.level === "error" ? "error" : "warning",
semantic: message.level === "error" ? "blocking" : "warning",
tool: "rust-clippy",
rule: message.code?.code,
defectClass: "correctness",
fixable: fixableSpan !== undefined,
fixSuggestion: fixableSpan?.suggested_replacement,
});
}
catch {
// Not a JSON line, skip
}
}
return diagnostics;
}
export default rustClippyRunner;