mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-14 11:06:08 +00:00
* chore(deadcode): add generated knip unused-export baseline * ci(deadcode): replace broken export report lanes with enforced knip exports ratchet * chore(deadcode): regenerate unused-export baseline on current main * chore(deadcode): regenerate unused-export baseline on current main
215 lines
6.7 KiB
JavaScript
215 lines
6.7 KiB
JavaScript
#!/usr/bin/env node
|
|
// Enforces a ratcheting baseline for Knip's unused exports.
|
|
import { writeFile } from "node:fs/promises";
|
|
import { fileURLToPath } from "node:url";
|
|
import {
|
|
KNIP_OPTIONAL_UNUSED_EXPORT_BASELINE,
|
|
KNIP_UNUSED_EXPORT_BASELINE,
|
|
} from "./deadcode-exports.baseline.mjs";
|
|
import {
|
|
compareStringListToAllowlist,
|
|
isLikelyRepoFilePath,
|
|
runKnip,
|
|
uniqueSorted,
|
|
} from "./deadcode-knip-runner.mjs";
|
|
|
|
const KNIP_ARGS = [
|
|
"--config",
|
|
"config/knip.config.ts",
|
|
"--production",
|
|
"--no-progress",
|
|
"--reporter",
|
|
"compact",
|
|
"--include",
|
|
"exports,types,enumMembers",
|
|
"--no-config-hints",
|
|
];
|
|
|
|
const BASELINE_HEADER = `// Pre-existing unused exports awaiting deletion.
|
|
// New entries fail CI. After deleting dead code, run \`pnpm deadcode:exports:update\`.
|
|
// Do not add entries to avoid fixing new findings.`;
|
|
|
|
/** Parses compact Knip export sections into one path-and-symbol entry per finding. */
|
|
export function parseKnipCompactUnusedExportsResult(output) {
|
|
const entries = [];
|
|
let inExportSection = false;
|
|
let sawExportSection = false;
|
|
|
|
for (const line of output.split(/\r?\n/u)) {
|
|
const sectionMatch = /^Unused (exports|exported types|exported enum members) \(\d+\)$/u.exec(
|
|
line,
|
|
);
|
|
if (sectionMatch) {
|
|
inExportSection = true;
|
|
sawExportSection = true;
|
|
continue;
|
|
}
|
|
if (/^Unused .+ \(\d+\)$/u.test(line)) {
|
|
inExportSection = false;
|
|
continue;
|
|
}
|
|
if (!inExportSection) {
|
|
continue;
|
|
}
|
|
|
|
const separatorIndex = line.indexOf(": ");
|
|
if (separatorIndex === -1) {
|
|
continue;
|
|
}
|
|
const file = line.slice(0, separatorIndex).trim();
|
|
if (!isLikelyRepoFilePath(file)) {
|
|
continue;
|
|
}
|
|
const symbols = line.slice(separatorIndex + 2).split(", ");
|
|
for (const symbol of symbols) {
|
|
const trimmedSymbol = symbol.trim();
|
|
if (trimmedSymbol) {
|
|
entries.push(`${file}: ${trimmedSymbol}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return { entries: uniqueSorted(entries), sawExportSection };
|
|
}
|
|
|
|
/** Parses compact Knip export sections into one path-and-symbol entry per finding. */
|
|
export function parseKnipCompactUnusedExports(output) {
|
|
return parseKnipCompactUnusedExportsResult(output).entries;
|
|
}
|
|
|
|
/** Compares detected unused exports against the checked-in baseline. */
|
|
export function compareUnusedExportsToBaseline(
|
|
actualEntries,
|
|
baselineEntries,
|
|
optionalBaselineEntries = [],
|
|
) {
|
|
return compareStringListToAllowlist(actualEntries, baselineEntries, optionalBaselineEntries);
|
|
}
|
|
|
|
function formatUnusedExportComparison(comparison) {
|
|
const lines = [];
|
|
if (!comparison.allowlistIsSorted) {
|
|
lines.push("deadcode unused-export baseline is not sorted.");
|
|
}
|
|
if (comparison.duplicateAllowedCount > 0) {
|
|
lines.push(
|
|
`deadcode unused-export baseline contains ${comparison.duplicateAllowedCount} duplicate entr${
|
|
comparison.duplicateAllowedCount === 1 ? "y" : "ies"
|
|
}.`,
|
|
);
|
|
}
|
|
if (comparison.unexpected.length > 0) {
|
|
lines.push("Unexpected unused exports:");
|
|
lines.push(...comparison.unexpected.map((entry) => ` ${entry}`));
|
|
}
|
|
if (comparison.stale.length > 0) {
|
|
lines.push("Stale required baseline entries:");
|
|
lines.push(...comparison.stale.map((entry) => ` ${entry}`));
|
|
}
|
|
if (lines.length > 0) {
|
|
lines.push("Run `pnpm deadcode:exports:update` after removing dead code.");
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
|
|
function formatArrayExport(name, entries) {
|
|
if (entries.length === 0) {
|
|
return `export const ${name} = [];`;
|
|
}
|
|
return `export const ${name} = [\n${entries.map((entry) => ` ${JSON.stringify(entry)},`).join("\n")}\n];`;
|
|
}
|
|
|
|
/** Emits the checked-in baseline module used by --update. */
|
|
export function formatUnusedExportBaseline(
|
|
requiredEntries,
|
|
optionalEntries = KNIP_OPTIONAL_UNUSED_EXPORT_BASELINE,
|
|
) {
|
|
// Optional entries are platform-variant: promoting one into the required
|
|
// list would make CI stale-fail on platforms where it is legitimately absent.
|
|
const optionalSet = new Set(uniqueSorted(optionalEntries));
|
|
return `${BASELINE_HEADER}\n${formatArrayExport(
|
|
"KNIP_UNUSED_EXPORT_BASELINE",
|
|
uniqueSorted(requiredEntries).filter((entry) => !optionalSet.has(entry)),
|
|
)}\n\n// Platform-variant findings. Allowed when present; never required.\n${formatArrayExport(
|
|
"KNIP_OPTIONAL_UNUSED_EXPORT_BASELINE",
|
|
uniqueSorted(optionalEntries),
|
|
)}\n`;
|
|
}
|
|
|
|
/** Checks Knip output against the current baseline. */
|
|
export function checkUnusedExports(
|
|
output,
|
|
baselineEntries = KNIP_UNUSED_EXPORT_BASELINE,
|
|
optionalBaselineEntries = KNIP_OPTIONAL_UNUSED_EXPORT_BASELINE,
|
|
) {
|
|
const actual = parseKnipCompactUnusedExports(output);
|
|
const comparison = compareUnusedExportsToBaseline(
|
|
actual,
|
|
baselineEntries,
|
|
optionalBaselineEntries,
|
|
);
|
|
return {
|
|
ok:
|
|
comparison.allowlistIsSorted &&
|
|
comparison.duplicateAllowedCount === 0 &&
|
|
comparison.unexpected.length === 0 &&
|
|
comparison.stale.length === 0,
|
|
comparison,
|
|
message: formatUnusedExportComparison(comparison),
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
const update = process.argv.slice(2).includes("--update");
|
|
const result = await runKnip(KNIP_ARGS, { scanName: "unused-export scan" });
|
|
if (result.errorCode || result.status === null) {
|
|
console.error(
|
|
`deadcode unused-export scan failed: ${result.errorCode ?? result.signal ?? "unknown"}${
|
|
result.errorMessage ? `: ${result.errorMessage}` : ""
|
|
}`,
|
|
);
|
|
if (result.output) {
|
|
console.error(result.output);
|
|
}
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
const parsed = parseKnipCompactUnusedExportsResult(result.output);
|
|
// Knip's compact reporter omits empty sections, so a clean scan (exit 0)
|
|
// legitimately prints no export sections; sectionless output is only a
|
|
// failure signal when Knip also exited nonzero (crash/config error).
|
|
if (!parsed.sawExportSection && result.status !== 0) {
|
|
console.error("deadcode unused-export scan produced no export sections.");
|
|
if (result.output) {
|
|
console.error(result.output);
|
|
}
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
const actual = parsed.entries;
|
|
if (update) {
|
|
const baselinePath = fileURLToPath(new URL("./deadcode-exports.baseline.mjs", import.meta.url));
|
|
await writeFile(
|
|
baselinePath,
|
|
formatUnusedExportBaseline(actual, KNIP_OPTIONAL_UNUSED_EXPORT_BASELINE),
|
|
"utf8",
|
|
);
|
|
console.log(`[deadcode] Updated unused-export baseline with ${actual.length} entries.`);
|
|
return;
|
|
}
|
|
|
|
const check = checkUnusedExports(result.output);
|
|
if (!check.ok) {
|
|
console.error(check.message);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
console.log(`[deadcode] Knip unused-export baseline matched ${actual.length} entries.`);
|
|
}
|
|
|
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
await main();
|
|
}
|