UNPKG

pi-lens

Version:

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

119 lines (118 loc) • 5.15 kB
/** * golangci-lint runner for dispatch system * * Runs golangci-lint when a .golangci.yml config is present. * golangci-lint is the standard meta-linter for Go projects — it runs * staticcheck, errcheck, gosimple, and many others in one pass. * * Gate: skips when no .golangci.yml/.golangci.yaml config is found (project * relies on go-vet only). This avoids noisy default-rule runs on projects * that haven't opted in. */ import * as path from "node:path"; import { safeSpawnAsync } from "../../safe-spawn.js"; import { getLinterPolicyForCwd, hasGolangciConfig } from "../../tool-policy.js"; import { PRIORITY } from "../priorities.js"; import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js"; const golangci = createAvailabilityChecker("golangci-lint", ".exe"); /** * Summarize a golangci-lint Replacement into a single-line fixSuggestion. * Prefers the Inline rewrite (precise + short) over multi-line block * replacements, and reports a delete-only fix when there's no new content. * Returns undefined when there is no Replacement attached. */ function describeReplacement(replacement) { if (!replacement) return undefined; if (replacement.Inline?.NewString !== undefined) { return `Replace with: ${replacement.Inline.NewString}`; } if (replacement.NeedOnlyDelete) return "Delete this code"; if (replacement.NewLines && replacement.NewLines.length > 0) { const preview = replacement.NewLines[0].trim(); const hint = replacement.NewLines.length > 1 ? ` (+${replacement.NewLines.length - 1} more lines)` : ""; return `Replace with: ${preview}${hint}`; } return "Apply golangci-lint suggested fix"; } function parseGolangciJson(raw, filePath) { try { const output = JSON.parse(raw); if (!output.Issues) return []; const absFile = path.resolve(filePath); return output.Issues.filter((issue) => path.resolve(issue.Pos.Filename) === absFile).map((issue) => { const severity = issue.Severity === "error" ? "error" : "warning"; // golangci-lint's --out-format=json emits a Replacement object per // issue when `golangci-lint run --fix` would deterministically // rewrite the code. Mirror the rust-clippy structured-output path // (commit 221b34d): propagate the field to fixable / fixSuggestion // so the diagnostic routes through actionable-warnings. const fixSuggestion = describeReplacement(issue.Replacement); return { id: `golangci:${issue.FromLinter}:${issue.Pos.Line}`, message: `${issue.FromLinter}: ${issue.Text}`, filePath, line: issue.Pos.Line, column: issue.Pos.Column, severity, semantic: severity === "error" ? "blocking" : "warning", tool: "golangci-lint", rule: issue.FromLinter, defectClass: "correctness", fixable: Boolean(issue.Replacement), fixSuggestion, }; }); } catch { return []; } } // Exported for the parser unit tests (#112 golangci-lint slice). export { parseGolangciJson }; const golangciRunner = { id: "golangci-lint", appliesTo: ["go"], priority: PRIORITY.GENERAL_ANALYSIS, enabledByDefault: true, timeoutMs: 90_000, async run(ctx) { const cwd = ctx.cwd || process.cwd(); const policy = getLinterPolicyForCwd(ctx.filePath, cwd); if (policy && !policy.preferredRunners.includes("golangci-lint")) { return { status: "skipped", diagnostics: [], semantic: "none" }; } // Only run if project has opted in via config file if (!hasGolangciConfig(cwd)) { return { status: "skipped", diagnostics: [], semantic: "none" }; } const cmd = await resolveAvailableOrInstall(golangci, "golangci-lint", cwd); if (!cmd) return { status: "skipped", diagnostics: [], semantic: "none" }; // Run on the specific file. golangci-lint accepts file paths directly. const result = await safeSpawnAsync(cmd, ["run", "--out-format=json", ctx.filePath], { timeout: 60000, cwd }); if (result.status === 0) { return { status: "succeeded", diagnostics: [], semantic: "none" }; } const diagnostics = parseGolangciJson(result.stdout, ctx.filePath); let semantic = "none"; if (diagnostics.some((d) => d.semantic === "blocking")) { semantic = "blocking"; } else if (diagnostics.length > 0) { semantic = "warning"; } if (semantic === "none") { // Non-zero exit but no parseable issues — likely a config/tool error return { status: "skipped", diagnostics: [], semantic }; } return { status: semantic === "blocking" ? "failed" : "succeeded", diagnostics, semantic, }; }, }; export default golangciRunner;