diff --git a/rules/prefer-number-properties.js b/rules/prefer-number-properties.js index 3c4bb215b4..f1be2b34f8 100644 --- a/rules/prefer-number-properties.js +++ b/rules/prefer-number-properties.js @@ -1,9 +1,5 @@ 'use strict'; -const isShadowed = require('./utils/is-shadowed.js'); -const { - referenceIdentifierSelector, - callExpressionSelector, -} = require('./selectors/index.js'); +const {ReferenceTracker} = require('eslint-utils'); const {replaceReferenceIdentifier} = require('./fix/index.js'); const {fixSpaceAroundKeyword} = require('./fix/index.js'); @@ -25,102 +21,102 @@ const methods = { isFinite: false, }; -const methodsSelector = [ - callExpressionSelector(Object.keys(methods)), - ' > ', - '.callee', -].join(''); - -const propertiesSelector = referenceIdentifierSelector(['NaN', 'Infinity']); - const isNegative = node => { const {parent} = node; return parent && parent.type === 'UnaryExpression' && parent.operator === '-' && parent.argument === node; }; +function * checkMethods({sourceCode, tracker}) { + const traceMap = Object.fromEntries( + Object.keys(methods).map(name => [name, {[ReferenceTracker.CALL]: true}]), + ); + + for (const {node: callExpression, path: [name]} of tracker.iterateGlobalReferences(traceMap)) { + const node = callExpression.callee; + const isSafe = methods[name]; + + const problem = { + node, + messageId: METHOD_ERROR_MESSAGE_ID, + data: { + name, + }, + }; + + const fix = fixer => replaceReferenceIdentifier(node, `Number.${name}`, fixer, sourceCode); + + if (isSafe) { + problem.fix = fix; + } else { + problem.suggest = [ + { + messageId: METHOD_SUGGESTION_MESSAGE_ID, + data: { + name, + }, + fix, + }, + ]; + } + + yield problem; + } +} + +function * checkProperties({sourceCode, tracker, checkInfinity}) { + const properties = checkInfinity ? ['NaN', 'Infinity'] : ['NaN']; + const traceMap = Object.fromEntries( + properties.map(name => [name, {[ReferenceTracker.READ]: true}]), + ); + + for (const {node, path: [name]} of tracker.iterateGlobalReferences(traceMap)) { + const {parent} = node; + + let property = name; + if (name === 'Infinity') { + property = isNegative(node) ? 'NEGATIVE_INFINITY' : 'POSITIVE_INFINITY'; + } + + const problem = { + node, + messageId: PROPERTY_ERROR_MESSAGE_ID, + data: { + identifier: name, + property, + }, + }; + + if (property === 'NEGATIVE_INFINITY') { + problem.node = parent; + problem.data.identifier = '-Infinity'; + problem.fix = function * (fixer) { + yield fixer.replaceText(parent, 'Number.NEGATIVE_INFINITY'); + yield * fixSpaceAroundKeyword(fixer, parent, sourceCode); + }; + } else { + problem.fix = fixer => replaceReferenceIdentifier(node, `Number.${property}`, fixer, sourceCode); + } + + yield problem; + } +} + /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { - const sourceCode = context.getSourceCode(); - const options = { + const { + checkInfinity, + } = { checkInfinity: true, ...context.options[0], }; - // Cache `NaN` and `Infinity` in `foo = {NaN, Infinity}` - const reported = new WeakSet(); - return { - [methodsSelector](node) { - if (isShadowed(context.getScope(), node)) { - return; - } - - const {name} = node; - const isSafe = methods[name]; - - const problem = { - node, - messageId: METHOD_ERROR_MESSAGE_ID, - data: { - name, - }, - }; - - const fix = fixer => replaceReferenceIdentifier(node, `Number.${name}`, fixer, sourceCode); - - if (isSafe) { - problem.fix = fix; - } else { - problem.suggest = [ - { - messageId: METHOD_SUGGESTION_MESSAGE_ID, - data: { - name, - }, - fix, - }, - ]; - } - - return problem; - }, - [propertiesSelector](node) { - if (reported.has(node) || isShadowed(context.getScope(), node)) { - return; - } - - const {name, parent} = node; - if (name === 'Infinity' && !options.checkInfinity) { - return; - } - - let property = name; - if (name === 'Infinity') { - property = isNegative(node) ? 'NEGATIVE_INFINITY' : 'POSITIVE_INFINITY'; - } - - const problem = { - node, - messageId: PROPERTY_ERROR_MESSAGE_ID, - data: { - identifier: name, - property, - }, - }; + * 'Program:exit'() { + const sourceCode = context.getSourceCode(); + const tracker = new ReferenceTracker(context.getScope()); - if (property === 'NEGATIVE_INFINITY') { - problem.node = parent; - problem.data.identifier = '-Infinity'; - problem.fix = function * (fixer) { - yield fixer.replaceText(parent, 'Number.NEGATIVE_INFINITY'); - yield * fixSpaceAroundKeyword(fixer, parent, sourceCode); - }; - } else { - problem.fix = fixer => replaceReferenceIdentifier(node, `Number.${property}`, fixer, sourceCode); - } - - reported.add(node); - return problem; + yield * checkMethods({sourceCode, tracker}); + yield * checkProperties({sourceCode, tracker, checkInfinity}); }, }; }; diff --git a/test/prefer-number-properties.mjs b/test/prefer-number-properties.mjs index a6dc668778..61efa8ed8d 100644 --- a/test/prefer-number-properties.mjs +++ b/test/prefer-number-properties.mjs @@ -346,7 +346,11 @@ test.typescript({ }); test.snapshot({ - valid: [], + valid: [ + 'const foo = ++Infinity;', + 'const foo = --Infinity;', + 'const foo = -(--Infinity);', + ], invalid: [ 'const foo = {[NaN]: 1}', 'const foo = {[NaN]() {}}', @@ -369,12 +373,9 @@ test.snapshot({ 'const foo = -Infinity.toString();', 'const foo = (-Infinity).toString();', 'const foo = +Infinity;', - 'const foo = ++Infinity;', 'const foo = +-Infinity;', 'const foo = -Infinity;', - 'const foo = --Infinity;', 'const foo = -(-Infinity);', - 'const foo = -(--Infinity);', 'const foo = 1 - Infinity;', 'const foo = 1 - -Infinity;', 'const isPositiveZero = value => value === 0 && 1 / value === Infinity;', @@ -389,5 +390,16 @@ test.snapshot({ // Space after keywords 'function foo() {return-Infinity}', + + 'globalThis.isNaN(foo);', + 'global.isNaN(foo);', + 'window.isNaN(foo);', + 'self.isNaN(foo);', + 'globalThis.parseFloat(foo);', + 'global.parseFloat(foo);', + 'window.parseFloat(foo);', + 'self.parseFloat(foo);', + 'globalThis.NaN', + '-globalThis.Infinity', ], }); diff --git a/test/snapshots/prefer-number-properties.mjs.md b/test/snapshots/prefer-number-properties.mjs.md index 3a4374291e..1d1515616f 100644 --- a/test/snapshots/prefer-number-properties.mjs.md +++ b/test/snapshots/prefer-number-properties.mjs.md @@ -339,22 +339,6 @@ Generated by [AVA](https://avajs.dev). ` ## Invalid #21 - 1 | const foo = ++Infinity; - -> Output - - `␊ - 1 | const foo = ++Number.POSITIVE_INFINITY;␊ - ` - -> Error 1/1 - - `␊ - > 1 | const foo = ++Infinity;␊ - | ^^^^^^^^ Prefer \`Number.POSITIVE_INFINITY\` over \`Infinity\`.␊ - ` - -## Invalid #22 1 | const foo = +-Infinity; > Output @@ -370,7 +354,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^ Prefer \`Number.NEGATIVE_INFINITY\` over \`-Infinity\`.␊ ` -## Invalid #23 +## Invalid #22 1 | const foo = -Infinity; > Output @@ -386,23 +370,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^ Prefer \`Number.NEGATIVE_INFINITY\` over \`-Infinity\`.␊ ` -## Invalid #24 - 1 | const foo = --Infinity; - -> Output - - `␊ - 1 | const foo = --Number.POSITIVE_INFINITY;␊ - ` - -> Error 1/1 - - `␊ - > 1 | const foo = --Infinity;␊ - | ^^^^^^^^ Prefer \`Number.POSITIVE_INFINITY\` over \`Infinity\`.␊ - ` - -## Invalid #25 +## Invalid #23 1 | const foo = -(-Infinity); > Output @@ -418,23 +386,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^ Prefer \`Number.NEGATIVE_INFINITY\` over \`-Infinity\`.␊ ` -## Invalid #26 - 1 | const foo = -(--Infinity); - -> Output - - `␊ - 1 | const foo = -(--Number.POSITIVE_INFINITY);␊ - ` - -> Error 1/1 - - `␊ - > 1 | const foo = -(--Infinity);␊ - | ^^^^^^^^ Prefer \`Number.POSITIVE_INFINITY\` over \`Infinity\`.␊ - ` - -## Invalid #27 +## Invalid #24 1 | const foo = 1 - Infinity; > Output @@ -450,7 +402,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^ Prefer \`Number.POSITIVE_INFINITY\` over \`Infinity\`.␊ ` -## Invalid #28 +## Invalid #25 1 | const foo = 1 - -Infinity; > Output @@ -466,7 +418,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^ Prefer \`Number.NEGATIVE_INFINITY\` over \`-Infinity\`.␊ ` -## Invalid #29 +## Invalid #26 1 | const isPositiveZero = value => value === 0 && 1 / value === Infinity; > Output @@ -482,7 +434,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^ Prefer \`Number.POSITIVE_INFINITY\` over \`Infinity\`.␊ ` -## Invalid #30 +## Invalid #27 1 | const isNegativeZero = value => value === 0 && 1 / value === -Infinity; > Output @@ -498,7 +450,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^ Prefer \`Number.NEGATIVE_INFINITY\` over \`-Infinity\`.␊ ` -## Invalid #31 +## Invalid #28 1 | const {a = NaN} = {}; > Output @@ -514,7 +466,7 @@ Generated by [AVA](https://avajs.dev). | ^^^ Prefer \`Number.NaN\` over \`NaN\`.␊ ` -## Invalid #32 +## Invalid #29 1 | const {[NaN]: a = NaN} = {}; > Output @@ -537,7 +489,7 @@ Generated by [AVA](https://avajs.dev). | ^^^ Prefer \`Number.NaN\` over \`NaN\`.␊ ` -## Invalid #33 +## Invalid #30 1 | const [a = NaN] = []; > Output @@ -553,7 +505,7 @@ Generated by [AVA](https://avajs.dev). | ^^^ Prefer \`Number.NaN\` over \`NaN\`.␊ ` -## Invalid #34 +## Invalid #31 1 | function foo({a = NaN}) {} > Output @@ -569,7 +521,7 @@ Generated by [AVA](https://avajs.dev). | ^^^ Prefer \`Number.NaN\` over \`NaN\`.␊ ` -## Invalid #35 +## Invalid #32 1 | function foo({[NaN]: a = NaN}) {} > Output @@ -592,7 +544,7 @@ Generated by [AVA](https://avajs.dev). | ^^^ Prefer \`Number.NaN\` over \`NaN\`.␊ ` -## Invalid #36 +## Invalid #33 1 | function foo([a = NaN]) {} > Output @@ -608,7 +560,7 @@ Generated by [AVA](https://avajs.dev). | ^^^ Prefer \`Number.NaN\` over \`NaN\`.␊ ` -## Invalid #37 +## Invalid #34 1 | function foo() {return-Infinity} > Output @@ -623,3 +575,155 @@ Generated by [AVA](https://avajs.dev). > 1 | function foo() {return-Infinity}␊ | ^^^^^^^^^ Prefer \`Number.NEGATIVE_INFINITY\` over \`-Infinity\`.␊ ` + +## Invalid #35 + 1 | globalThis.isNaN(foo); + +> Error 1/1 + + `␊ + > 1 | globalThis.isNaN(foo);␊ + | ^^^^^^^^^^^^^^^^ Prefer \`Number.isNaN()\` over \`isNaN()\`.␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Replace \`isNaN()\` with \`Number.isNaN()\`.␊ + 1 | Number.isNaN(foo);␊ + ` + +## Invalid #36 + 1 | global.isNaN(foo); + +> Error 1/1 + + `␊ + > 1 | global.isNaN(foo);␊ + | ^^^^^^^^^^^^ Prefer \`Number.isNaN()\` over \`isNaN()\`.␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Replace \`isNaN()\` with \`Number.isNaN()\`.␊ + 1 | Number.isNaN(foo);␊ + ` + +## Invalid #37 + 1 | window.isNaN(foo); + +> Error 1/1 + + `␊ + > 1 | window.isNaN(foo);␊ + | ^^^^^^^^^^^^ Prefer \`Number.isNaN()\` over \`isNaN()\`.␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Replace \`isNaN()\` with \`Number.isNaN()\`.␊ + 1 | Number.isNaN(foo);␊ + ` + +## Invalid #38 + 1 | self.isNaN(foo); + +> Error 1/1 + + `␊ + > 1 | self.isNaN(foo);␊ + | ^^^^^^^^^^ Prefer \`Number.isNaN()\` over \`isNaN()\`.␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Replace \`isNaN()\` with \`Number.isNaN()\`.␊ + 1 | Number.isNaN(foo);␊ + ` + +## Invalid #39 + 1 | globalThis.parseFloat(foo); + +> Output + + `␊ + 1 | Number.parseFloat(foo);␊ + ` + +> Error 1/1 + + `␊ + > 1 | globalThis.parseFloat(foo);␊ + | ^^^^^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat()\` over \`parseFloat()\`.␊ + ` + +## Invalid #40 + 1 | global.parseFloat(foo); + +> Output + + `␊ + 1 | Number.parseFloat(foo);␊ + ` + +> Error 1/1 + + `␊ + > 1 | global.parseFloat(foo);␊ + | ^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat()\` over \`parseFloat()\`.␊ + ` + +## Invalid #41 + 1 | window.parseFloat(foo); + +> Output + + `␊ + 1 | Number.parseFloat(foo);␊ + ` + +> Error 1/1 + + `␊ + > 1 | window.parseFloat(foo);␊ + | ^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat()\` over \`parseFloat()\`.␊ + ` + +## Invalid #42 + 1 | self.parseFloat(foo); + +> Output + + `␊ + 1 | Number.parseFloat(foo);␊ + ` + +> Error 1/1 + + `␊ + > 1 | self.parseFloat(foo);␊ + | ^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat()\` over \`parseFloat()\`.␊ + ` + +## Invalid #43 + 1 | globalThis.NaN + +> Output + + `␊ + 1 | Number.NaN␊ + ` + +> Error 1/1 + + `␊ + > 1 | globalThis.NaN␊ + | ^^^^^^^^^^^^^^ Prefer \`Number.NaN\` over \`NaN\`.␊ + ` + +## Invalid #44 + 1 | -globalThis.Infinity + +> Output + + `␊ + 1 | Number.NEGATIVE_INFINITY␊ + ` + +> Error 1/1 + + `␊ + > 1 | -globalThis.Infinity␊ + | ^^^^^^^^^^^^^^^^^^^^ Prefer \`Number.NEGATIVE_INFINITY\` over \`-Infinity\`.␊ + ` diff --git a/test/snapshots/prefer-number-properties.mjs.snap b/test/snapshots/prefer-number-properties.mjs.snap index b94f4d51b5..b87e45ab44 100644 Binary files a/test/snapshots/prefer-number-properties.mjs.snap and b/test/snapshots/prefer-number-properties.mjs.snap differ