t3-code-android-nightly/oxlint-plugin-t3code/rules/no-test-in-loop.ts
Julius Marminge fd7ee2c30a
chore(lint): forbid declaring tests inside for loops (#14921)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 16:07:50 -07:00

79 lines
3.1 KiB
TypeScript

import { defineRule, type ESTree } from "@oxlint/plugins";
import * as Option from "effect/Option";
import { getPropertyName, unwrapExpression } from "../utils.ts";
const TEST_FILE_PATTERN = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
// node:test has no `.each`, so files written against it keep their loops.
const NODE_TEST_IMPORT_PATTERN = /["']node:test["']/u;
// Calls that declare a test or a group of tests; all of them expose `.each`.
const TEST_FUNCTIONS = new Set(["describe", "it", "suite", "test"]);
// Modifiers whose tester also exposes `.each` (@effect/vitest's `it.effect` and `it.live`).
const EACH_CAPABLE_MODIFIERS = new Set(["effect", "live"]);
const LOOPS = new Set(["ForStatement", "ForInStatement", "ForOfStatement"]);
const FUNCTIONS = new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
/** Returns the reported name for `it(…)`, `describe(…)`, `it.effect(…)`, and friends. */
const testCallName = (callee: unknown): Option.Option<string> => {
const expression = unwrapExpression(callee);
if (Option.isNone(expression)) return Option.none();
if (expression.value.type === "Identifier") {
return TEST_FUNCTIONS.has(expression.value.name)
? Option.some(expression.value.name)
: Option.none();
}
if (expression.value.type !== "MemberExpression") return Option.none();
const object = unwrapExpression(expression.value.object);
const property = getPropertyName(expression.value.property);
if (Option.isNone(object) || Option.isNone(property)) return Option.none();
if (!EACH_CAPABLE_MODIFIERS.has(property.value)) return Option.none();
if (object.value.type !== "Identifier" || !TEST_FUNCTIONS.has(object.value.name)) {
return Option.none();
}
return Option.some(`${object.value.name}.${property.value}`);
};
/**
* Reports a test declaration that runs once per loop iteration. A loop around a
* `describe` is reported on the `describe`, so the tests inside it stay quiet.
*/
const isInsideLoop = (node: ESTree.Node): boolean => {
let current = node.parent;
while (current) {
if (LOOPS.has(current.type)) return true;
// Anything inside a function (a describe body, a helper, a test body) runs
// once per call of that function, not once per iteration.
if (FUNCTIONS.has(current.type)) return false;
current = current.parent;
}
return false;
};
export default defineRule({
meta: {
type: "suggestion",
docs: {
description:
"Disallow declaring tests inside a for loop; use it.each / it.effect.each / describe.each instead.",
},
},
create(context) {
if (!TEST_FILE_PATTERN.test(context.filename)) return {};
if (NODE_TEST_IMPORT_PATTERN.test(context.sourceCode.text)) return {};
return {
CallExpression(node) {
const name = testCallName(node.callee);
if (Option.isNone(name)) return;
if (!isInsideLoop(node)) return;
context.report({
node: node.callee,
message: `Do not call ${name.value}(…) inside a for loop. Use ${name.value}.each(cases)(name, …) instead.`,
});
},
};
},
});