pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
127 lines (126 loc) • 5.26 kB
JavaScript
/**
* ESLint runner for dispatch system
*
* Runs ESLint on JS/TS files when an ESLint config is present in the project.
* Prefers the local node_modules installation over global.
*
* Gate: skips when no ESLint config is detected (project uses Biome/OxLint instead).
*/
import { safeSpawnAsync } from "../../safe-spawn.js";
import { getAutofixCapability, hasEslintConfig } from "../../tool-policy.js";
import { PRIORITY } from "../priorities.js";
import { createCwdCachedProbe, resolveToolCommand, } from "./utils/runner-helpers.js";
// Per-cwd cached eslint `--version` verification (#120). Before this, every
// dispatch invocation ran a fresh `safeSpawnAsync(cmd, ["--version"])` after
// the local cmd resolution. The probe is cached per cwd because
// `resolveToolCommand("eslint")` is deterministic from cwd; the underlying
// cmd identity is captured inside the probe closure.
function makeEslintProbe(cmd) {
return createCwdCachedProbe(async (cwd) => {
const r = await safeSpawnAsync(cmd, ["--version"], { timeout: 5000, cwd });
return !r.error && r.status === 0;
});
}
const eslintProbeByCmd = new Map();
function getEslintProbe(cmd) {
const existing = eslintProbeByCmd.get(cmd);
if (existing)
return existing;
const created = makeEslintProbe(cmd);
eslintProbeByCmd.set(cmd, created);
return created;
}
function parseEslintJson(raw, filePath) {
try {
const results = JSON.parse(raw);
const autofix = getAutofixCapability("eslint");
const diagnostics = [];
for (const fileResult of results) {
for (const msg of fileResult.messages) {
const severity = msg.severity === 2 ? "error" : "warning";
diagnostics.push({
id: `eslint:${msg.ruleId ?? "unknown"}:${msg.line}`,
message: msg.ruleId ? `${msg.ruleId}: ${msg.message}` : msg.message,
filePath,
line: msg.line ?? 1,
column: msg.column ?? 1,
severity,
semantic: severity === "error" ? "blocking" : "warning",
tool: "eslint",
rule: msg.ruleId ?? undefined,
fixable: !!msg.fix,
autoFixAvailable: !!msg.fix && (autofix?.safePipelineAutofix ?? false),
fixKind: !!msg.fix && autofix?.fixKind !== "none"
? autofix?.fixKind
: undefined,
});
}
}
return { diagnostics };
}
catch (err) {
return {
diagnostics: [],
parseError: err instanceof Error ? err.message : String(err),
};
}
}
const eslintRunner = {
id: "eslint",
appliesTo: ["jsts"],
priority: PRIORITY.LINT_SECONDARY,
enabledByDefault: true,
async run(ctx) {
const cwd = ctx.cwd || process.cwd();
const userHasConfig = hasEslintConfig(cwd);
// Only run if project has an ESLint config.
if (!userHasConfig) {
return { status: "skipped", diagnostics: [], semantic: "none" };
}
const cmd = resolveToolCommand(cwd, "eslint") ?? "eslint";
// Verify ESLint is actually executable (cached per cwd, see getEslintProbe).
if (!(await getEslintProbe(cmd)(cwd))) {
return { status: "skipped", diagnostics: [], semantic: "none" };
}
const result = await safeSpawnAsync(cmd, ["--format", "json", "--no-error-on-unmatched-pattern", ctx.filePath], { timeout: 30000, cwd });
// ESLint exits 1 when there are lint errors, 2 on fatal/config error
if (result.status === 2) {
return { status: "skipped", diagnostics: [], semantic: "none" };
}
const raw = result.stdout || result.stderr || "";
if (result.status === 0) {
return { status: "succeeded", diagnostics: [], semantic: "none" };
}
const parsed = parseEslintJson(raw, ctx.filePath);
if (parsed.parseError && raw.trim().length > 0) {
const preview = raw.replace(/\s+/g, " ").slice(0, 160);
return {
status: "failed",
diagnostics: [
{
id: "eslint:parse-error:1",
message: "ESLint JSON parse failed: " + parsed.parseError + (preview ? " (output preview: " + preview + ")" : ""),
filePath: ctx.filePath,
line: 1,
column: 1,
severity: "warning",
semantic: "warning",
tool: "eslint",
},
],
semantic: "warning",
};
}
const diagnostics = parsed.diagnostics;
if (diagnostics.length === 0) {
return { status: "succeeded", diagnostics: [], semantic: "none" };
}
const hasErrors = diagnostics.some((d) => d.semantic === "blocking");
return {
status: "failed",
diagnostics,
semantic: hasErrors ? "blocking" : "warning",
};
},
};
export default eslintRunner;