mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
310 lines
10 KiB
TypeScript
310 lines
10 KiB
TypeScript
import { parse } from "@babel/parser";
|
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { expect } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { writeRepositoryOxlintConfiguration } from "../../fixtures/oxlintConfiguration.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { processTest } from "../../support/process/processFixture.js";
|
|
|
|
const repository = fileURLToPath(new URL("../../../", import.meta.url));
|
|
const oxlint = join(repository, "node_modules/oxlint/bin/oxlint");
|
|
const applicationSource = "src/application/lintRuleAcceptance.ts";
|
|
const testSource = "tests/boundary/lintRuleAcceptance.test.ts";
|
|
|
|
interface AcceptanceCase {
|
|
readonly name: string;
|
|
readonly filename: string;
|
|
readonly source: string;
|
|
readonly exitCode: number;
|
|
readonly diagnostics: readonly {
|
|
readonly code?: string;
|
|
readonly severity: "error" | "warning";
|
|
readonly message?: string;
|
|
}[];
|
|
}
|
|
|
|
const cases: readonly AcceptanceCase[] = [
|
|
{
|
|
name: "enables imported Evidence JSON admission errors through the repository plugin",
|
|
filename: applicationSource,
|
|
source: `
|
|
import { createEvidence as record } from "../domain/evidence.js";
|
|
import { jsonValueSchema as json } from "../domain/jsonValue.js";
|
|
export const evidence = record(undefined, {}, { result: json.parse({ ready: true }) });
|
|
`,
|
|
exitCode: 1,
|
|
diagnostics: [
|
|
{ code: "rea(no-evidence-json-preadmission)", severity: "error" },
|
|
],
|
|
},
|
|
{
|
|
name: "recognizes canonical Evidence owners imported through file URLs",
|
|
filename: applicationSource,
|
|
source: `
|
|
import { createEvidence as record } from ${JSON.stringify(pathToFileURL(join(repository, "src/domain/evidence.js")).href)};
|
|
import { jsonValueSchema as json } from ${JSON.stringify(pathToFileURL(join(repository, "src/domain/jsonValue.js")).href)};
|
|
export const evidence = record(undefined, {}, { result: json.parse({ ready: true }) });
|
|
`,
|
|
exitCode: 1,
|
|
diagnostics: [
|
|
{ code: "rea(no-evidence-json-preadmission)", severity: "error" },
|
|
],
|
|
},
|
|
{
|
|
name: "allows semantic schema admission and stored JSON contracts",
|
|
filename: applicationSource,
|
|
source: `
|
|
import { z } from "zod";
|
|
import { createEvidence } from "../domain/evidence.js";
|
|
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
const semanticSchema = z.object({ ready: z.boolean() });
|
|
export const semantic = createEvidence(undefined, {}, { result: semanticSchema.parse({ ready: true }) });
|
|
const result = jsonValueSchema.parse({ ready: true });
|
|
export const stored = createEvidence(undefined, {}, { result });
|
|
`,
|
|
exitCode: 0,
|
|
diagnostics: [],
|
|
},
|
|
{
|
|
name: "enables positional catalog type errors through the repository plugin",
|
|
filename: applicationSource,
|
|
source: `
|
|
import { GPU_TOOL_CONTRACTS as contracts } from "../contracts/gpu/gpuToolContracts.js";
|
|
export type Selected = (typeof contracts)[0];
|
|
`,
|
|
exitCode: 1,
|
|
diagnostics: [
|
|
{ code: "rea(no-positional-tool-contract)", severity: "error" },
|
|
],
|
|
},
|
|
{
|
|
name: "enables duplicate fixture root teardown errors through the repository plugin",
|
|
filename: testSource,
|
|
source: `
|
|
import { afterEach } from "vitest";
|
|
import { rm } from "node:fs/promises";
|
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
|
const root = await createTestTempDirectory("rea-lint-rule-");
|
|
afterEach(() => rm(root, { recursive: true, force: true }));
|
|
`,
|
|
exitCode: 1,
|
|
diagnostics: [{ code: "rea(no-fixture-root-cleanup)", severity: "error" }],
|
|
},
|
|
{
|
|
name: "allows explicit root deletion outside fixture teardown",
|
|
filename: testSource,
|
|
source: `
|
|
import { rm } from "node:fs/promises";
|
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
|
const root = await createTestTempDirectory("rea-lint-rule-");
|
|
await rm(root, { recursive: true, force: true });
|
|
`,
|
|
exitCode: 0,
|
|
diagnostics: [],
|
|
},
|
|
{
|
|
name: "rejects an unreturned promise callback containing assertions",
|
|
filename: testSource,
|
|
source: `
|
|
import { expect, it } from "vitest";
|
|
it("unjoined assertion", () => {
|
|
Promise.resolve("ready").then(value => { expect(value).toBe("ready"); });
|
|
});
|
|
`,
|
|
exitCode: 1,
|
|
diagnostics: [
|
|
{ code: "vitest(valid-expect-in-promise)", severity: "error" },
|
|
],
|
|
},
|
|
{
|
|
name: "rejects focused tests through the repository Vitest configuration",
|
|
filename: testSource,
|
|
source: `
|
|
import { expect, it } from "vitest";
|
|
it.only("focused assertion", () => { expect("ready").toBe("ready"); });
|
|
`,
|
|
exitCode: 1,
|
|
diagnostics: [{ code: "vitest(no-focused-tests)", severity: "error" }],
|
|
},
|
|
{
|
|
name: "keeps native stored assertion limitations at warning severity",
|
|
filename: testSource,
|
|
source: `
|
|
import { expect, it } from "vitest";
|
|
it("joins a stored assertion", async () => {
|
|
const assertion = expect(Promise.reject(new Error("controlled"))).rejects.toThrow("controlled");
|
|
await assertion;
|
|
});
|
|
`,
|
|
exitCode: 0,
|
|
diagnostics: [{ code: "vitest(valid-expect)", severity: "warning" }],
|
|
},
|
|
{
|
|
name: "allows an assertion diagnostic message",
|
|
filename: testSource,
|
|
source: `
|
|
import { expect, it } from "vitest";
|
|
it("keeps diagnostic context", () => {
|
|
expect("ready", "controlled diagnostic context").toBe("ready");
|
|
});
|
|
`,
|
|
exitCode: 0,
|
|
diagnostics: [],
|
|
},
|
|
{
|
|
name: "rejects unused suppression directives through the repository configuration",
|
|
filename: testSource,
|
|
source: `
|
|
// oxlint-disable-next-line no-var
|
|
export const ready = true;
|
|
`,
|
|
exitCode: 1,
|
|
diagnostics: [
|
|
{
|
|
severity: "error",
|
|
message: "Unused oxlint-disable directive (no problems were reported).",
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const reportSchema = z.object({
|
|
diagnostics: z.array(
|
|
z.object({
|
|
code: z.string().optional(),
|
|
severity: z.enum(["error", "warning"]),
|
|
message: z.string(),
|
|
}),
|
|
),
|
|
});
|
|
|
|
const relocateImports = (scenario: AcceptanceCase, path: string): string => {
|
|
const source = scenario.source;
|
|
const parsed = parse(source, {
|
|
sourceType: "module",
|
|
plugins: ["typescript"],
|
|
});
|
|
const replacements = [];
|
|
for (const statement of parsed.program.body) {
|
|
if (
|
|
statement.type !== "ImportDeclaration" ||
|
|
!statement.source.value.startsWith(".")
|
|
)
|
|
continue;
|
|
const target = resolve(
|
|
repository,
|
|
dirname(scenario.filename),
|
|
statement.source.value,
|
|
);
|
|
const relativePath = relative(dirname(path), target);
|
|
const specifier = isAbsolute(relativePath)
|
|
? pathToFileURL(target).href
|
|
: relativePath.startsWith(".")
|
|
? relativePath.split(sep).join("/")
|
|
: `./${relativePath.split(sep).join("/")}`;
|
|
const { start, end } = statement.source;
|
|
if (
|
|
start === null ||
|
|
start === undefined ||
|
|
end === null ||
|
|
end === undefined
|
|
)
|
|
throw new Error("Authored lint fixture import has no source range");
|
|
replacements.push({
|
|
start,
|
|
end,
|
|
specifier,
|
|
});
|
|
}
|
|
let relocated = source;
|
|
for (const replacement of replacements.reverse())
|
|
relocated =
|
|
relocated.slice(0, replacement.start) +
|
|
JSON.stringify(replacement.specifier) +
|
|
relocated.slice(replacement.end);
|
|
return relocated;
|
|
};
|
|
|
|
for (const scenario of cases) {
|
|
processTest(scenario.name, async ({ processes }) => {
|
|
const project = await createTestTempDirectory("rea-lint-configuration-");
|
|
const configuration = await writeRepositoryOxlintConfiguration(project);
|
|
const path = join(project, scenario.filename);
|
|
await mkdir(dirname(path), { recursive: true });
|
|
await writeFile(path, relocateImports(scenario, path));
|
|
const result = await processes.run(
|
|
process.execPath,
|
|
[
|
|
oxlint,
|
|
"--config",
|
|
configuration,
|
|
"--threads",
|
|
"1",
|
|
"--format",
|
|
"json",
|
|
scenario.filename,
|
|
],
|
|
{
|
|
cwd: project,
|
|
timeoutMs: 15_000,
|
|
},
|
|
);
|
|
expect(result.signal, result.stderr).toBeNull();
|
|
expect(result.exitCode, result.stderr || result.stdout).toBe(
|
|
scenario.exitCode,
|
|
);
|
|
const { diagnostics } = reportSchema.parse(JSON.parse(result.stdout));
|
|
expect(
|
|
diagnostics.map(({ code, severity }) => ({ code, severity })),
|
|
).toEqual(
|
|
scenario.diagnostics.map(({ code, severity }) => ({ code, severity })),
|
|
);
|
|
for (const [index, expected] of scenario.diagnostics.entries()) {
|
|
if (expected.message !== undefined)
|
|
expect(diagnostics[index]?.message).toBe(expected.message);
|
|
}
|
|
});
|
|
}
|
|
|
|
processTest(
|
|
"preserves portable regex escapes while fixing unnecessary ones",
|
|
async ({ processes }) => {
|
|
const project = await createTestTempDirectory("rea-lint-regex-fix-");
|
|
const configuration = await writeRepositoryOxlintConfiguration(project);
|
|
const path = join(project, applicationSource);
|
|
await mkdir(dirname(path), { recursive: true });
|
|
await writeFile(
|
|
path,
|
|
String.raw`export const intentional = /[+\-]/u;
|
|
export const unnecessary = /\!/;
|
|
`,
|
|
);
|
|
|
|
const result = await processes.run(
|
|
process.execPath,
|
|
[
|
|
oxlint,
|
|
"--config",
|
|
configuration,
|
|
"--threads",
|
|
"1",
|
|
"--fix",
|
|
applicationSource,
|
|
],
|
|
{ cwd: project, timeoutMs: 15_000 },
|
|
);
|
|
expect(result.signal, result.stderr).toBeNull();
|
|
expect(result.exitCode, result.stderr || result.stdout).toBe(0);
|
|
|
|
const fixed = await readFile(path, "utf8");
|
|
expect(fixed).toContain(String.raw`export const intentional = /[+\-]/u;`);
|
|
expect(fixed).toContain("export const unnecessary = /!/;");
|
|
const advertised = /intentional\s*=\s*\/(.+)\/u/u.exec(fixed)?.[1];
|
|
if (advertised === undefined) throw new Error("Fixed regex was not found");
|
|
expect(() => new RegExp(advertised, "v")).not.toThrow();
|
|
},
|
|
);
|