prefer-number-isnan: treat ESM import bindings as local shadows#43500
Conversation
…-number-isnan Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
✅ Test Quality Sentinel completed test quality analysis. |
|
|
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅ |
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR #43500 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100). |
There was a problem hiding this comment.
Pull request overview
This pull request updates the prefer-number-isnan ESLint rule to treat ESM ImportBinding identifiers as locally shadowing bindings, preventing false positives when isNaN/window/global are imported or shimmed rather than referring to the real globals.
Changes:
- Updated
hasLocalBindingto treat any local definition (includingImportBinding) as shadowing the global candidate. - Added ESM-focused valid test cases covering imported/shimmed
isNaN,window, andglobal. - Added an inline comment clarifying the intended shadowing semantics.
Show a summary per file
| File | Description |
|---|---|
| eslint-factory/src/rules/prefer-number-isnan.ts | Treats any in-scope definition (including ESM imports) as a local shadow to avoid behavior-changing suggestions on imported/shimmed globals. |
| eslint-factory/src/rules/prefer-number-isnan.test.ts | Adds ESM sourceType: "module" valid cases to lock in the updated shadowing behavior for imports. |
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: 0
- Review effort level: Low
🧪 Test Quality Sentinel Report✅ Test Quality Score: 100/100 — Excellent
Note: Test file is 📊 Metrics (3 tests)
Verdict
Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
There was a problem hiding this comment.
Review: prefer-number-isnan — ESM ImportBinding fix
The change is correct and complete.
Root cause & fix
The old predicate variable?.defs.some(d => d.type !== "ImportBinding") returned false when every def was an ImportBinding, so ESM-imported names were never treated as local shadows — causing false-positive diagnostics on imported isNaN/window/global shims.
The new predicate variable && variable.defs.length > 0 is both simpler and semantically correct: any locally-defined variable (regardless of def type) shadows the global candidate.
Tests
The three new valid cases use esmRuleTester (sourceType: module), which is required for import syntax to parse — that's correct placement.
Minor gap (pre-existing, not introduced here)
There are no invalid-case ESM tests confirming that an unbound isNaN(value); in a module file is still flagged. All invalid cases run under cjsRuleTester. Adding one ESM invalid case would close this gap, but it isn't a regression introduced by this PR.
No blocking issues. LGTM.
🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 21.5 AIC · ⌖ 5.99 AIC · ⊞ 4.9K
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /diagnosing-bugs and /tdd — the bug fix itself is correct and well-scoped; two non-blocking test-coverage gaps flagged as suggestions.
📋 Key Themes & Highlights
Key Themes
- Fix is correct: Changing
defs.some(d => d.type !== "ImportBinding")→variable && variable.defs.length > 0properly treats every local definition (function, const, and ImportBinding alike) as shadowing the global. The old condition returnedfalsefor a pureImportBindingdef-list, creating the false positive. - Missing
globalThisimport-shadow test:windowandglobalnow have ESM import cases butglobalThisdoes not — leaving a gap in symmetry withGLOBAL_IS_NAN_OBJECTS. - No ESM invalid tests: All
invalidblocks usecjsRuleTester. Adding a small ESM invalid block would prove the rule still fires correctly insourceType: "module"without a shadow.
Positive Highlights
- ✅ Minimal, surgical change — only 3 lines changed in the implementation
- ✅ Regression tests added for each new valid ESM pattern
- ✅ Intent comment clearly documents the
ImportBindinginclusion rationale - ✅ PR description is thorough and links behaviour to the code change
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 34.2 AIC · ⌖ 6.8 AIC · ⊞ 6.6K
Comment /matt to run again
Comments that could not be inline-anchored
eslint-factory/src/rules/prefer-number-isnan.test.ts:42
[/tdd] The three GLOBAL_IS_NAN_OBJECTS members (globalThis, window, global) are tested for const-shadowing, but only window and global get ESM import-shadow coverage — globalThis is missing.
<details>
<summary>💡 Suggested addition</summary>
Add a parallel case to keep the set symmetric:
`import { globalThis } from "./global-shim"; globalThis.isNaN(value);`,This mirrors the window/global import cases and prevents a future regression if the globalThis bra…
eslint-factory/src/rules/prefer-number-isnan.test.ts:48
[/tdd] There are no invalid ESM test cases confirming the rule still fires in ESM source when the global is not shadowed. The existing invalid block uses cjsRuleTester, so it does not exercise the ESM path.
<details>
<summary>💡 Suggested addition</summary>
Add a minimal esmRuleTester invalid block, e.g. inside the same it block or a dedicated one:
esmRuleTester.run("prefer-number-isnan", preferNumberIsNanRule, {
valid: [],
invalid: [
{
code: `isNaN(value)…
</details>There was a problem hiding this comment.
Review: prefer-number-isnan ESM import binding fix
The core logic change is correct — is the right way to detect locally-bound identifiers in ESLint's scope model. Globals registered via environment configs appear with in the global scope, so the condition correctly skips them. The case now works as it should.
Two test coverage gaps need attention:
Findings
Medium — Missing ESM invalid test (blocking gap in regression coverage)
All invalid: tests use cjsRuleTester. The scope-walking behavior differs across sourceType values in some ESLint scope implementations. If variable.defs.length > 0 inadvertently suppresses global detection in ESM mode (e.g., ESLint's module scope providing a synthetic variable record), there is no test that would catch it. An esmRuleTester invalid: case for bare isNaN(value); and window.isNaN(value); would close this gap.
Low — Incomplete GLOBAL_IS_NAN_OBJECTS symmetry
The new ESM valid cases cover window and global import shims but skip globalThis. All three are in GLOBAL_IS_NAN_OBJECTS; the test suite should mirror that uniformly.
🔎 Code quality review by PR Code Quality Reviewer · 131.6 AIC · ⌖ 9.05 AIC · ⊞ 5.4K
Comment /review to run again
| `const window = { isNaN(value) { return value; } }; window["isNaN"](value);`, | ||
| `const global = { isNaN(value) { return value; } }; global.isNaN(value);`, | ||
| `import { window } from "./browser-shim"; window.isNaN(value);`, | ||
| `import { global } from "./server-shim"; global["isNaN"](value);`, |
There was a problem hiding this comment.
Missing ESM invalid test: no test proves the rule still flags isNaN() in ESM modules after this change, leaving a correctness regression undetectable.
💡 Explanation and suggested fix
All invalid: test cases use cjsRuleTester. The fix changes scope-walking behavior, which is determined per sourceType. If the new variable && variable.defs.length > 0 branch misbehaves in ESM mode (e.g., due to how eslint scope analysis treats globals under sourceType: "module"), no test would catch it.
Consider adding to the new esmRuleTester.run block:
esmRuleTester.run("prefer-number-isnan", preferNumberIsNanRule, {
valid: [ /* existing */ ],
invalid: [
{
code: `isNaN(value);`,
errors: [{ messageId: "preferNumberIsNaN", suggestions: [{ messageId: "replaceWithNumberIsNaN", output: `Number.isNaN(value);` }] }],
},
{
code: `window.isNaN(value);`,
errors: [{ messageId: "preferNumberIsNaN", suggestions: [{ messageId: "replaceWithNumberIsNaN", output: `Number.isNaN(value);` }] }],
},
],
});Without this, the PR's test coverage only validates the non-regression path (valid cases), not that the rule still fires in ESM when no import shadowing is present.
| `const global = { isNaN(value) { return value; } }; global.isNaN(value);`, | ||
| `import { window } from "./browser-shim"; window.isNaN(value);`, | ||
| `import { global } from "./server-shim"; global["isNaN"](value);`, | ||
| // Dynamic computed access — identifier property reference, not string literal "isNaN" |
There was a problem hiding this comment.
Incomplete test symmetry: globalThis is in GLOBAL_IS_NAN_OBJECTS but has no import-shim valid case.
💡 Suggested addition
The three new valid ESM tests cover import { isNaN }, import { window }, and import { global } — but not import { globalThis }. Since globalThis is an equal member of GLOBAL_IS_NAN_OBJECTS, a missing test here leaves asymmetric coverage.
Consider adding:
`import { globalThis } from "./global-shim"; globalThis.isNaN(value);`,Low risk by itself, but the gap creates a mental model mismatch between the rule's constant and the test suite.
|
@copilot please run the
|
…nvalid tests Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Done. Addressed all feedback from the bot reviewers:
CI must be re-triggered by a maintainer before merge. |
|
@copilot please run the
|
…mber-isnan-issue Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Branch updated from main (merge commit
CI must be re-triggered by a maintainer before merge. |
🤖 PR Triage — Run §28741077056
Rationale: Companion fix to #43515 — makes import-bound
|
|
@copilot please run the
|
prefer-number-isnantreated identifiers bound only viaImportBindingas if they were globals, which created false positives for imported/shimmedisNaN/window/globalin ESM and could suggest behavior-changing replacements. This change makes import-bound names follow the same shadowing semantics as other local bindings and pins that behavior with ESM rule tests.Rule semantics
ImportBinding) as shadowing the global candidate.Behavior coverage (ESM)
import { isNaN } from "lodash"; isNaN(value);import { window } from "./browser-shim"; window.isNaN(value);import { global } from "./server-shim"; global["isNaN"](value);Intent documentation in code
hasLocalBindingclarifying that local defs, including imports, shadow globals.