UNPKG

pi-lens

Version:

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

81 lines (80 loc) • 3.48 kB
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;