UNPKG

pi-lens

Version:

Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo

127 lines (126 loc) • 5.26 kB
/** * 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;