Skip to content

[eslint-miner] eslint-factory: add require-lastindex-reset-before-global-exec-loop rule - #52875

Draft
github-actions[bot] wants to merge 1 commit into
mainfrom
eslint-miner/require-lastindex-reset-before-global-exec-loop-e581edd7ed8ef0ff
Draft

[eslint-miner] eslint-factory: add require-lastindex-reset-before-global-exec-loop rule#52875
github-actions[bot] wants to merge 1 commit into
mainfrom
eslint-miner/require-lastindex-reset-before-global-exec-loop-e581edd7ed8ef0ff

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Summary

Adds one new custom ESLint rule: require-lastindex-reset-before-global-exec-loop.

It flags module-scoped regex literals declared with the g or y flag that are consumed in the common while ((match = RE.exec(str)) !== null) idiom without an explicit RE.lastIndex = 0 reset beforehand.

Why

.exec() on a stateful (g/y) regex resumes scanning from RE.lastIndex on every call. When such a regex is declared at module scope (shared across every invocation of the enclosing function), and a prior call exits the loop early — throws, returns before exhausting matches, or is run against shorter input — lastIndex is left pointing partway through the previous string. The next call then silently resumes scanning mid-string instead of from position 0, which can skip content entirely or miss matches, with no error raised.

Real bug found in actions/setup/js

temporary_id.cjs declares two sibling module-level global regexes:

const TEMPORARY_ID_PATTERN = /#(aw_[A-Za-z0-9_]{3,12})\b/gi;
const TEMPORARY_ID_CANDIDATE_PATTERN = /#aw_([A-Za-z0-9_-]+)/gi;

TEMPORARY_ID_CANDIDATE_PATTERN is correctly reset with .lastIndex = 0 before both of its exec loops. TEMPORARY_ID_PATTERN, used in extractTemporaryIdReferences() to pull #aw_XXXX temporary-ID references out of safe-output message body/title/description text, has no such reset in its exec loop — a genuine, unfixed latent bug that can cause safe-output temporary ID cross-references to be silently dropped depending on call history.

Running npm run lint:setup-js with the new rule enabled confirms it fires exactly once, precisely at that call site (temporary_id.cjs:649), with zero false positives across the rest of actions/setup/js.

Changes

  • eslint-factory/src/rules/require-lastindex-reset-before-global-exec-loop.ts — new rule implementation
  • eslint-factory/src/rules/require-lastindex-reset-before-global-exec-loop.test.ts — unit tests (valid: reset present, non-global regex, function-local regex; invalid: missing reset, including a case modeled directly on the real temporary_id.cjs bug, and the y sticky flag variant)
  • eslint-factory/src/index.ts — registers the new rule in the plugin
  • eslint-factory/eslint.config.cjs — enables the rule as warn for actions/setup/js

This PR only adds the lint rule; it does not fix the flagged bug in temporary_id.cjs, per the mining task scope.

Validation

  • cd eslint-factory && npm install
  • cd eslint-factory && npm run build — passes
  • cd eslint-factory && npx vitest run src/rules/require-lastindex-reset-before-global-exec-loop.test.ts — 2/2 passing
  • cd eslint-factory && npm run lint:setup-js — new rule fires once (the real bug above), no false positives; pre-existing unrelated warnings unaffected

Note: the full vitest run suite has 5 pre-existing failures in require-fs-io-try-catch.test.ts unrelated to this change (verified before and after).

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • proxy.golang.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "proxy.golang.org"

See Network Configuration for more information.

Generated by ESLint Miner · auto · 123.9 AIC · ⌖ 12.2 AIC · ⊞ 6.7K ·

  • expires on Aug 22, 2026, 1:00 AM UTC-08:00

Adds a new custom ESLint rule that flags module-scoped regexes with the
'g' or 'y' flag that are consumed in a `while ((match = RE.exec(str)))`
loop without an explicit `RE.lastIndex = 0` reset beforehand.

Motivation: `.exec()` on a stateful ('g'/'y') regex resumes scanning
from `lastIndex` on each call. When such a regex is declared at module
scope and reused across multiple invocations of the enclosing
function, a prior call that exits early (throws, returns before the
loop drains, or scans shorter input) leaves `lastIndex` pointing past
position 0. The next call then silently skips content or misses
matches entirely, since scanning resumes mid-string instead of from
the start.

Found via a real, unfixed instance of this bug in
actions/setup/js/temporary_id.cjs: `TEMPORARY_ID_CANDIDATE_PATTERN` is
correctly reset with `.lastIndex = 0` at its two call sites, but the
sibling `TEMPORARY_ID_PATTERN` (used to extract '#aw_XXXX' temporary ID
references from safe-output message text) has no such reset in its
exec loop. Running lint:setup-js with the new rule enabled confirms it
fires exactly once, precisely on that call site, with zero false
positives across the rest of actions/setup/js.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added automation cookie Issue Monster Loves Cookies! eslint labels Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automation cookie Issue Monster Loves Cookies! eslint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants