Skip to content

Add ESLint rule: no-math-minmax-array-spread - #53653

Merged
pelikhan merged 3 commits into
mainfrom
copilot/eslint-miner-add-no-math-minmax-array-spread
Aug 18, 2026
Merged

Add ESLint rule: no-math-minmax-array-spread#53653
pelikhan merged 3 commits into
mainfrom
copilot/eslint-miner-add-no-math-minmax-array-spread

Conversation

Copilot AI commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Math.max(...arr) expands every element into call arguments, so an array whose size depends on runtime data can throw RangeError: Maximum call stack size exceeded (limit is engine-dependent, commonly tens of thousands of elements). Three instances of this pattern exist in actions/setup/js, all over arrays derived from workflow-run or file-scan data.

// flagged — size depends on external data
const oldest = Math.min(...runs.map(r => r.startedAt).filter(Boolean));

// suggested
const oldest = values.reduce((a, b) => Math.min(a, b));

Rule (eslint-factory/src/rules/no-math-minmax-array-spread.ts)

  • Flags Math.min / Math.max calls whose sole argument is a spread of an identifier, member expression, or call expression. Handles computed access (Math["max"]).
  • Skips mixed forms (Math.max(0, ...arr)) — fixed arguments imply an intentional, bounded call shape.
  • Skips inline array literals (Math.max(...[1, 2, 3])) — statically bounded by the source.
  • Skips calls where Math is shadowed by a local binding.
  • Message names the concrete fix inline, substituting the actual expression text.

Wiring

  • Registered in src/index.ts; enabled as warn in eslint.config.cjs.
  • README rule-index row + rule section (enforced by the existing index-readme-parity test).
  • Tests in no-math-minmax-array-spread.test.ts covering each valid/invalid branch above.

Scope is limited to eslint-factory/**; no source changes to the flagged actions/setup/js call sites, which surface as warnings.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Add ESLint rule no-math-minmax-array-spread Add ESLint rule: no-math-minmax-array-spread Aug 18, 2026
Copilot AI requested a review from pelikhan August 18, 2026 11:57
@pelikhan
pelikhan marked this pull request as ready for review August 18, 2026 11:58
Copilot AI balanced review requested due to automatic review settings August 18, 2026 11:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a custom ESLint rule to detect potentially unsafe array spreading into Math.min and Math.max.

Changes:

  • Implements and tests the new rule.
  • Registers and enables it as a warning.
  • Documents detected forms and alternatives.
Show a summary per file
File Description
eslint-factory/src/rules/no-math-minmax-array-spread.ts Implements detection and diagnostics.
eslint-factory/src/rules/no-math-minmax-array-spread.test.ts Tests supported rule branches.
eslint-factory/src/index.ts Registers the rule.
eslint-factory/eslint.config.cjs Enables the rule as a warning.
eslint-factory/README.md Documents the rule.

Review details

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Suppressed comments (3)

eslint-factory/src/rules/no-math-minmax-array-spread.ts:73

  • Fixed arguments do not bound the spread: Math.max(0, ...values) still passes every element as a call argument and can hit the same engine limit. This early return leaves a common vulnerable form unreported. The rule should inspect every spread argument and report any unbounded one; the mixed-form test and README exclusion should be updated accordingly.
        // Only the single-argument spread form is reported: fixed arguments such as
        // `Math.max(0, ...arr)` suggest an intentional, likely bounded call shape.
        if (node.arguments.length !== 1) return;

eslint-factory/src/rules/no-math-minmax-array-spread.ts:60

  • An inline array literal is not necessarily bounded: Math.max(...[0, ...values]) has an ArrayExpression argument but can still expand an unbounded number of elements, so this predicate skips it. Treat array literals as bounded only when their own spread elements are also statically bounded, and add a regression test for nested spread arrays.
     * Returns true when the spread argument has a size that is not statically bounded by
     * the source itself. Inline array literals are always bounded, so they are excluded.
     */
    function isUnboundedSpreadArgument(node: TSESTree.Node): boolean {
      return node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.MemberExpression || node.type === AST_NODE_TYPES.CallExpression;

eslint-factory/src/rules/no-math-minmax-array-spread.ts:60

  • This treats every identifier and call result as unbounded, producing a concrete false positive in the configured scan: actions/setup/js/patch_path_helpers.cjs:26-28 spreads a value derived from filtering a two-element literal, so its maximum length is two. Resolve immutable identifier initializers and preserve cardinality through bounded operations such as filter before reporting; the repository already has a safe scope-resolution pattern in no-child-process-interpolated-command.ts:38-58.
    function isUnboundedSpreadArgument(node: TSESTree.Node): boolean {
      return node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.MemberExpression || node.type === AST_NODE_TYPES.CallExpression;
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Balanced

},
schema: [],
messages: {
noMathMinMaxArraySpread: "Avoid Math.{{method}}(...{{arg}}) — spreading an array of unknown size can throw `RangeError: Maximum call stack size exceeded`. Use `{{arg}}.reduce((a, b) => Math.{{method}}(a, b))` instead.",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d98e5c9 — the reduce fix now includes the identity value as the initializer (Infinity for Math.min, -Infinity for Math.max), so empty-array behavior matches the spread form. Updated the message text, tests, and README accordingly.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot Please refresh this PR branch if needed, review for any unresolved feedback, and run the pr-finisher skill before reporting validation results and remaining blockers.

  • No unresolved review threads were visible from the lightweight triage data.
  • No failed checks were listed in the compact queue for this run.
  • Branch refresh was requested for this PR.
  • After verification, please report whether maintainer review or CI follow-up is still needed.

Run: https://github.com/github/gh-aw/actions/runs/32134497286

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 41.8 AIC · ⌖ 8.03 AIC · ⊞ 8.8K ·
Comment /souschef to run again

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Ponytail Reviewer completed successfully!

Lean already. Ship. The PR adds a single well-scoped ESLint rule (no-math-minmax-array-spread) with tests and docs — no unnecessary complexity, no speculative abstraction, nothing to cut.

Generated by Ponytail Reviewer for #53653

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

Test Quality Sentinel: TypeScript tests (.test.ts) are not in the analyzed scope. This PR adds an ESLint rule with TypeScript tests in eslint-factory/src/rules/no-math-minmax-array-spread.test.ts. Only Go (_test.go) and JavaScript (*.test.cjs, *.test.js) tests are scored by this tool.

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR does not have the 'implementation' label and has 0 new lines of code in business logic directories (default_business_additions=0, threshold=100).

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

Copy link
Copy Markdown
Contributor

Comment Memory

reviewed_at: 2026-08-18T00:00:00Z
review_event: REQUEST_CHANGES
top_themes:
  - incorrect reduce replacement for empty arrays
  - rule guidance changes Math.min/Math.max semantics
files_reviewed:
  - eslint-factory/README.md
  - eslint-factory/eslint.config.cjs
  - eslint-factory/src/index.ts
  - eslint-factory/src/rules/no-math-minmax-array-spread.test.ts
  - eslint-factory/src/rules/no-math-minmax-array-spread.ts
comment_count: 1

Note

This comment is managed by comment memory.

It stores persistent context for this thread in the code block at the top of this comment.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 4.47 AIC · ⌖ 6.9 AIC · ⊞ 7K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Request changes

This rule currently recommends a transformation that changes Math.min/Math.max empty-input semantics and can introduce a new runtime throw.

Blocking theme
  • The diagnostic message and README both steer callers from Math.min(...values) / Math.max(...values) to values.reduce(...), but that replacement throws on [] where the built-ins return Infinity / -Infinity.
  • Because the rule targets arrays derived from runtime data, empty inputs are not rare edge cases here.

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 4.47 AIC · ⌖ 6.9 AIC · ⊞ 7K
Comment /review to run again

},
schema: [],
messages: {
noMathMinMaxArraySpread: "Avoid Math.{{method}}(...{{arg}}) — spreading an array of unknown size can throw `RangeError: Maximum call stack size exceeded`. Use `{{arg}}.reduce((a, b) => Math.{{method}}(a, b))` instead.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This warning hard-codes a broken replacement: Math.max(...[]) returns -Infinity and Math.min(...[]) returns Infinity, but values.reduce((a, b) => Math.max(a, b)) throws on empty arrays, so the rule will push users toward a semantic regression and potential runtime crash.

💡 Why this needs fixing

The rule message is part of the API here, and right now it recommends a transformation that is only valid when the array is known non-empty. Many of the flagged cases are explicitly about data coming from workflow runs, API responses, or file scans, so emptiness is a realistic edge case rather than a theoretical one.

At minimum, the warning text and README need to call out the empty-array behavior and suggest a safe pattern, for example:

const largest = values.length === 0
  ? -Infinity
  : values.reduce((a, b) => Math.max(a, b));

Likewise for Math.min, preserving Infinity on empty input.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d98e5c9 — the reduce fix now includes the identity value as the initializer (Infinity for Math.min, -Infinity for Math.max), so empty-array behavior matches the spread form. Updated the message text, tests, and README accordingly.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The rule is well-scoped and the tests cover the documented cases. One non-blocking issue was found, and there is an existing comment from Copilot about the empty-array behavior difference between Math.max(...[])-Infinity and [].reduce(...)RangeError.

Additional non-blocking issue: The arguments.length !== 1 guard passes Math.max(...a, ...b) (multiple unbounded spreads, no fixed arg), which has the same stack-overflow risk the rule is designed to prevent. The exemption intent is "at least one fixed argument is present", not "more than one argument total".

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 24.7 AIC · ⌖ 8.9 AIC · ⊞ 5.7K

if (argument.type !== AST_NODE_TYPES.SpreadElement) return;
if (!isUnboundedSpreadArgument(argument.argument)) return;

context.report({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The arguments.length !== 1 guard silently passes Math.max(...a, ...b) — two unbounded spreads with no fixed argument — which is equally risky. Consider only exempting the case where at least one argument is not a spread element (i.e. a fixed value is present), rather than exempting all calls with more than one argument:

// current: any call with != 1 argument is skipped
if (node.arguments.length !== 1) return;

// suggested: only skip when a non-spread (fixed) arg is present
const hasFixedArg = node.arguments.some(a => a.type !== AST_NODE_TYPES.SpreadElement);
if (hasFixedArg) return; // at least one fixed arg → intentional bounded shape
// then check each spread argument individually

This would also flag Math.max(...a, ...b) where all arguments are unbounded spreads.

@copilot please address this.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /tdd and /grill-with-docs — requesting changes on three focused issues.

📋 Key Themes & Highlights

Issues found

  1. Empty-array behavior gap in docs (README.md line 175) — the suggested reduce alternative throws on empty arrays, while Math.max(...[]) returns -Infinity. The safe forms with initial values (-Infinity/Infinity) should be documented.
  2. Missing empty-array test case (test file line 47) — no test pins the deliberate decision to exclude Math.max(...[]) from flagging; easy regression target.
  3. Fragile hardcoded message string (test file line 93) — one test case asserts the full rendered message string while all others use messageId. Makes the test brittle to wording changes.

Positive Highlights

  • ✅ Excellent scope discipline: skips mixed forms, inline literals, and shadowed Math — all edge cases handled correctly.
  • ✅ Computed access (Math["max"]) is covered — easy to miss.
  • ✅ Rule wiring, README index, and parity test all updated in the same PR.
  • hasLocalBinding scope-chain walk is a clean, reusable pattern.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 32.3 AIC · ⌖ 10.2 AIC · ⊞ 7.8K
Comment /matt to run again

invalid: [
{
code: `const largest = Math["max"](...values);`,
errors: [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] Hardcoded message string creates fragile coupling — if the message template changes, this test silently diverges from the messageId-based tests above.

💡 Use messageId for consistency

All other invalid test cases use { messageId: "noMathMinMaxArraySpread" }. This one case uses a raw string:

// fragile — breaks silently when message wording changes
errors: [{ message: "Avoid Math.max(...values) — ..." }]

// preferred — consistent with the rest of the suite
errors: [{ messageId: "noMathMinMaxArraySpread" }]

If the goal is to assert exact rendered text (e.g. to guard against template regressions), add a dedicated snapshot test for the formatted message separately.

@copilot please address this.

Comment thread eslint-factory/README.md
**Out of scope:**
- `Math.max(0, ...values)` and `Math.min(a, b, ...values)` — fixed arguments alongside the spread suggest an intentional, likely bounded call shape.
- `Math.max(...[1, 2, 3])` — inline array literals are statically bounded by the source.
- Calls where `Math` is shadowed by a local declaration.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/grill-with-docs] The documented safe alternative silently swaps one failure mode for another: [].reduce((a, b) => Math.max(a, b)) throws TypeError: Reduce of empty array with no initial value, while Math.max(...[]) returns -Infinity.

💡 Show the fully safe form with an initial value

Update the README's Safe alternative section to:

**Safe alternative:**
- `values.reduce((a, b) => Math.max(a, b), -Infinity)` — folds the array without expanding into arguments; safe for empty arrays.
- `values.reduce((a, b) => Math.min(a, b), Infinity)` — equivalent safe form for `Math.min`.

Without the initial value, users who follow the docs literally introduce a new crash on empty inputs.

@copilot please address this.

invalid: [],
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] No test documents the empty-array boundary: Math.max(...[]) returns -Infinity but the suggested [].reduce(...) throws. A test case anchors the deliberate design decision and guards against silent future changes.

💡 Suggested test case
it("valid: spreading an empty inline array literal is accepted (statically bounded)", () => {
  cjsRuleTester.run("no-math-minmax-array-spread", noMathMinMaxArraySpreadRule, {
    valid: [`const x = Math.max(...[]);`],
    invalid: [],
  });
});

This makes the current out-of-scope decision explicit and forces any future change to be a deliberate diff.

@copilot please address this.

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

PR Triage

Category: chore | Risk: low | Priority: low (score 35)

  • Impact 15/50 — adds internal ESLint rule, no user-facing/runtime impact
  • Urgency 8/30 — CHANGES_REQUESTED (2x), CI pending
  • Quality 12/20 — moderate diff (5 files), review feedback outstanding

Recommended action: defer — resolve lint-rule review feedback

Generated by 🔧 PR Triage Agent · auto · 53.1 AIC · ⌖ 2.52 AIC · ⊞ 8.3K ·

@pelikhan
pelikhan merged commit 957f7c5 into main Aug 18, 2026
@pelikhan
pelikhan deleted the copilot/eslint-miner-add-no-math-minmax-array-spread branch August 18, 2026 12:35
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.87.1

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[eslint-miner] Add ESLint rule: no-math-minmax-array-spread

4 participants