Keep static feedback concise while retaining complete diagnostic reports, visible lint errors, module-boundary checks, and the 3% duplication gate. Upload complete reports after static failures and restore their outputs from Turbo caches. Preserve unambiguous, checkout-relative clone locations. Bind both lint caches to the website, documentation configuration and experiment sources actually scanned by oxlint, including ignore policy. Guard this ownership by comparing live checker discovery with resolved Turbo inputs. A real website syntax error now invalidates a cached pass. Validation: full current-head CI and the separate Go lane passed. Seven real-checker regressions passed; failure-status and stale-report mutations were rejected, and restored source passed. Actual Turbo restored both reports byte-for-byte; full static/docs/workflow checks passed. Closes #2238. Refs #2190. Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
150 lines
4.8 KiB
JavaScript
150 lines
4.8 KiB
JavaScript
import { spawnSync } from "node:child_process";
|
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { createRequire } from "node:module";
|
|
import { dirname, join, relative, sep } from "node:path";
|
|
import { z } from "zod";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const mode = process.argv[2];
|
|
const reportRoot = join(".cache", "static-checks");
|
|
const extraArguments = process.argv.slice(3);
|
|
const lintReport = z.object({
|
|
number_of_files: z.number(),
|
|
diagnostics: z.array(
|
|
z.object({
|
|
message: z.string(),
|
|
code: z.string().optional(),
|
|
severity: z.enum(["error", "warning", "advice"]),
|
|
filename: z.string(),
|
|
labels: z.array(
|
|
z.object({
|
|
span: z.object({ line: z.number(), column: z.number() }),
|
|
}),
|
|
),
|
|
}),
|
|
),
|
|
});
|
|
const duplicateReport = z
|
|
.object({
|
|
duplicates: z.array(
|
|
z
|
|
.object({
|
|
firstFile: z.object({ name: z.string() }).passthrough(),
|
|
secondFile: z.object({ name: z.string() }).passthrough(),
|
|
})
|
|
.passthrough(),
|
|
),
|
|
statistics: z
|
|
.object({
|
|
total: z
|
|
.object({
|
|
sources: z.number(),
|
|
clones: z.number(),
|
|
percentage: z.number(),
|
|
})
|
|
.passthrough(),
|
|
})
|
|
.passthrough(),
|
|
})
|
|
.passthrough();
|
|
|
|
const run = (packageName, entrypoint, arguments_) => {
|
|
const executable = join(
|
|
dirname(require.resolve(`${packageName}/package.json`)),
|
|
entrypoint,
|
|
);
|
|
const result = spawnSync(process.execPath, [executable, ...arguments_], {
|
|
encoding: "utf8",
|
|
maxBuffer: 32 * 1024 * 1024,
|
|
});
|
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
if (result.error !== undefined) throw result.error;
|
|
if (result.status === null)
|
|
throw new Error(`${packageName} terminated by ${result.signal}`);
|
|
return result;
|
|
};
|
|
|
|
try {
|
|
await mkdir(reportRoot, { recursive: true });
|
|
if (mode === "lint") {
|
|
const reportPath = join(reportRoot, "oxlint.json");
|
|
await rm(reportPath, { force: true });
|
|
const result = run("oxlint", "bin/oxlint", [
|
|
...extraArguments,
|
|
"--format",
|
|
"json",
|
|
]);
|
|
let report;
|
|
try {
|
|
report = lintReport.parse(JSON.parse(result.stdout));
|
|
} catch (cause) {
|
|
process.stderr.write(result.stdout);
|
|
throw cause;
|
|
}
|
|
await writeFile(reportPath, result.stdout);
|
|
const warnings = report.diagnostics.filter(
|
|
({ severity }) => severity === "warning",
|
|
);
|
|
const errors = report.diagnostics.filter(
|
|
({ severity }) => severity === "error",
|
|
);
|
|
console.log(
|
|
`Lint: ${report.number_of_files} files, ${errors.length} errors, ${warnings.length} warnings.`,
|
|
);
|
|
for (const diagnostic of errors) {
|
|
const location = diagnostic.labels[0]?.span;
|
|
console.error(
|
|
`${diagnostic.filename}:${location?.line ?? 1}:${location?.column ?? 1}: ${diagnostic.code ?? "lint"}: ${diagnostic.message}`,
|
|
);
|
|
}
|
|
const counts = new Map();
|
|
for (const { code } of warnings)
|
|
counts.set(code ?? "warning", (counts.get(code ?? "warning") ?? 0) + 1);
|
|
for (const [rule, count] of [...counts]
|
|
.sort(
|
|
(left, right) => right[1] - left[1] || left[0].localeCompare(right[0]),
|
|
)
|
|
.slice(0, 8))
|
|
console.log(` ${rule}: ${count} warnings`);
|
|
console.log(`Full diagnostics: ${reportPath}; npm run lint:details`);
|
|
process.exitCode = result.status;
|
|
} else if (mode === "duplicates") {
|
|
const output = join(reportRoot, "jscpd");
|
|
const reportPath = join(output, "jscpd-report.json");
|
|
await rm(reportPath, { force: true });
|
|
const result = run("jscpd", "run-jscpd.js", [
|
|
"src/",
|
|
"tests/",
|
|
...extraArguments,
|
|
"--reporters",
|
|
"json",
|
|
"--output",
|
|
output,
|
|
"--no-colors",
|
|
"--no-tips",
|
|
"--absolute",
|
|
]);
|
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
const report = duplicateReport.parse(
|
|
JSON.parse(await readFile(reportPath, "utf8")),
|
|
);
|
|
// jscpd otherwise strips each scan root, making src/ and tests/ names
|
|
// ambiguous. Normalize absolute producer paths once for portable reports.
|
|
for (const clone of report.duplicates)
|
|
for (const file of [clone.firstFile, clone.secondFile])
|
|
file.name = relative(process.cwd(), file.name).split(sep).join("/");
|
|
await writeFile(reportPath, JSON.stringify(report, null, 2));
|
|
const { sources, clones, percentage } = report.statistics.total;
|
|
console.log(
|
|
`Duplication: ${sources} files, ${clones} clones, ${percentage.toFixed(2)}% duplicated lines.`,
|
|
);
|
|
console.log(`Full diagnostics: ${reportPath}; npm run jscpd:details`);
|
|
process.exitCode = result.status;
|
|
} else {
|
|
throw new Error("Expected lint or duplicates");
|
|
}
|
|
} catch (cause) {
|
|
console.error(cause instanceof Error ? cause.message : String(cause));
|
|
process.exitCode = 1;
|
|
}
|