pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
81 lines (80 loc) • 3.48 kB
JavaScript
import * as path from "node:path";
import { safeSpawnAsync } from "../../safe-spawn.js";
import { hasMypyConfig } from "../../tool-policy.js";
import { PRIORITY } from "../priorities.js";
import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
const mypy = createAvailabilityChecker("mypy", "");
// mypy output: file.py:10: error: Incompatible types [assignment]
//
// mypy follows imports and reports errors in OTHER modules, not just the file
// it was invoked on. Attribute each diagnostic to the file mypy names (group 1,
// resolved against cwd) rather than blanket-stamping ctx.filePath — otherwise a
// cross-file regression is mis-located onto the edited file (#265 A2). We do NOT
// filter to the edited file: surfacing the cross-file impact is the point.
export function parseMypyOutput(raw, fallbackPath, cwd) {
const diagnostics = [];
const linePattern = /^(.+?):(\d+)(?::(\d+))?:\s*(error|warning|note):\s*(.+?)(?:\s+\[([^\]]+)\])?$/gm;
for (const match of raw.matchAll(linePattern)) {
const [, file, lineNum, col, level, message, errorCode] = match;
if (!lineNum || !level || !message)
continue;
if (level === "note")
continue; // skip contextual notes
const severity = level === "error" ? "error" : "warning";
const rule = errorCode ?? "mypy";
const filePath = file && file.trim()
? path.isAbsolute(file)
? file
: path.resolve(cwd, file)
: fallbackPath;
diagnostics.push({
id: `mypy-${lineNum}-${rule}`,
message: errorCode ? `[${errorCode}] ${message}` : message,
filePath,
line: Number(lineNum),
column: col ? Number(col) : 1,
severity,
semantic: severity === "error" ? "blocking" : "warning",
tool: "mypy",
rule,
defectClass: "correctness",
});
}
return diagnostics;
}
const mypyRunner = {
id: "mypy",
appliesTo: ["python"],
priority: PRIORITY.GENERAL_ANALYSIS,
enabledByDefault: true,
skipTestFiles: false,
async run(ctx) {
const cwd = ctx.cwd || process.cwd();
// Only run if mypy config exists — avoids false positives in untyped projects
if (!hasMypyConfig(cwd)) {
return { status: "skipped", diagnostics: [], semantic: "none" };
}
let cmd = null;
if (await (mypy.isAvailableAsync(cwd))) {
cmd = mypy.getCommand(cwd);
}
else {
cmd = await resolveToolCommandWithInstallFallback(cwd, "mypy");
}
if (!cmd)
return { status: "skipped", diagnostics: [], semantic: "none" };
const result = await safeSpawnAsync(cmd, ["--no-error-summary", "--show-column-numbers", ctx.filePath], { timeout: 30000, cwd });
const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
const diagnostics = parseMypyOutput(raw, 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 mypyRunner;