t3-code-android-nightly/oxlint-plugin-t3code/rules/no-manual-effect-runtime-in-tests.ts
Theo Browne ddcd310282
chore: docs, dev scripts and CI catch up with orchestration V2 (#15041)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 22:25:00 -07:00

100 lines
2.8 KiB
TypeScript

import { defineRule } from "@oxlint/plugins";
import * as Option from "effect/Option";
import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.ts";
const TEST_FILE_PATTERN = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
const EFFECT_RUNTIME_METHODS = new Set([
"runCallback",
"runCallbackWith",
"runFork",
"runForkWith",
"runPromise",
"runPromiseExit",
"runPromiseExitWith",
"runPromiseWith",
"runSync",
"runSyncExit",
"runSyncExitWith",
"runSyncWith",
]);
// The lint config can set `maxOccurrences` per file to track existing manual
// runners as debt. The rule permits no net-new occurrences in those files,
// while every other test file must have zero.
const readMaxOccurrences = (options: ReadonlyArray<unknown>): number => {
const [first] = options;
return typeof first === "object" &&
first !== null &&
"maxOccurrences" in first &&
typeof first.maxOccurrences === "number"
? first.maxOccurrences
: 0;
};
const manualRunnerName = (callee: unknown): Option.Option<string> => {
const expression = unwrapExpression(callee);
if (Option.isNone(expression) || expression.value.type !== "MemberExpression") {
return Option.none();
}
const object = unwrapExpression(expression.value.object);
const property = getPropertyName(expression.value.property);
if (Option.isNone(property)) return Option.none();
if (isIdentifier(object, "Effect") && EFFECT_RUNTIME_METHODS.has(property.value)) {
return Option.some(`Effect.${property.value}`);
}
if (isIdentifier(object, "ManagedRuntime") && property.value === "make") {
return Option.some("ManagedRuntime.make");
}
return Option.none();
};
export default defineRule({
meta: {
type: "problem",
docs: {
description:
"Disallow manually creating or running Effect runtimes in tests; use @effect/vitest.",
},
schema: [
{
type: "object",
properties: {
maxOccurrences: {
type: "integer",
minimum: 0,
description:
"Legacy debt ceiling for this file: occurrences beyond this count are reported.",
},
},
additionalProperties: false,
},
],
defaultOptions: [{ maxOccurrences: 0 }],
},
create(context) {
if (!TEST_FILE_PATTERN.test(context.filename)) return {};
const allowedCount = readMaxOccurrences(context.options);
let occurrenceCount = 0;
return {
CallExpression(node) {
const runner = manualRunnerName(node.callee);
if (Option.isNone(runner)) return;
occurrenceCount++;
if (occurrenceCount <= allowedCount) return;
context.report({
node: node.callee,
message: `Do not use ${runner.value} in tests. Use @effect/vitest with it.effect(...) and test layers instead.`,
});
},
};
},
});