pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
79 lines (78 loc) • 3.22 kB
JavaScript
import * as path from "node:path";
import * as fs from "node:fs";
import { safeSpawnAsync } from "../../safe-spawn.js";
import { PRIORITY } from "../priorities.js";
import { createCwdCachedProbe } from "./utils/runner-helpers.js";
// Per-cwd cached `mix credo --version` probe (#120). Before this, the probe
// fired on every Elixir file save in projects with mix.exs.
const probeCredo = createCwdCachedProbe(async (cwd) => {
const r = await safeSpawnAsync("mix", ["credo", "--version"], {
timeout: 10000,
cwd,
});
return !r.error && r.status === 0;
});
// `mix credo <file>` is single-file-scoped today, so blanket attribution would
// be correct — but map issue.filename (resolved against cwd) for robustness so
// this stays correct if the invocation ever widens to a directory (#265 A4).
export function parseCredoJson(raw, fallbackPath, cwd) {
try {
const output = JSON.parse(raw);
return (output.issues ?? []).map((issue) => ({
id: `credo:${issue.check}:${issue.line_no}`,
message: `[${issue.check}] ${issue.message}`,
filePath: issue.filename && issue.filename.trim()
? path.isAbsolute(issue.filename)
? issue.filename
: path.resolve(cwd, issue.filename)
: fallbackPath,
line: issue.line_no,
column: issue.column ?? 1,
severity: issue.priority <= 10 ? "error" : "warning",
semantic: issue.priority <= 10 ? "blocking" : "warning",
tool: "credo",
rule: issue.check,
fixable: false,
}));
}
catch {
return [];
}
}
function hasMixExs(cwd) {
return fs.existsSync(path.join(cwd, "mix.exs"));
}
const credoRunner = {
id: "credo",
appliesTo: ["elixir"],
priority: PRIORITY.GENERAL_ANALYSIS,
enabledByDefault: true,
skipTestFiles: false,
async run(ctx) {
const cwd = ctx.cwd || process.cwd();
if (!hasMixExs(cwd)) {
return { status: "skipped", diagnostics: [], semantic: "none" };
}
// Credo ships as a mix dependency — cached per-cwd probe (see probeCredo).
if (!(await probeCredo(cwd))) {
return { status: "skipped", diagnostics: [], semantic: "none" };
}
const absPath = path.resolve(cwd, ctx.filePath);
const result = await safeSpawnAsync("mix", ["credo", "--format", "json", "--strict", absPath], { timeout: 30000, cwd });
// credo exits 1 when issues found, 0 when clean
if (result.status === null || result.status > 1) {
return { status: "skipped", diagnostics: [], semantic: "none" };
}
const diagnostics = parseCredoJson(result.stdout ?? "", ctx.filePath, cwd);
if (diagnostics.length === 0) {
return { status: "succeeded", diagnostics: [], semantic: "none" };
}
const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
return {
status: hasBlocking ? "failed" : "succeeded",
diagnostics,
semantic: hasBlocking ? "blocking" : "warning",
};
},
};
export default credoRunner;