t3-code-android-nightly/oxlint-plugin-t3code/test/utils.ts
Julius Marminge 194c73f3f9
chore(deps): upgrade Effect to stable 4.0.1 (#16138)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:15:14 -07:00

178 lines
6.2 KiB
TypeScript

import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as Data from "effect/Data";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as PlatformError from "effect/PlatformError";
import * as Predicate from "effect/Predicate";
import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";
import { ChildProcess, ChildProcessSpawner } from "effect/process";
import * as NodeModule from "node:module";
// oxlint is only a transitive dependency (via vite-plus), so its bin placement
// varies by package manager: pnpm hoists it into the virtual store, while
// other layouts expose vite-plus's LSP-only wrapper instead. Resolve the real
// package through vite-plus rather than hardcoding either layout, and do it at
// module scope so a broken install fails once with a resolution error instead
// of as an opaque defect in every test.
const oxlintPackageJsonPath = NodeModule.createRequire(
NodeModule.createRequire(import.meta.url).resolve("vite-plus/package.json"),
).resolve("oxlint/package.json");
class OxlintFixtureFailure extends Data.TaggedError("OxlintFixtureFailure")<{
readonly exitCode: number;
readonly stdout: string;
readonly stderr: string;
}> {
static readonly is = (u: unknown): u is OxlintFixtureFailure =>
Predicate.isTagged(u, "OxlintFixtureFailure");
}
class OxlintFixtureExpectedFailure extends Data.TaggedError("OxlintFixtureExpectedFailure")<{
readonly ruleName: string;
}> {
override get message() {
return `Expected oxlint to report a failure for rule ${this.ruleName}, but it passed.`;
}
}
const encodeOxlintConfig = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
interface RuleHarness {
readonly run: (
source: string,
) => Effect.Effect<
string,
OxlintFixtureFailure | PlatformError.PlatformError | Schema.SchemaError,
NodeServices.NodeServices
>;
readonly runAndExpectFailure: (
source: string,
) => Effect.Effect<
string,
OxlintFixtureExpectedFailure | PlatformError.PlatformError | Schema.SchemaError,
NodeServices.NodeServices
>;
readonly valid: (name: string, source: string) => void;
readonly invalid: (name: string, source: string, assertion?: (output: string) => void) => void;
}
interface RuleHarnessOptions {
readonly filename?: string;
/** Rule options, as they would appear after the severity in the lint config. */
readonly ruleOptions?: ReadonlyArray<unknown>;
}
const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> =>
stream.pipe(
Stream.decodeText(),
Stream.runFold(
() => "",
(acc, chunk) => acc + chunk,
),
);
const spawnAndCollectOutput = Effect.fnUntraced(function* (command: ChildProcess.Command) {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const child = yield* spawner.spawn(command);
const [stdout, stderr, exitCode] = yield* Effect.all(
[
collectStreamAsString(child.stdout),
collectStreamAsString(child.stderr),
child.exitCode.pipe(Effect.map(Number)),
],
{ concurrency: "unbounded" },
);
return { exitCode, stdout, stderr };
}, Effect.scoped);
export const createOxlintRuleHarness = (
ruleName: string,
options: RuleHarnessOptions = {},
): RuleHarness => {
const [pluginName, shortRuleName] = ruleName.split("/");
const diagnosticRuleName =
pluginName && shortRuleName ? `${pluginName}\\(${shortRuleName}\\)` : ruleName;
const test = it.layer(NodeServices.layer);
const run: RuleHarness["run"] = Effect.fnUntraced(function* (source: string) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const fixtureDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-oxlint-" });
const configPath = path.join(fixtureDir, ".oxlintrc.json");
const sourcePath = path.join(fixtureDir, options.filename ?? "fixture.ts");
const repoRoot = path.join(import.meta.dirname, "..", "..");
const oxlintBin = path.join(path.dirname(oxlintPackageJsonPath), "bin", "oxlint");
const pluginPath = path.join(repoRoot, "oxlint-plugin-t3code", "index.ts");
yield* fs.writeFileString(
configPath,
yield* encodeOxlintConfig({
jsPlugins: [{ name: "t3code", specifier: pluginPath }],
rules: { [ruleName]: ["error", ...(options.ruleOptions ?? [])] },
}),
);
yield* fs.makeDirectory(path.dirname(sourcePath), { recursive: true });
yield* fs.writeFileString(sourcePath, source);
// Run through the current Node binary: oxlint's bin is an extensionless
// shebang script, which Windows cannot spawn directly and which would
// otherwise pick up whatever node is first on PATH.
const output = yield* spawnAndCollectOutput(
ChildProcess.make(process.execPath, [oxlintBin, "--config", configPath, sourcePath], {
cwd: repoRoot,
}),
);
if (output.exitCode !== 0) {
return yield* new OxlintFixtureFailure({
exitCode: output.exitCode,
stdout: output.stdout,
stderr: output.stderr,
});
}
return `${output.stdout}${output.stderr}`;
}, Effect.scoped);
const runAndExpectFailure: RuleHarness["runAndExpectFailure"] = (source) =>
run(source).pipe(
Effect.matchEffect({
onFailure: (error) =>
OxlintFixtureFailure.is(error)
? Effect.succeed(
`oxlint fixture failed with exit code ${error.exitCode}\n${error.stdout}\n${error.stderr}`,
)
: Effect.fail(error),
onSuccess: () => Effect.fail(new OxlintFixtureExpectedFailure({ ruleName })),
}),
);
return {
run,
runAndExpectFailure,
valid(name, source) {
test(name, (it) => {
it.effect("passes", () => run(source));
});
},
invalid(name, source, assertion) {
test(name, (it) => {
it.effect("reports the rule diagnostic", () =>
runAndExpectFailure(source).pipe(
Effect.tap((output) =>
Effect.sync(() => {
assert.match(output, new RegExp(diagnosticRuleName));
assertion?.(output);
}),
),
),
);
});
},
};
};