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

  1. Replace the loop with `describe.each(items)('item %j', (item) => { ... })`.
  2. 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', ...)`.
  3. 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

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


AI-assisted analysis of oxc-project/oxc@e1e7af627c (2026-08-20). Data as JSON: /api/errors/3a5c5074b34ab618. Report an issue: GitHub.