UNPKG

pi-lens

Version:

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

117 lines (116 loc) • 4.87 kB
import * as path from "node:path"; import { safeSpawnAsync } from "../../safe-spawn.js"; import { getLinterPolicyForCwd, hasStylelintConfig, } from "../../tool-policy.js"; import { PRIORITY } from "../priorities.js"; import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js"; const stylelint = createAvailabilityChecker("stylelint", ".cmd"); // Stylelint's standard JSON output reports only aggregate fixableErrorCount / // fixableWarningCount per file — there is no per-warning fix flag in the CLI // surface. To route the actionable warnings here without a tool rewrite, we // keep a curated set of rule IDs whose `--fix` behavior is deterministic and // safe. Sourced from rule pages in stylelint's docs that explicitly state // "stylelint can automatically fix all of the problems reported by this rule". // Update when stylelint adds or removes fixable rules. const STYLELINT_FIXABLE_RULES = new Set([ // whitespace / spacing — formatter-style, always safe "block-no-empty", "color-hex-length", "declaration-block-no-duplicate-properties", "declaration-block-no-redundant-longhand-properties", "declaration-block-no-shorthand-property-overrides", "declaration-block-single-line-max-declarations", "font-family-name-quotes", "function-url-quotes", "length-zero-no-unit", "media-feature-name-no-vendor-prefix", "no-descending-specificity", "no-duplicate-at-import-rules", "no-duplicate-selectors", "no-empty-source", "no-eol-whitespace", "no-extra-semicolons", "no-invalid-double-slash-comments", "no-missing-end-of-source-newline", "number-leading-zero", "number-no-trailing-zeros", "property-no-vendor-prefix", "selector-attribute-quotes", "selector-no-vendor-prefix", "selector-pseudo-element-colon-notation", "selector-type-case", "shorthand-property-no-redundant-values", "string-quotes", "value-no-vendor-prefix", ]); function parseStylelintJson(raw, filePath) { try { const results = JSON.parse(raw); const diagnostics = []; for (const result of results) { for (const w of result.warnings) { const severity = w.severity === "error" ? "error" : "warning"; const fixable = STYLELINT_FIXABLE_RULES.has(w.rule); diagnostics.push({ id: `stylelint-${w.line}-${w.rule}`, message: `[${w.rule}] ${w.text.replace(/\s*\(stylelint.*?\)$/, "")}`, filePath, line: w.line, column: w.column, severity, semantic: severity === "error" ? "blocking" : "warning", tool: "stylelint", rule: w.rule, fixable, fixSuggestion: fixable ? "Run `stylelint --fix` to apply the deterministic auto-correction for this rule." : undefined, }); } } return diagnostics; } catch { return []; } } const stylelintRunner = { id: "stylelint", appliesTo: ["css"], priority: PRIORITY.FORMAT_AND_LINT_PRIMARY, enabledByDefault: true, skipTestFiles: false, async run(ctx) { const cwd = ctx.cwd || process.cwd(); const policy = getLinterPolicyForCwd(ctx.filePath, cwd); if (policy && !policy.preferredRunners.includes("stylelint")) { return { status: "skipped", diagnostics: [], semantic: "none" }; } const fileDir = path.dirname(path.resolve(cwd, ctx.filePath)); const hasConfig = hasStylelintConfig(fileDir) || hasStylelintConfig(cwd); if (!hasConfig) { ctx.log("stylelint: no config detected, running with default rules"); } let cmd = null; if (await (stylelint.isAvailableAsync(cwd))) { cmd = stylelint.getCommand(cwd); } else { cmd = await resolveToolCommandWithInstallFallback(cwd, "stylelint"); } if (!cmd) return { status: "skipped", diagnostics: [], semantic: "none" }; const result = await safeSpawnAsync(cmd, ["--formatter", "json", ctx.filePath], { timeout: 20000, cwd }); const raw = result.stdout ?? ""; const diagnostics = parseStylelintJson(raw, ctx.filePath); 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 stylelintRunner;