152 lines
5.0 KiB
JavaScript
152 lines
5.0 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("/");
|
|
// Record the actual checker result, including caller-selected thresholds.
|
|
report.reaCheck = { exitCode: result.status };
|
|
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;
|
|
}
|