oxc-project/oxc · warning
Enforce using `each` rather than manual loops
Error message
Enforce using `each` rather than manual loops
What it means
This is the oxlint `prefer-each` rule (jest/vitest plugin). It reports `for`/`for-in`/`for-of` loops whose body directly contains a Jest test call (`test`/`it`, or `describe`/hooks), because parameterized loops are better expressed with `test.each`/`describe.each`. The loop is skipped when it is itself nested inside a test callback (a runtime loop within one test is legitimate). The file-scope `should_run` gate skips files with no loop statement at all, so the rule is cheap.
Source
Thrown at crates/oxc_linter/src/rules/shared/jest_vitest/prefer_each.rs:13
use oxc_ast::{AstKind, AstType};
use oxc_diagnostics::OxcDiagnostic;
use oxc_semantic::{AstNode, AstTypesBitset, NodeId};
use oxc_span::{GetSpan, Span};
use rustc_hash::FxHashSet;
use crate::{
context::{ContextHost, LintContext},
utils::{JestFnKind, JestGeneralFnKind, PossibleJestNode, parse_jest_fn_call},
};
fn use_prefer_each(span: Span, fn_name: &str) -> OxcDiagnostic {
OxcDiagnostic::warn("Enforce using `each` rather than manual loops")
.with_help(format!("Prefer using `{fn_name}.each` rather than a manual loop."))
.with_label(span)
}
#[inline]
fn is_in_test(ctx: &LintContext<'_>, id: NodeId) -> bool {
ctx.nodes().ancestors(id).any(|node| {
let AstKind::CallExpression(ancestor_call_expr) = node.kind() else { return false };
let Some(ancestor_member_expr) = ancestor_call_expr.callee.as_member_expression() else {
return false;
};
let Some(id) = ancestor_member_expr.object().get_identifier_reference() else {
return false;
};
matches!(JestFnKind::from(id.name.as_str()), JestFnKind::General(JestGeneralFnKind::Test))
})
}View on GitHub (pinned to e1e7af627c)
Solutions
- Replace the loop with `describe.each(items)('item %j', (item) => { ... })`.
- For per-case tests use `it.each(items)('works for %j', (item) => { ... })` or the template-table form `it.each([[a,b],[c,d]])('%s + %s', ...)`.
- Keep the loop only if each iteration must run inside a single test (e.g. assertions share state) — the rule does not fire for loops inside a test callback.
Example fix
// before
for (const item of items) {
it(`handles ${item}`, () => {
expect(handle(item)).toBeTruthy();
});
}
// after
it.each(items)('handles %s', (item) => {
expect(handle(item)).toBeTruthy();
}); Defensive patterns
Strategy: validation
Validate before calling
// .oxlintrc.json
{ "rules": { "jest/prefer-each": "error" } }
npx oxlint tests/ Prevention
- Reach for it.each/describe.each first when generating tests from data.
- Keep loops only inside a single test body where iteration is runtime behavior.
- Let CI flag generated describe-loops before they spread through a suite.
When it happens
Trigger: A `for (const item of items) { describe(item, ...) }`, `for (...) { it(...) }`, or `for-in` loop at describe/module scope that directly (not nested inside another call) contains a test-family jest call; the diagnostic spans from the loop keyword to the start of its body.
Common situations: Data-driven test suites written before `.each` syntax was known; loops over config objects generating a describe per entry; teams migrating from Mocha where loops were the only parameterization option.
Related errors
- `expect` must be inside of a test block.
- Jest tests should not return a value
- Unnecessary async function wrapper
- Suggest using `toBeCalledWith()` or `toHaveBeenCalledWith()`
- Suggest using the built-in equality matchers.
AI-assisted analysis of oxc-project/oxc@e1e7af627c (2026-08-20).
Data as JSON: /api/errors/3a5c5074b34ab618.
Report an issue: GitHub.