Skip to content

feat(eslint-plugin): add prefer-array-methods lint rule#358

Open
iornstein wants to merge 7 commits intomainfrom
GRO-1729/prefer-array-methods-lint-rule
Open

feat(eslint-plugin): add prefer-array-methods lint rule#358
iornstein wants to merge 7 commits intomainfrom
GRO-1729/prefer-array-methods-lint-rule

Conversation

@iornstein
Copy link
Contributor

@iornstein iornstein commented Feb 3, 2026

Summary

This adds a new ESLint rule, prefer-array-methods, that flags for loops without control flow breaks. This is to adhere with our typescript best practice to Favor array methods over for loops (when not breaking).

Also adds this rule to eslint-config for all TypeScript files.

Motivation

Array methods are more declarative and functional, and provide readability, clarity of intent, and method chaining.

However, if the loop can break early (e.g. for optimizing code), then it is preferred to use the for loop. This is because functional loops like map and forEach cannot be exited early. (some and every can exit early; however, using them to do so just for early exiting could lead to a lack of readability).

Examples

// Invalid
const usingForLoop: number[] = [];
for (const item of items) {
  usingForLoop.push(item * 2);
}

// Valid
const usingArrayMethod = items.map((item) => item * 2);
// Example 1. Valid: optimized for early exit
let totalPrice = 0;
let maxAllowedPrice = 500;
for (const [item, index] of items.entries()) {
  totalPrice += item.price;
  if (totalPrice >= maxAllowedPrice) break;
}

// Example 2. Valid: Skip items with continue.
for (const item of items) {
  if (!item.valid) continue;
  processItem(item);
}

// Alternative for Example 2 using array methods
items.forEach((item) => {
  if (!item.valid) {
    return; // slightly confusing, return doesn't end the forEach loop. For loop might be preferred.
  }
  processItem(item);
});

// Another alternative for Example 2 using array methods
items.forEach((item) => {
  if (item.valid) {
    processItem(item); // deeply nested of code rather than guard clause pattern can be less readable. For loop might be preferred.
  }
});

// Example 3. Valid: Return from enclosing function
function findItem(items: Item[]): Item | undefined {
  for (const item of items) {
    if (item.matches) return item;
  }
  return undefined;
}

Specialized Error Message For Await

In addition, this rule shows a specialized error message if the for loop has await in it. This is because await doesn't work in array methods like you think it would. For instance:

[1, 2, 3, 4, 5].forEach(async (item) => {
    await process(item);
  });

does not wait for process(1) to finish before moving on to process(2).

However; this is different than the behavior of

for (const item in [1, 2, 3, 4, 5]) {
    await process(item);
  }

which does not begin invoking process(2) until process(1) finishes.

In order to make sure engineers don't accidentally change behavior, we make this more verbose error message.

This specialized error message refers to potentially using forEachAsyncSequentially.

Later work will inventory the usages of that method to see if we can make calls in parallel instead

// Bad: Sequential await without control flow
for (const item of items) {
  await processItem(item); // Error with specialized message
}

// Good: Parallel execution when possible
await Promise.all(items.map((item) => processItem(item)));

// Good: Sequential when required
import { forEachAsyncSequentially } from "@clipboard-health/util-ts";

await forEachAsyncSequentially(items, async (item) => {
  await processItem(item);
});

Related

Add a new ESLint rule that flags for loops without control flow breaks
(break, continue, return), encouraging the use of array methods like
.forEach, .map, etc.

The rule:
- Flags `for` and `for...of` loops without outer-level control flow
- Allows loops with break/continue/return at the outer level
- Ignores control flow inside nested functions, inner loops, or switches
- Shows a specialized message for loops with `await`, suggesting
  `Promise.all()` or `forEachAsyncSequentially` from @clipboard-health/util-ts

Also adds the rule to eslint-config for all TypeScript files.

GRO-1729

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 3, 2026

📝 Walkthrough

Walkthrough

Adds a new ESLint rule "prefer-array-methods" (implementation, utilities, tests, README), registers it in the plugin and package.json, and enables the rule as an error for TypeScript files in the shared ESLint config when outside the core-utils monorepo.

Changes

Cohort / File(s) Summary
Rule Implementation & Utilities
packages/eslint-plugin/src/lib/rules/prefer-array-methods/index.ts, packages/eslint-plugin/src/lib/rules/ruleUtilities.ts
New rule module "prefer-array-methods" (ForStatement/ForOfStatement analysis) and a forAnyChildNode utility using @typescript-eslint/visitor-keys. Reports either preferArrayMethods or awaitInLoop.
Rule Tests & Documentation
packages/eslint-plugin/src/lib/rules/prefer-array-methods/index.spec.ts, packages/eslint-plugin/src/lib/rules/prefer-array-methods/README.md
Adds comprehensive unit tests covering valid/invalid sync/async and nested scenarios, and README with examples and configuration notes.
Plugin Registration
packages/eslint-plugin/src/index.ts, packages/eslint-plugin/package.json
Registers "prefer-array-methods" in exported rules and adds dependency @typescript-eslint/visitor-keys@8.33.0.
ESLint Config
packages/eslint-config/src/index.js
Appends an override for **/*.ts, **/*.tsx under the external-monorepo condition to set @clipboard-health/prefer-array-methods to "error".

Sequence Diagram(s)

sequenceDiagram
    participant Editor
    participant ESLint
    participant Plugin
    participant Rule
    participant VisitorKeys
    Editor->>ESLint: Save / Lint request
    ESLint->>Plugin: Load registered rules
    Plugin->>Rule: Invoke rule on ForStatement / ForOfStatement
    Rule->>VisitorKeys: get child keys / inspect children
    VisitorKeys-->>Rule: Return child property keys / nodes
    Rule-->>ESLint: Report findings (preferArrayMethods / awaitInLoop)
    ESLint-->>Editor: Present diagnostics
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the primary change: adding a new ESLint lint rule named prefer-array-methods.
Linked Issues check ✅ Passed The code changes fully implement the requirements from GRO-1729: creating a new ESLint rule that flags for/for-of loops without outer-level control-flow breaks and encourages array methods.
Out of Scope Changes check ✅ Passed All changes are in-scope: new rule implementation, tests, documentation, configuration, utility helpers, and dependency additions directly support the objective of creating the prefer-array-methods lint rule.
Description check ✅ Passed The PR description clearly explains the new prefer-array-methods rule, its purpose, motivation, examples, and the specialized async error message.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch GRO-1729/prefer-array-methods-lint-rule

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link

nx-cloud bot commented Feb 3, 2026

View your CI Pipeline Execution ↗ for commit 75bfb58

Command Status Duration Result
nx affected --configuration ci --parallel 3 --t... ✅ Succeeded 55s View ↗
nx build ai-rules ✅ Succeeded 5s View ↗

☁️ Nx Cloud last updated this comment at 2026-02-06 23:43:51 UTC

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/eslint-plugin/src/lib/rules/prefer-array-methods/index.ts`:
- Around line 34-53: The function hasAnOuterLevelControlFlowStatement currently
skips switch statements entirely which misses return/continue inside switch
cases; update it to traverse into switches but treat only break statements as
non-outer control flow. Concretely, keep the early-return for isFunction(node)
and isForLoop(node) to avoid nested functions/loops, but when
isSwitchStatement(node) is true call forAnyChildNode with a small wrapper
predicate that returns false for isBreakStatement(child) and otherwise delegates
to hasAnOuterLevelControlFlowStatement(child) so returns/continues inside switch
cases are detected while breaks are ignored.
🧹 Nitpick comments (2)
packages/eslint-plugin/src/lib/rules/prefer-array-methods/index.ts (2)

12-25: Prefer function declarations over const arrows for helpers.

To align with the TS style guide, switch the helper predicates (and the inner reportIfForLoopNotAllowed) to function declarations.

🔧 Example refactor
-const isFunction = (node: TSESTree.Node): boolean =>
-  node.type === AST_NODE_TYPES.ArrowFunctionExpression ||
-  node.type === AST_NODE_TYPES.FunctionExpression ||
-  node.type === AST_NODE_TYPES.FunctionDeclaration;
+function isFunction(node: TSESTree.Node): boolean {
+  return (
+    node.type === AST_NODE_TYPES.ArrowFunctionExpression ||
+    node.type === AST_NODE_TYPES.FunctionExpression ||
+    node.type === AST_NODE_TYPES.FunctionDeclaration
+  );
+}
As per coding guidelines, Use `function` keyword for function declarations instead of `const` in TypeScript.

Also applies to: 89-109


72-115: Add explicit types for create(context) and its return.

Typing the context parameter and return type makes the rule contract explicit and matches the TS guidelines.

🔧 Example typing
-import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
+import { AST_NODE_TYPES, type TSESLint, type TSESTree } from "@typescript-eslint/utils";
+
+type MessageIds = "preferArrayMethods" | "awaitInLoop";
+type Options = [];
@@
-  create(context) {
+  create(context: TSESLint.RuleContext<MessageIds, Options>): TSESLint.RuleListener {
As per coding guidelines, Explicitly define parameter types and return types on functions in TypeScript.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@packages/eslint-plugin/src/lib/rules/prefer-array-methods/index.spec.ts`:
- Around line 47-54: Add a new valid test case to the test cases array in
packages/eslint-plugin/src/lib/rules/prefer-array-methods/index.spec.ts to cover
`continue` inside a switch (similar to the existing "return inside switch"
cases); specifically add an entry named like "for...of loop with continue inside
switch statement" whose code is `for (const item of items) { switch (item.type)
{ case 'a': continue; } process(item); }` so the test suite will detect the
current bug where `continue` inside `switch` is not treated as allowing the
loop.

In `@packages/eslint-plugin/src/lib/rules/prefer-array-methods/index.ts`:
- Around line 31-45: The current hasOuterLevelBreakOrContinue treats
isSwitchStatement as a hard stop so BreakStatements are ignored (correct) but
ContinueStatements inside switches are also ignored (wrong); change the logic so
switch nodes are traversed but with BreakStatements suppressed only within the
switch. Concretely: remove the early return for isSwitchStatement in
hasOuterLevelBreakOrContinue, and add a special-case branch for nodes where
isSwitchStatement(node) is true that calls forAnyChildNode over the switch's
children while treating AST_NODE_TYPES.BreakStatement as non-exiting (return
false) but still allowing AST_NODE_TYPES.ContinueStatement to return true; keep
the existing early stops for isFunction and isForLoop. Use the existing helpers
hasOuterLevelBreakOrContinue, forAnyChildNode, isSwitchStatement, isFunction,
isForLoop and the AST_NODE_TYPES constants to locate and implement this change.
- Around line 98-102: The awaitInLoop message in the messages object embeds a
newline plus leading spaces due to the multi-line template literal (symbols:
messages, awaitInLoop, STYLE_GUIDE_URL), causing unwanted whitespace in lint
output; fix it by replacing the multi-line template literal with a single-line
template string or explicit concatenation (e.g., join the lines or use + to
combine strings) so the rendered message contains no embedded
newline/indentation while still including STYLE_GUIDE_URL and the same content;
update the messages.awaitInLoop value accordingly.
🧹 Nitpick comments (1)
packages/eslint-plugin/src/lib/rules/prefer-array-methods/index.ts (1)

12-24: Predicate helpers use arrow functions instead of function declarations.

Per your coding guidelines, prefer function keyword for function declarations. These three predicates (isFunction, isForLoop, isSwitchStatement) are file-scoped helpers and would be more consistent as function declarations. This is a minor style nit given they're small predicates.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/eslint-plugin/src/lib/rules/prefer-array-methods/index.ts`:
- Around line 108-110: Add an explicit return type to the function
hasABreakInControlThatWouldStopOuterForLoop — it currently lacks a return
annotation while other functions do. Update the signature of
hasABreakInControlThatWouldStopOuterForLoop to include the appropriate return
type (boolean) so it reads like the other helpers and matches the return type of
containsNodesThatWouldStopOuterForLoop.
🧹 Nitpick comments (3)
packages/eslint-plugin/src/lib/rules/prefer-array-methods/index.ts (3)

52-85: Solid refactoring of the control-flow traversal logic.

The single-pass approach with narrowing nodeTypesThatCauseOuterLoopToStopEarly at each boundary is clean and correctly handles the nuanced semantics:

  • Functions: stop all control-flow detection ✓
  • Inner for loops: only propagate return
  • Switch: filter out break but propagate continue/return

Nit: parameter node on line 76 shadows the outer function parameter.

Consider renaming to child for consistency with line 82.

-    return forAnyChildNode(node, (node: TSESTree.Node): boolean =>
-      containsNodesThatWouldStopOuterForLoop(node, nodeTypesWithoutBreakStatement),
+    return forAnyChildNode(node, (child: TSESTree.Node): boolean =>
+      containsNodesThatWouldStopOuterForLoop(child, nodeTypesWithoutBreakStatement),
     );

56-58: Type assertion as ControlFlowType can be avoided.

Per coding guidelines, type assertions should be avoided unless absolutely necessary. You can use a type-safe check instead:

Suggested fix
-  if (nodeTypesThatCauseOuterLoopToStopEarly.includes(node.type as ControlFlowType)) {
+  if (
+    (nodeTypesThatCauseOuterLoopToStopEarly as readonly string[]).includes(node.type)
+  ) {

Or alternatively, use .some():

-  if (nodeTypesThatCauseOuterLoopToStopEarly.includes(node.type as ControlFlowType)) {
+  if (nodeTypesThatCauseOuterLoopToStopEarly.some((type) => type === node.type)) {

The .some() approach avoids all assertions entirely. As per coding guidelines, "Avoid type assertions (as, !) in TypeScript unless absolutely necessary".


9-10: Consider whether a Notion link is stable enough for lint rule messages.

If the Notion page is reorganized, renamed, or access-restricted, the link in every lint violation will be broken. You might want to point to a more permanent location (e.g., a markdown file in the repo or a redirect URL you control).

Comment on lines +108 to +110
function hasABreakInControlThatWouldStopOuterForLoop(node: TSESTree.Node) {
return containsNodesThatWouldStopOuterForLoop(node, ALL_CONTROL_FLOW_TYPES);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Missing explicit return type annotation.

All other functions in this file declare their return types. This one is missing it.

-function hasABreakInControlThatWouldStopOuterForLoop(node: TSESTree.Node) {
+function hasABreakInControlThatWouldStopOuterForLoop(node: TSESTree.Node): boolean {

As per coding guidelines, "Explicitly define parameter types and return types on functions in TypeScript".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function hasABreakInControlThatWouldStopOuterForLoop(node: TSESTree.Node) {
return containsNodesThatWouldStopOuterForLoop(node, ALL_CONTROL_FLOW_TYPES);
}
function hasABreakInControlThatWouldStopOuterForLoop(node: TSESTree.Node): boolean {
return containsNodesThatWouldStopOuterForLoop(node, ALL_CONTROL_FLOW_TYPES);
}
🤖 Prompt for AI Agents
In `@packages/eslint-plugin/src/lib/rules/prefer-array-methods/index.ts` around
lines 108 - 110, Add an explicit return type to the function
hasABreakInControlThatWouldStopOuterForLoop — it currently lacks a return
annotation while other functions do. Update the signature of
hasABreakInControlThatWouldStopOuterForLoop to include the appropriate return
type (boolean) so it reads like the other helpers and matches the return type of
containsNodesThatWouldStopOuterForLoop.

@iornstein iornstein requested a review from a team February 7, 2026 00:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant