UNPKG

pi-lens

Version:

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

150 lines (149 loc) • 6.01 kB
/** * Shared diagnostic output parsers for pi-lens runners * * Common patterns for parsing tool output into standardized diagnostics. * Supports the common `file:line:col: message` format used by most linters. */ import { getAutofixCapability } from "../../../tool-policy.js"; /** * Create a parser for line-based tool output. * Common format: file:line:col: message (with variations) */ export function createLineParser(config) { return (raw, filePath) => { const diagnostics = []; // Optionally strip ANSI codes (for tools that output colored text) const clean = config.stripAnsi !== false ? raw.replace(/\x1b\[[0-9;]*m/g, "") : raw; const lines = clean.split("\n").filter((l) => l.trim()); for (const line of lines) { const match = line.match(config.regex); if (!match) continue; const lineNum = parseInt(match[2], 10); const colNum = parseInt(match[3], 10); const severity = config.getSeverity ? config.getSeverity(line, match) : "warning"; const fixable = typeof config.fixable === "function" ? config.fixable(match) : (config.fixable ?? false); const autoFixAvailable = typeof config.autoFixAvailable === "function" ? config.autoFixAvailable(match) : (config.autoFixAvailable ?? false); const fixKind = typeof config.fixKind === "function" ? config.fixKind(match) : config.fixKind; diagnostics.push({ id: config.generateId(match), message: config.extractMessage(match), filePath, line: lineNum, column: colNum, severity, semantic: severity === "error" ? "blocking" : "warning", tool: config.tool, rule: config.extractRule?.(match), defectClass: config.defectClass, fixable, autoFixAvailable, fixKind, }); } return diagnostics; }; } // ============================================================================= // PRE-BUILT PARSERS FOR COMMON TOOLS // ============================================================================= /** * Parse Ruff output: file:line:col: CODE message */ const ruffAutofix = getAutofixCapability("ruff"); export const parseRuffOutput = createLineParser({ tool: "ruff", regex: /^(.+?):(\d+):(\d+):\s*(\w+)\s*(.+)/, extractMessage: (m) => `${m[4]}: ${m[5]}`, // CODE: message extractRule: (m) => m[4], generateId: (m) => `ruff-${m[4]}`, fixable: true, // Ruff can fix most issues autoFixAvailable: ruffAutofix?.safePipelineAutofix ?? false, fixKind: ruffAutofix?.fixKind === "none" ? undefined : ruffAutofix?.fixKind, }); /** * Parse Go vet output: file:line:col: message */ export const parseGoVetOutput = createLineParser({ tool: "go-vet", regex: /^(.+?):(\d+):(\d+):\s*(.+)/, extractMessage: (m) => m[4], generateId: (m) => `go-vet-${m[2]}`, defectClass: "correctness", }); /** * Parse Biome output: file:line:col message (category) * With autofix support for fix suggestions */ export function createBiomeParser(_autofix = false) { const biomeAutofix = getAutofixCapability("biome"); return createLineParser({ tool: "biome", regex: /^(.+?):(\d+):(\d+)\s+(.+?)\s*\((.+?)\)/, extractMessage: (m) => `${m[5]}: ${m[4]}`, // category: message extractRule: (m) => m[5], generateId: (m) => `biome-${m[2]}-${m[5]}`, getSeverity: (line) => (line.includes("error") ? "error" : "warning"), fixable: true, autoFixAvailable: biomeAutofix?.safePipelineAutofix ?? false, fixKind: biomeAutofix?.fixKind === "none" ? undefined : biomeAutofix?.fixKind, }); } // Backward-compatible default biome parser export const parseBiomeOutput = createBiomeParser(false); // ============================================================================= // GENERIC PARSER FACTORY // ============================================================================= /** * Create a simple parser for tools using standard file:line:col format. * Format variations: :line:col:, line:col, (line,col), etc. */ export function createSimpleParser(tool, options = {}) { const sep = options.separator ?? ":"; const severity = options.severity ?? "warning"; const fixable = options.fixable ?? false; // Build regex based on separator type const escapedSep = sep === " " ? "\\s+" : escapeRegExp(sep); const regex = options.includesFileName ? new RegExp(`^(.+?)${escapedSep}(\\d+)${escapedSep}(\\d+)${escapedSep}(.+)`) : new RegExp(`^(\\d+)${escapedSep}(\\d+)${escapedSep}(.+)`); return (raw, filePath) => { const diagnostics = []; const lines = raw.split("\n").filter((l) => l.trim()); for (const line of lines) { const match = line.match(regex); if (!match) continue; const lineNum = options.includesFileName ? parseInt(match[2], 10) : parseInt(match[1], 10); const colNum = options.includesFileName ? parseInt(match[3], 10) : parseInt(match[2], 10); const message = options.includesFileName ? match[4] : match[3]; diagnostics.push({ id: `${tool}-${lineNum}`, message: message.trim(), filePath, line: lineNum, column: colNum, severity, semantic: severity === "error" ? "blocking" : "warning", tool, fixable, }); } return diagnostics; }; } function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }