mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
* test(signal): deslop s080 tests * test(node-host): deslop s085 tests * test(imessage): deslop s084 tests * test(scripts): deslop s075 tests * test(flows): deslop s089 tests * test(slack): deslop s087 tests * test(mattermost): deslop s091 tests * test(infra): deslop s086 tests * test(plugin-sdk): deslop s090 tests * test(voice-call): deslop s092 tests * style(test): format batch d016 replay * test(mattermost): preserve retained reaction regression * test: retain independent contracts in batch d016 * test(plugin-sdk): avoid shadowed table labels * test(node-host): remove unused runtime test type
171 lines
5.1 KiB
TypeScript
171 lines
5.1 KiB
TypeScript
#!/usr/bin/env node
|
|
// Enforces a hard-zero policy for Knip's unused exports.
|
|
import { fileURLToPath } from "node:url";
|
|
import {
|
|
isLikelyRepoFilePath,
|
|
runKnipScans,
|
|
type KnipRunResult,
|
|
uniqueSorted,
|
|
} from "./deadcode-knip-runner.mts";
|
|
|
|
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: string) {
|
|
const entries: string[] = [];
|
|
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: string) {
|
|
return parseKnipCompactUnusedExportsResult(output).entries;
|
|
}
|
|
|
|
/** Rejects every unused export reported by Knip. */
|
|
function checkUnusedExports(output: string) {
|
|
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"),
|
|
};
|
|
}
|
|
|
|
/** Classifies Knip export output, rejecting findings after resolution failures. */
|
|
export function checkExportScan(scanName: string, output: string) {
|
|
const resolutionErrors = output
|
|
.split(/\r?\n/u)
|
|
.filter((line) => line.startsWith("ERROR: Error loading "));
|
|
if (resolutionErrors.length > 0) {
|
|
return {
|
|
ok: false,
|
|
entries: [],
|
|
message: [
|
|
`deadcode ${scanName} could not resolve workspace modules; export findings would be unreliable and are discarded.`,
|
|
...resolutionErrors,
|
|
"Install workspace dependencies in-tree (pnpm install) — on delegated boxes the crabbox wrapper links the hydrated modules dir into the workdir — and rerun.",
|
|
].join("\n"),
|
|
};
|
|
}
|
|
|
|
const check = checkUnusedExports(output);
|
|
return {
|
|
...check,
|
|
message: check.ok ? "" : `${scanName}:\n${check.message}`,
|
|
};
|
|
}
|
|
|
|
function reportUnusedExportScan(scan: (typeof KNIP_SCANS)[number], result: KnipRunResult) {
|
|
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 check = checkExportScan(scan.name, result.output);
|
|
if (!check.ok) {
|
|
console.error(check.message);
|
|
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;
|
|
}
|
|
|
|
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 runKnipScans(KNIP_SCANS, KNIP_COMMON_ARGS, reportUnusedExportScan);
|
|
}
|