pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
95 lines (94 loc) • 4.01 kB
JavaScript
/**
* Ruff runner for dispatch system
*
* Dispatch mode is diagnostics-only.
* Autofix is handled earlier by the post-write pipeline to avoid
* mutating files mid-dispatch after LSP sync has already happened.
* Supports venv-local installations.
*/
import { resolvePackagePath } from "../../package-root.js";
import { safeSpawnAsync } from "../../safe-spawn.js";
import { stripAnsi } from "../../sanitize.js";
import { getAutofixCapability, getLinterPolicyForCwd, hasRuffConfig, } from "../../tool-policy.js";
import { PRIORITY } from "../priorities.js";
import { parseRuffOutput } from "./utils/diagnostic-parsers.js";
import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
const ruff = createAvailabilityChecker("ruff", ".exe");
function parseRuffJson(raw, filePath) {
try {
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed))
return [];
const autofix = getAutofixCapability("ruff");
return parsed.map((item, index) => {
const severity = item.severity === "error" ? "error" : "warning";
const code = item.code || "ruff";
const toolFixable = Boolean(item.fix);
return {
id: `ruff-${code}-${item.location?.row ?? index + 1}`,
message: item.message || code,
filePath: item.filename || filePath,
line: item.location?.row ?? 1,
column: item.location?.column ?? 1,
severity,
semantic: severity === "error" ? "blocking" : "warning",
tool: "ruff",
rule: code,
fixable: toolFixable,
autoFixAvailable: toolFixable && (autofix?.safePipelineAutofix ?? false),
fixKind: toolFixable && autofix?.fixKind !== "none"
? autofix?.fixKind
: undefined,
};
});
}
catch {
return [];
}
}
const ruffRunner = {
id: "ruff-lint",
appliesTo: ["python"],
priority: PRIORITY.FORMAT_AND_LINT_PRIMARY,
enabledByDefault: true,
async run(ctx) {
const cwd = ctx.cwd || process.cwd();
const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
if (policy && !policy.preferredRunners.includes("ruff-lint")) {
return { status: "skipped", diagnostics: [], semantic: "none" };
}
const cmd = await resolveAvailableOrInstall(ruff, "ruff", cwd);
if (!cmd) {
return { status: "skipped", diagnostics: [], semantic: "none" };
}
const configArgs = hasRuffConfig(cwd)
? []
: [
"--config",
resolvePackagePath(import.meta.url, "config/ruff/core.toml"),
];
// Step 1: Capture diagnostics (before fixing) — teaching signal for the agent
const checkResult = await safeSpawnAsync(cmd, ["check", "--output-format", "json", ...configArgs, ctx.filePath], { timeout: 30000 });
const raw = stripAnsi(checkResult.stdout + checkResult.stderr);
const diagnostics = parseRuffJson(checkResult.stdout || "", ctx.filePath);
const parsedDiagnostics = diagnostics.length > 0 ? diagnostics : parseRuffOutput(raw, ctx.filePath);
if (checkResult.status === 0 && parsedDiagnostics.length === 0) {
return { status: "succeeded", diagnostics: [], semantic: "none" };
}
if (parsedDiagnostics.length === 0) {
return {
status: "failed",
diagnostics: [],
semantic: "warning",
rawOutput: raw.slice(0, 500),
};
}
const hasErrors = parsedDiagnostics.some((d) => d.severity === "error");
return {
status: hasErrors ? "failed" : "succeeded",
diagnostics: parsedDiagnostics,
semantic: hasErrors ? "blocking" : "warning",
};
},
};
export default ruffRunner;