openclaw/scripts/check-max-lines-ratchet.mts
Peter Steinberger 567d7d393c
refactor(scripts): deslop root scripts a–k (#159199)
Consolidate repeated CLI parsing, scanning, benchmark and release projections through their existing owners. Preserve script and CI contracts.

Fix malformed installer version diagnostics, flat ClawHub artifact sealing, completed polling sleep listener retention, and metadata output symlink confinement.
2026-09-27 01:08:19 -07:00

308 lines
9.9 KiB
TypeScript

import { execFileSync, spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import type * as ts from "typescript/unstable/ast";
import {
addEnvVarNames,
isCountedSourcePath,
main as checkEnvVarCount,
} from "./check-env-var-count.mts";
import { reportLimitViolations } from "./lib/check-limits.mts";
import { createNativeTypeScriptParser } from "./lib/native-typescript.mts";
import {
compareRatchetSets,
listRatchetRenames,
loadRatchetReference,
loadRatchetSnapshot,
loadRatchetSources,
parseRatchetArgs,
parseRatchetPaths,
reportRatchetFailures,
reportRatchetSuccess,
resolveRatchetBase,
} from "./lib/shrink-ratchet.mts";
import { collectTypeScriptCommentRanges } from "./lib/ts-guard-utils.mts";
const BASELINE_PATH = "config/max-lines-baseline.txt";
const GIT_MAX_BUFFER = 256 * 1024 * 1024;
const SOURCE_ROOTS = ["src", "ui/src", "packages", "extensions"];
const SOURCE_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts"]);
const BASELINE_HEADER = [
"# Files currently allowed to exceed the oxlint max-lines budget.",
"# Ratchet: this list may only shrink. Split files; never add entries.",
"# Existing suppressions carry a TODO at the file site.",
"",
].join("\n");
const compareStrings = (left: string, right: string) => left.localeCompare(right);
export function isGovernedSourcePath(filePath: string) {
const normalized = filePath.replaceAll("\\", "/");
if (!SOURCE_ROOTS.some((root) => normalized === root || normalized.startsWith(root + "/"))) {
return false;
}
if (!SOURCE_EXTENSIONS.has(path.posix.extname(normalized))) {
return false;
}
return !(
normalized.startsWith("ui/src/i18n/locales/") ||
normalized.startsWith("src/wizard/i18n/locales/") ||
/(?:^|\/)(?:__generated__|generated|protocol-gen|dist)(?:\/|$)/u.test(normalized) ||
/\.generated\.[^/]+$/u.test(normalized)
);
}
export function collectLintDisableDirectives(
source: string,
_filePath: string,
sourceFile: ts.SourceFile,
) {
if (!source.includes("oxlint-disable") && !source.includes("eslint-disable")) {
return [];
}
const directive = /^(?:eslint|oxlint)-disable(?:-next-line|-line)?(?=$|\s)([\s\S]*)$/u;
const directives: string[][] = [];
for (const range of collectTypeScriptCommentRanges(sourceFile)) {
const text = source.slice(range.pos, range.end);
const comment = text.slice(2, text.startsWith("/*") ? -2 : undefined);
const match = directive.exec(comment.trim());
if (!match) {
continue;
}
const directiveBody = match[1] ?? "";
const reason = /--|(?<=\s)-(?=\s)/u.exec(directiveBody);
const rules = (reason ? directiveBody.slice(0, reason.index) : directiveBody).trim();
directives.push(rules === "" ? [] : rules.split(/[\s,]+/u));
}
return directives;
}
export function isMaxLinesRule(rule: string) {
return rule === "max-lines" || rule.endsWith("/max-lines");
}
function baselineWithVerifiedRenames(
root: string,
baseRef: string,
staged: boolean,
baseline: ReadonlySet<string>,
baseBaseline: ReadonlySet<string>,
) {
const allowed = new Set(baseBaseline);
for (const { from, to } of listRatchetRenames(root, baseRef, staged, SOURCE_ROOTS)) {
if (baseBaseline.has(from) && !baseline.has(from) && baseline.has(to)) {
allowed.delete(from);
allowed.add(to);
}
}
return allowed;
}
function listStagedSuppressionCandidates(root: string) {
// The staged policy covers the whole index. Narrow candidates once so a one-file
// check does not spawn a Git process for every governed source.
const result = spawnSync(
"git",
[
"grep",
"--cached",
"-z",
"-l",
"-e",
"oxlint-disable",
"-e",
"eslint-disable",
"--",
...SOURCE_ROOTS,
],
{ cwd: root, maxBuffer: GIT_MAX_BUFFER },
);
if (result.status === 1) {
return [];
}
if (result.status !== 0) {
throw new Error(result.stderr.toString("utf8").trim() || "git grep failed");
}
return result.stdout.toString("utf8").split("\0").filter(Boolean);
}
export function collectCurrentSuppressionState(
root = process.cwd(),
options: { staged?: boolean; envVarNames?: Map<string, ReadonlySet<string>> } = {},
) {
using parser = createNativeTypeScriptParser({ cwd: root });
const staged = options.staged === true;
const filePaths = staged
? listStagedSuppressionCandidates(root)
: execFileSync(
"git",
["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...SOURCE_ROOTS],
{ cwd: root, maxBuffer: GIT_MAX_BUFFER },
)
.toString("utf8")
.split("\0");
const governedPaths = filePaths
.filter(Boolean)
.filter(isGovernedSourcePath)
.filter((filePath) => staged || fs.existsSync(path.join(root, filePath)));
const stagedSources = staged ? loadRatchetSources(root, governedPaths) : undefined;
const allRules: string[] = [];
const explicit: string[] = [];
for (const filePath of governedPaths) {
const source = stagedSources
? stagedSources.get(filePath)!
: fs.readFileSync(path.join(root, filePath), "utf8");
if (!staged && options.envVarNames && isCountedSourcePath(filePath)) {
const names = new Set<string>();
addEnvVarNames(source, names);
options.envVarNames.set(filePath, names);
}
if (!source.includes("oxlint-disable") && !source.includes("eslint-disable")) {
continue;
}
const directives = collectLintDisableDirectives(
source,
filePath,
parser.parseSourceFile(filePath, source),
);
if (directives.some((rules) => rules.length === 0)) {
allRules.push(filePath);
}
if (directives.some((rules) => rules.some(isMaxLinesRule))) {
explicit.push(filePath);
}
}
return {
allRules: allRules.toSorted(compareStrings),
explicit: explicit.toSorted(compareStrings),
};
}
function writeBaseline(root: string, entries: string[]) {
fs.writeFileSync(path.join(root, BASELINE_PATH), BASELINE_HEADER + entries.join("\n") + "\n");
}
function envVarCountArgs(argv: string[]) {
const args = parseRatchetArgs(argv);
return [...(args.staged ? ["--staged"] : []), ...(args.base ? ["--base", args.base] : [])];
}
export function main(
root = process.cwd(),
argv: string[] = process.argv.slice(2),
envVarNames?: Map<string, ReadonlySet<string>>,
) {
try {
const args = parseRatchetArgs(argv);
if (args.staged && args.prune) {
throw new Error("--prune cannot be combined with --staged");
}
let baseline;
try {
baseline = loadRatchetSnapshot(root, BASELINE_PATH, args.staged, parseRatchetPaths);
} catch {
throw new Error("Missing " + BASELINE_PATH + (args.staged ? " in the index" : ""));
}
const { allRules, explicit: current } = collectCurrentSuppressionState(root, {
staged: args.staged,
envVarNames,
});
const { added, removed: stale } = compareRatchetSets(current, baseline, compareStrings);
const baseRef = resolveRatchetBase(root, { base: args.base, staged: args.staged });
const baseBaseline = baseRef
? loadRatchetReference(root, baseRef, BASELINE_PATH, parseRatchetPaths)
: null;
const allowedBaseline =
baseRef && baseBaseline
? baselineWithVerifiedRenames(root, baseRef, args.staged, baseline, baseBaseline)
: baseBaseline;
const expanded = allowedBaseline
? compareRatchetSets(baseline, allowedBaseline, compareStrings).added
: [];
if (
reportRatchetFailures([
{
entries: allRules,
title: "All-rule lint disables are forbidden; name only the required rules:",
},
])
) {
return 1;
}
if (
reportLimitViolations([
...added.map((file) => ({
file,
title: "New max-lines suppressions are forbidden; split these files:",
message: "Remove the new max-lines suppression and split the file.",
})),
...expanded.map((file) => ({
file: BASELINE_PATH,
title: "The max-lines baseline may only shrink; remove these entries:",
message: file,
})),
])
) {
return 1;
}
if (args.prune) {
const kept = [...baseline]
.filter((entry) => current.includes(entry))
.toSorted(compareStrings);
writeBaseline(root, kept);
reportRatchetSuccess(
"Pruned " + BASELINE_PATH + ": " + baseline.size + " -> " + kept.length + ".",
);
return 0;
}
if (
reportLimitViolations(
stale.map((file) => ({
file: BASELINE_PATH,
title: "Remove stale max-lines baseline entries (or run with --prune):",
message: file,
})),
)
) {
return 1;
}
if (added.length + expanded.length + stale.length === 0) {
reportRatchetSuccess(
"max-lines ratchet OK: " + current.length + " grandfathered suppressions.",
);
}
return 0;
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
return 1;
}
}
function runBaselineRatchets(root = process.cwd(), argv: string[] = process.argv.slice(2)) {
// Keep name sets local to this invocation, including files with no matches.
const envVarNames = argv.includes("--staged")
? undefined
: new Map<string, ReadonlySet<string>>();
const maxLinesStatus = main(root, argv, envVarNames);
if (maxLinesStatus !== 0) {
return maxLinesStatus;
}
try {
// CI invokes this entry with its frozen fork-point ref. Carry the same snapshot
// into the env budget so every baseline ratchet judges one tested tree.
checkEnvVarCount(envVarCountArgs(argv), root, envVarNames);
return 0;
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
return 1;
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
process.exitCode = runBaselineRatchets();
}