Skip to content

eslint-factory: require-lastindex-reset-before-global-exec-loop false positive on loops that always exhaust naturally (temporary [Content truncated due to length] #53592

Description

@github-actions

Summary

require-lastindex-reset-before-global-exec-loop (eslint-factory/src/rules/require-lastindex-reset-before-global-exec-loop.ts) flags any while ((m = RE.exec(str)) !== null) loop over a module-scoped g/y regex that lacks a textual RE.lastIndex = reset earlier in the enclosing function — even when the loop body can never exit early (no break/return/throw inside it). In that case the loop always runs to natural exhaustion, and JS automatically resets lastIndex to 0 when .exec() returns null from running out of matches. There is nothing to fix, so the warning is a false positive.

Grounded false positive

actions/setup/js/temporary_id.cjs:637-653, function extractTemporaryIdReferences:

function extractTemporaryIdReferences(message) {
  const tempIds = new Set();
  if (!message || typeof message !== "object") return tempIds;

  const textFields = ["body", "title", "description"];
  for (const field of textFields) {
    if (typeof message[field] === "string") {
      let match;
      while ((match = TEMPORARY_ID_PATTERN.exec(message[field])) !== null) {
        tempIds.add(normalizeTemporaryId(match[1]));
      }
    }
  }
  ...
}

TEMPORARY_ID_PATTERN is declared /#(aw_[A-Za-z0-9_]{3,12})\b/gi at module scope (line 30), matching the rule's "stateful regex" detector. The loop body only calls tempIds.add(normalizeTemporaryId(match[1])) — both are pure, non-throwing string operations (verified: normalizeTemporaryId just does String()/substring()/toLowerCase()). No break, return, or throw appears inside the while, so every invocation drains the regex to natural exhaustion, at which point the engine resets lastIndex to 0 on its own. Reusing the pattern across the 3 textFields iterations is therefore safe by construction — yet the rule reports it because it only checks for a textual .lastIndex = reset, not whether the loop can exit early.

Contrast with the two loops that do need (and have) an explicit reset in the same file — replaceTemporaryIdReferences (line 104) and replaceArtifactUrlReferences (line 551) — both call TEMPORARY_ID_CANDIDATE_PATTERN.lastIndex = 0; immediately before their loop specifically because those loops run standalone (not nested per-field in a for), so the author already reasons about this correctly elsewhere; the rule just can't tell the difference.

Why this matters

The "fix" a developer would reach for — adding TEMPORARY_ID_PATTERN.lastIndex = 0; before the loop — is a no-op that adds noise without adding safety, training people to pattern-match the rule instead of understanding it. Left as-is, this is a standing false positive on every eslint gh-aw-custom/require-lastindex-reset-before-global-exec-loop run over this file.

Suggested fix

Before reporting, walk the while loop's body for BreakStatement, ReturnStatement, ThrowStatement, or a ContinueStatement targeting an outer loop (any node that could end the current iteration set before .exec() naturally returns null), scoped so it doesn't cross into a nested function. If none of these appear anywhere in the loop body, the exec loop is guaranteed to exhaust naturally and reporting can be skipped (or downgraded).

Acceptance criteria

  • Add a valid test case mirroring extractTemporaryIdReferences's shape: module-scope g regex, while (( ) !== null) loop whose body has no break/return/throw, reused across a for loop with no reset — rule does not report.
  • Existing invalid tests (loop body with no exit points but also no reset today) continue to report — i.e. don't broaden the exemption to cover loops that genuinely never reset.
  • Add an invalid test where the loop body does contain an early break/return/throw and still lacks a reset — rule still reports (this is the real risk case the rule protects against).
  • Re-verify actions/setup/js/temporary_id.cjs:649 is no longer flagged once the fix lands.

Generated by 🤖 ESLint Refiner · agent · 212 AIC · ⌖ 4.87 AIC · ⊞ 5.3K ·

  • expires on Aug 24, 2026, 9:29 PM UTC-08:00

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions