mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-09 03:41:17 +02:00
178 lines
6.2 KiB
TypeScript
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);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
},
|
|
};
|
|
};
|