Skip to content

Extend require-parseInt-radix to computed and global-object parseInt calls - #42027

Merged
pelikhan merged 2 commits into
mainfrom
copilot/eslint-factory-require-parseint-radix
Jun 28, 2026
Merged

Extend require-parseInt-radix to computed and global-object parseInt calls#42027
pelikhan merged 2 commits into
mainfrom
copilot/eslint-factory-require-parseint-radix

Conversation

Copilot AI commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

require-parseInt-radix only caught bare parseInt(...) and direct Number.parseInt(...), leaving the same false-negative class already closed for JSON["parse"] in the sibling rule. This change expands coverage to computed member access and global-object receivers while keeping alias/destructure tracking explicitly out of scope.

  • Rule coverage

    • Detect missing radix for computed Number["parseInt"](...)
    • Detect missing radix for globalThis.parseInt(...), window.parseInt(...), and global.parseInt(...)
    • Detect the same global-object forms when accessed via computed string literals
  • Intentional scope boundary

    • Preserve the existing non-goal for aliased/destructured bindings, e.g.:
      • const p = parseInt; p(value)
      • const { parseInt } = Number; parseInt(value)
    • Document the rationale inline: deeper alias/scope analysis is not worth the cost for this rule’s risk surface
  • Tests

    • Add valid/invalid cases for each newly supported access form
    • Add explicit valid cases proving locally bound parseInt, globalThis, window, and global identifiers are not reported
Number["parseInt"](value);        // now reported
globalThis.parseInt(value);       // now reported
window["parseInt"](value);        // now reported
global.parseInt(value);           // now reported

const { parseInt } = Number;
parseInt(value);                  // still out of scope

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix require-parseInt-radix to catch computed access cases Extend require-parseInt-radix to computed and global-object parseInt calls Jun 28, 2026
Copilot AI requested a review from pelikhan June 28, 2026 07:50
@pelikhan
pelikhan marked this pull request as ready for review June 28, 2026 08:58
Copilot AI review requested due to automatic review settings June 28, 2026 08:58
@pelikhan
pelikhan merged commit 04c80c2 into main Jun 28, 2026
14 checks passed
@pelikhan
pelikhan deleted the copilot/eslint-factory-require-parseint-radix branch June 28, 2026 08:58
@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

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

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

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

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

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

This PR expands the require-parseInt-radix ESLint rule in eslint-factory to catch additional parseInt(...) call shapes that previously slipped through, specifically computed member access and common global-object receivers, while explicitly keeping alias/destructure tracking out of scope.

Changes:

  • Extend detection to computed Number["parseInt"](...) calls missing a radix.
  • Extend detection to globalThis|window|global .parseInt(...) (and computed "parseInt") missing a radix, while avoiding reports when those identifiers are locally bound.
  • Add a new test suite covering the newly supported access forms and explicit “out of scope”/shadowing cases.
Show a summary per file
File Description
eslint-factory/src/rules/require-parseInt-radix.ts Adds scope-aware detection for computed and global-object parseInt member calls without radix.
eslint-factory/src/rules/require-parseInt-radix.test.ts Introduces tests for the expanded detection surface and shadowing/non-goal cases.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment on lines +52 to +55
const isDirectAccess = property.type === "Identifier" && property.name === "parseInt";
const isComputedAccess = property.type === "Literal" && property.value === "parseInt";

return isDirectAccess || isComputedAccess;
Comment on lines +50 to +60
it("invalid: computed Number.parseInt access without radix is flagged", () => {
cjsRuleTester.run("require-parseInt-radix", requireParseIntRadixRule, {
valid: [],
invalid: [
{
code: `Number["parseInt"](value);`,
errors: [{ messageId: "requireRadix" }],
},
],
});
});
@github-actions github-actions Bot mentioned this pull request Jun 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 4 test(s) across 1 TypeScript/vitest file (require-parseInt-radix.test.ts): 4 design, 0 implementation, 0 guideline violations.

📊 Metrics & Test Classification (4 tests analyzed)
Metric Value
New/modified tests analyzed 4
✅ Design tests (behavioral contracts) 4 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 4 (100%)
Duplicate test clusters 0
Test inflation detected No (93 test lines / 54 prod lines = 1.72×)
🚨 Coding-guideline violations 0
Test File Classification Issues Detected
valid: explicit radix accepted require-parseInt-radix.test.ts:19 ✅ Design
valid: aliased/destructured out of scope require-parseInt-radix.test.ts:37 ✅ Design
invalid: computed Number.parseInt without radix require-parseInt-radix.test.ts:51 ✅ Design
invalid: global-object parseInt without radix require-parseInt-radix.test.ts:63 ✅ Design

Language breakdown: Go: 0; JavaScript/TypeScript: 4 (*.test.ts using vitest — scored as JS-equivalent). No other languages detected.

Test notes:

  • Test 1 covers 9 valid forms (direct + computed, with radix): behavioral contract for the rule's non-reporting behavior.
  • Test 2 covers 5 scope-boundary edge cases (aliased/destructured bindings, locally-bound global objects): verifies the intentional non-goal is preserved.
  • Test 3 verifies Number["parseInt"](value) without radix triggers requireRadix.
  • Test 4 verifies all 6 global-object forms (globalThis, window, global × dot and computed notation) trigger requireRadix.

Verdict

Check passed. 0% implementation tests (threshold: 30%). All 4 tests verify observable ESLint rule behavior (what gets reported vs. not reported). Each test covers a distinct detection surface introduced by this PR.

🧪 Test quality analysis by Test Quality Sentinel · 64.5 AIC · ⌖ 11 AIC · ⊞ 8.2K ·

@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.

✅ Test Quality Sentinel: 100/100. Test quality is acceptable — 0% of new tests are implementation tests (threshold: 30%).

@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 /improve-codebase-architecture — requesting changes on test coverage and naming.

📋 Key Themes & Highlights

Key Themes

  • Missing baseline regression test (/tdd): The refactored hasLocalBinding path for bare parseInt(value) has no invalid test case — the most important gap since this is the rule's original behaviour and the first-ever test file for it.
  • Test granularity (/tdd): Multiple assertions bundled into single it blocks make failure diagnosis harder; the sibling rule sets a one-case-per-it convention.
  • ESM invalid coverage (/tdd): All invalid cases run only under cjsRuleTester; ESM scope semantics affecting hasLocalBinding are untested.
  • Constant naming and coverage (/improve-codebase-architecture): GLOBAL_PARSE_INT_OBJECTS conflates a static built-in (Number) with global-object aliases (globalThis, window, global). self (browser/Web Worker alias) is also missing.

Positive Highlights

  • ✅ The hasLocalBinding scope-walk correctly prevents false positives on locally-shadowed identifiers — the logic is sound.
  • ✅ Scope boundary is explicitly documented in both the PR description and inline comments, making the intentional non-goals clear.
  • ✅ The computed-access (Number["parseInt"]) and global-object detection are cleanly unified through isParseIntProperty, keeping the rule implementation readable.
  • ✅ Valid cases for shadowed globalThis/window/global identifiers are well thought out.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 77.8 AIC · ⌖ 7.42 AIC · ⊞ 6.6K

});
});

it("invalid: computed Number.parseInt access without radix is flagged", () => {

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] Missing baseline regression test — parseInt(value) (original behaviour) has no invalid case.

This is the first test file for this rule, and the hasLocalBinding change refactored the bare-call detection path. Without an invalid case for parseInt(value), a regression in that path would go undetected.

💡 Suggested addition

Add before this block:

it("invalid: bare global parseInt without radix is flagged", () => {
  cjsRuleTester.run("require-parseInt-radix", requireParseIntRadixRule, {
    valid: [],
    invalid: [
      {
        code: `parseInt(value);`,
        errors: [{ messageId: "requireRadix" }],
      },
    ],
  });
});

@copilot please address this.

});
});

it("invalid: global-object parseInt access without radix is flagged", () => {

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] Six invalid cases share one it block — consider splitting into one it per access form.

The sibling require-json-parse-try-catch.test.ts gives each logical scenario its own it. Bundling 6 cases here makes it hard to pinpoint which specific form failed when the test suite turns red.

💡 Suggested structure
it("invalid: globalThis.parseInt without radix is flagged", () => { ... });
it("invalid: globalThis[\"parseInt\"] without radix is flagged", () => { ... });
it("invalid: window.parseInt without radix is flagged", () => { ... });
it("invalid: window[\"parseInt\"] without radix is flagged", () => { ... });
it("invalid: global.parseInt without radix is flagged", () => { ... });
it("invalid: global[\"parseInt\"] without radix is flagged", () => { ... });

This also mirrors the granularity of the valid-cases suite above.

@copilot please address this.

});
});

it("valid: aliased and destructured bindings remain out of scope", () => {

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] All invalid cases use cjsRuleTester — no ESM invalid coverage.

esmRuleTester is only used for the valid/aliased block. The sibling rule (require-json-parse-try-catch.test.ts) adds a dedicated ESM invalid case (line 72). ESM introduces different scope semantics that could affect hasLocalBinding; at minimum, one invalid case (e.g. Number["parseInt"](value)) should run against esmRuleTester to confirm identical behaviour in module context.

@copilot please address this.

import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";

const createRule = ESLintUtils.RuleCreator(name => `https://github.com/github/gh-aw/tree/main/actions/setup/js/eslint-factory#${name}`);
const GLOBAL_PARSE_INT_OBJECTS = new Set(["Number", "globalThis", "window", "global"]);

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.

[/improve-codebase-architecture] GLOBAL_PARSE_INT_OBJECTS conflates two distinct concepts — Number is a built-in constructor, not a global-object alias.

The constant mixes Number (a built-in whose parseInt is a static method) with globalThis/window/global (runtime aliases for the global object). The name implies all four are 'global objects', which is misleading. A clearer name would be PARSE_INT_HOSTS or KNOWN_PARSE_INT_NAMESPACES. Additionally, self is a valid global-object alias in browser and Web Worker environments — self.parseInt(value) currently escapes the rule.

💡 Suggestions

Rename and extend:

const PARSE_INT_HOSTS = new Set(["Number", "globalThis", "window", "global", "self"]);

Or, if you want to keep the structural split clear:

// Number holds parseInt as a static method
const PARSE_INT_STATIC_OBJECTS = new Set(["Number"]);
// Runtime aliases for the global object
const GLOBAL_OBJECT_ALIASES = new Set(["globalThis", "window", "global", "self"]);

@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.

REQUEST_CHANGES — two issues that should be fixed before this lands.

Blocking issues (2)

isParseIntProperty — false positive on computed variable access

The isDirectAccess branch (property.type === "Identifier" && property.name === "parseInt") does not guard against node.computed === true. A computed MemberExpression like Number[parseInt] (where parseInt is a locally declared variable) has an Identifier property node with name === "parseInt", so it matches isDirectAccess and is incorrectly reported. The original code explicitly checked !node.callee.computed and the refactor dropped that guard. Fixing it requires branching on node.computed in isParseIntProperty — see inline comment for the patch.

Test file covers only new cases; original parseInt(value) and Number.parseInt(value) paths are untested as invalid

The test file is brand new and never asserts that the two cases the rule was originally written to catch actually fire. A regression in the Identifier branch or a GLOBAL_PARSE_INT_OBJECTS edit that drops "Number" would merge silently. Inline comment has the suggested test block.

🔎 Code quality review by PR Code Quality Reviewer · 102 AIC · ⌖ 7.46 AIC · ⊞ 5.2K

*/
function isParseIntProperty(node: TSESTree.MemberExpression): boolean {
const property = node.property;
const isDirectAccess = property.type === "Identifier" && property.name === "parseInt";

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.

isDirectAccess falsely matches Number[parseInt] (computed access with a variable named parseInt), producing a false positive.

When node.computed === true and the bracket expression is a variable named parseInt — e.g. Number[parseInt](value) — the property AST node is Identifier { name: "parseInt" }. This is identical to the Number.parseInt non-computed case, so isDirectAccess returns true and the call gets incorrectly flagged. The original code avoided this by explicitly asserting !node.callee.computed before checking property name.

💡 Suggested fix

Gate each branch on node.computed:

function isParseIntProperty(node: TSESTree.MemberExpression): boolean {
  if (!node.computed) {
    // Number.parseInt — property is a non-computed Identifier
    return node.property.type === "Identifier" && (node.property as TSESTree.Identifier).name === "parseInt";
  }
  // Number["parseInt"] — property must be a string literal, not a variable
  return node.property.type === "Literal" && (node.property as TSESTree.Literal).value === "parseInt";
}

Concrete false-positive case that currently fires incorrectly:

const parseInt = "someKey";
Number[parseInt](value); // ← rule reports missing radix; should be silent

},
],
});
});

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 two original cases (parseInt(value) and Number.parseInt(value)) have no invalid test coverage after the refactor — a silent regression in either path would go undetected.

The old rule used two explicit conditional blocks for these cases. The refactor merges them into a generic handler, but the new test suite only validates the newly-added code paths. The parseInt(value) case is the primary violation this rule exists to catch and it has zero invalid assertions anywhere in the file.

💡 Suggested addition

Add a dedicated it block (or extend an existing one) covering both original cases:

it("invalid: bare parseInt and Number.parseInt without radix are flagged", () => {
  cjsRuleTester.run("require-parseInt-radix", requireParseIntRadixRule, {
    valid: [],
    invalid: [
      {
        code: `parseInt(value);`,
        errors: [{ messageId: "requireRadix" }],
      },
      {
        code: `Number.parseInt(value);`,
        errors: [{ messageId: "requireRadix" }],
      },
    ],
  });
});

Without these, a typo in the Identifier check on line 69 of the rule, or a change to GLOBAL_PARSE_INT_OBJECTS that accidentally drops "Number", would merge unnoticed.

@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.82.0

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants