pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
122 lines (121 loc) • 4.58 kB
JavaScript
import { safeSpawnAsync } from "../../safe-spawn.js";
import { getLinterPolicyForCwd, hasMarkdownlintConfig, } from "../../tool-policy.js";
import { PRIORITY } from "../priorities.js";
import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
const markdownlint = createAvailabilityChecker("markdownlint-cli2", ".cmd");
// markdownlint-cli2 text output does not include per-violation fixability,
// so we keep a static allowlist of MD### rules whose --fix is deterministic.
// Sourced from the rule pages in
// https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md — every
// entry below is documented as "Fixable: yes" (or equivalent). Update when
// markdownlint adds or changes auto-fix support.
const MARKDOWNLINT_FIXABLE_RULES = new Set([
"MD001",
"MD004",
"MD005",
"MD007",
"MD009",
"MD010",
"MD011",
"MD012",
"MD014",
"MD018",
"MD019",
"MD020",
"MD021",
"MD022",
"MD023",
"MD026",
"MD027",
"MD030",
"MD031",
"MD032",
"MD034",
"MD037",
"MD038",
"MD039",
"MD044",
"MD047",
"MD049",
"MD050",
"MD053",
"MD058",
]);
// markdownlint-cli2 output: `path:line[:col] [error|warning] MD###/name[/name…] message`
// Two things the original parser missed (→ silent 0 diagnostics, #212):
// 1. cli2 emits a severity token (`error`/`warning`) between the col and the
// rule code — older markdownlint-cli did not.
// 2. some rules carry MULTIPLE slash-separated names (e.g.
// `MD041/first-line-heading/first-line-h1`).
// The severity token is optional so older/relative-path output still parses.
function parseMarkdownlintOutput(raw, filePath) {
const diagnostics = [];
for (const line of raw.split(/\r?\n/)) {
if (!line.trim())
continue;
// Rule code is MD### followed by one or more slash-joined names. Use a
// single char class (`[\w/-]+`) rather than a nested quantifier
// (`(?:/[\w-]+)+`) so there's no super-linear backtracking (S5852).
const match = line.match(/^.*?:(\d+)(?::(\d+))?\s+(?:error|warning)?\s*(MD\d+\/[\w/-]+)\s+(.+)$/);
if (!match)
continue;
const [, lineNum, col, ruleCode, message] = match;
const ruleName = ruleCode.split("/")[0];
const fixable = MARKDOWNLINT_FIXABLE_RULES.has(ruleName);
diagnostics.push({
id: `markdownlint-${lineNum}-${ruleName}`,
message: `[${ruleCode}] ${message}`,
filePath,
line: Number(lineNum),
column: col ? Number(col) : 1,
severity: "warning",
semantic: "warning",
tool: "markdownlint",
rule: ruleName,
fixable,
fixSuggestion: fixable
? "Run `markdownlint-cli2 --fix` to apply the deterministic auto-correction for this rule."
: undefined,
});
}
return diagnostics;
}
const markdownlintRunner = {
id: "markdownlint",
appliesTo: ["markdown"],
priority: PRIORITY.DOC_QUALITY,
enabledByDefault: true,
skipTestFiles: false,
async run(ctx) {
const cwd = ctx.cwd || process.cwd();
const policy = getLinterPolicyForCwd(ctx.filePath, cwd);
if (policy && !policy.preferredRunners.includes("markdownlint")) {
return { status: "skipped", diagnostics: [], semantic: "none" };
}
const hasConfig = hasMarkdownlintConfig(cwd);
if (!hasConfig) {
// Run with sensible defaults even without explicit config
}
let cmd = null;
if (await (markdownlint.isAvailableAsync(cwd))) {
cmd = markdownlint.getCommand(cwd);
}
else {
cmd = await resolveToolCommandWithInstallFallback(cwd, "markdownlint");
}
if (!cmd)
return { status: "skipped", diagnostics: [], semantic: "none" };
const configArgs = hasConfig ? [] : ["--disable", "MD013"];
const result = await safeSpawnAsync(cmd, [...configArgs, ctx.filePath], {
timeout: 15000,
cwd,
});
const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
const diagnostics = parseMarkdownlintOutput(raw, ctx.filePath);
if (diagnostics.length === 0) {
return { status: "succeeded", diagnostics: [], semantic: "none" };
}
return { status: "succeeded", diagnostics, semantic: "warning" };
},
};
export default markdownlintRunner;