1import { defineRule } from "@oxlint/plugins";
2import * as Option from "effect/Option";
3
4import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.ts";
5
6const TEST_FILE_PATTERN = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
7const EFFECT_RUNTIME_METHODS = new Set([
8 "runCallback",
9 "runCallbackWith",
10 "runFork",
11 "runForkWith",
12 "runPromise",
13 "runPromiseExit",
14 "runPromiseExitWith",
15 "runPromiseWith",
16 "runSync",
17 "runSyncExit",
18 "runSyncExitWith",
19 "runSyncWith",
20]);
21
22const readMaxOccurrences = (options: ReadonlyArray<unknown>): number => {
23 const [first] = options;
24 return typeof first === "object" &&
25 first !== null &&
26 "maxOccurrences" in first &&
27 typeof first.maxOccurrences === "number"
28 ? first.maxOccurrences
29 : 0;
30};
31
32const manualRunnerName = (callee: unknown): Option.Option<string> => {
33 const expression = unwrapExpression(callee);
34 if (Option.isNone(expression) || expression.value.type !== "MemberExpression") {
35 return Option.none();
36 }
37
38 const object = unwrapExpression(expression.value.object);
39 const property = getPropertyName(expression.value.property);
40 if (Option.isNone(property)) return Option.none();
41
42 if (isIdentifier(object, "Effect") && EFFECT_RUNTIME_METHODS.has(property.value)) {
43 return Option.some(`Effect.${property.value}`);
44 }
45
46 if (isIdentifier(object, "ManagedRuntime") && property.value === "make") {
47 return Option.some("ManagedRuntime.make");
48 }
49
50 return Option.none();
51};
52
53export default defineRule({
54 meta: {
55 type: "problem",
56 docs: {
57 description:
58 "Disallow manually creating or running Effect runtimes in tests; use @effect/vitest.",
59 },
60 schema: [
61 {
62 type: "object",
63 properties: {
64 maxOccurrences: {
65 type: "integer",
66 minimum: 0,
67 description:
68 "Legacy debt ceiling for this file: occurrences beyond this count are reported.",
69 },
70 },
71 additionalProperties: false,
72 },
73 ],
74 defaultOptions: [{ maxOccurrences: 0 }],
75 },
76 create(context) {
77 if (!TEST_FILE_PATTERN.test(context.filename)) return {};
78
79 const allowedCount = readMaxOccurrences(context.options);
80 let occurrenceCount = 0;
81
82 return {
83 CallExpression(node) {
84 const runner = manualRunnerName(node.callee);
85 if (Option.isNone(runner)) return;
86
87 occurrenceCount++;
88 if (occurrenceCount <= allowedCount) return;
89
90 context.report({
91 node: node.callee,
92 message: `Do not use ${runner.value} in tests. Use @effect/vitest with it.effect(...) and test layers instead.`,
93 });
94 },
95 };
96 },
97});