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/unstable/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; } const collectStreamAsString = (stream: Stream.Stream): Effect.Effect => 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); }), ), ), ); }); }, }; };