Files
openclaw/scripts/check-deadcode-exports.mjs
2026-07-15 22:40:00 -07:00

156 lines
4.5 KiB
JavaScript

#!/usr/bin/env node
// Enforces a hard-zero policy for Knip's unused exports.
import { fileURLToPath } from "node:url";
import { isLikelyRepoFilePath, runKnip, uniqueSorted } from "./deadcode-knip-runner.mjs";
const KNIP_ISSUES = "exports,nsExports,types,nsTypes,enumMembers,namespaceMembers";
const KNIP_SCANS = [
{
name: "production unused-export scan",
args: ["--config", "config/knip.config.ts", "--production"],
},
{
name: "full-tree unused-export scan",
args: ["--config", "config/knip.all-exports.config.ts"],
},
{
name: "script unused-export scan",
args: ["--config", "config/knip.scripts-exports.config.ts", "--include-entry-exports"],
},
];
const KNIP_COMMON_ARGS = [
"--no-progress",
"--reporter",
"compact",
"--include",
KNIP_ISSUES,
"--no-config-hints",
];
/** 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|Exports in used namespace|Unused exported types|Exported types in used namespace|Unused exported enum members|Unused exported namespace 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;
}
/** Rejects every unused export reported by Knip. */
export function checkUnusedExports(output) {
const entries = parseKnipCompactUnusedExports(output);
return {
ok: entries.length === 0,
entries,
message:
entries.length === 0
? ""
: [
"Unused exports are not allowed:",
...entries.map((entry) => ` ${entry}`),
"Delete the exports or model their real production consumers in Knip.",
].join("\n"),
};
}
async function main() {
for (const scan of KNIP_SCANS) {
const ok = await runUnusedExportScan(scan);
if (!ok) {
process.exitCode = 1;
return;
}
}
console.log(
"[deadcode] Knip production and full-tree unused-export checks passed with 0 entries.",
);
}
async function runUnusedExportScan(scan) {
const result = await runKnip([...scan.args, ...KNIP_COMMON_ARGS], { scanName: scan.name });
if (result.errorCode || result.status === null) {
console.error(
`deadcode ${scan.name} failed: ${result.errorCode ?? result.signal ?? "unknown"}${
result.errorMessage ? `: ${result.errorMessage}` : ""
}`,
);
if (result.output) {
console.error(result.output);
}
return false;
}
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 ${scan.name} produced no export sections.`);
if (result.output) {
console.error(result.output);
}
return false;
}
const check = checkUnusedExports(result.output);
if (!check.ok) {
console.error(`${scan.name}:\n${check.message}`);
return false;
}
if (result.status !== 0) {
console.error(`deadcode ${scan.name} exited with status ${result.status}.`);
if (result.output) {
console.error(result.output);
}
return false;
}
console.log(`[deadcode] Knip ${scan.name} passed with 0 entries.`);
return true;
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
await main();
}