diff --git a/rules/prefer-number-properties.js b/rules/prefer-number-properties.js index f1be2b34f8..6bf677ad4a 100644 --- a/rules/prefer-number-properties.js +++ b/rules/prefer-number-properties.js @@ -3,20 +3,21 @@ const {ReferenceTracker} = require('eslint-utils'); const {replaceReferenceIdentifier} = require('./fix/index.js'); const {fixSpaceAroundKeyword} = require('./fix/index.js'); -const METHOD_ERROR_MESSAGE_ID = 'method-error'; -const METHOD_SUGGESTION_MESSAGE_ID = 'method-suggestion'; -const PROPERTY_ERROR_MESSAGE_ID = 'property-error'; +const MESSAGE_ID_ERROR = 'error'; +const MESSAGE_ID_SUGGESTION = 'suggestion'; const messages = { - [METHOD_ERROR_MESSAGE_ID]: 'Prefer `Number.{{name}}()` over `{{name}}()`.', - [METHOD_SUGGESTION_MESSAGE_ID]: 'Replace `{{name}}()` with `Number.{{name}}()`.', - [PROPERTY_ERROR_MESSAGE_ID]: 'Prefer `Number.{{property}}` over `{{identifier}}`.', + [MESSAGE_ID_ERROR]: 'Prefer `Number.{{property}}` over `{{description}}`.', + [MESSAGE_ID_SUGGESTION]: 'Replace `{{description}}` with `Number.{{property}}`.', }; -const methods = { - // Safe +const globalObjects = { + // Safe to replace with `Number` properties parseInt: true, parseFloat: true, - // Unsafe + NaN: true, + Infinity: true, + + // Unsafe to replace with `Number` properties isNaN: false, isFinite: false, }; @@ -26,47 +27,14 @@ const isNegative = 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}) { + let names = Object.keys(globalObjects); + if (!checkInfinity) { + names = names.filter(name => name !== 'Infinity'); } -} -function * checkProperties({sourceCode, tracker, checkInfinity}) { - const properties = checkInfinity ? ['NaN', 'Infinity'] : ['NaN']; const traceMap = Object.fromEntries( - properties.map(name => [name, {[ReferenceTracker.READ]: true}]), + names.map(name => [name, {[ReferenceTracker.READ]: true}]), ); for (const {node, path: [name]} of tracker.iterateGlobalReferences(traceMap)) { @@ -79,22 +47,38 @@ function * checkProperties({sourceCode, tracker, checkInfinity}) { const problem = { node, - messageId: PROPERTY_ERROR_MESSAGE_ID, + messageId: MESSAGE_ID_ERROR, data: { - identifier: name, + description: name, property, }, }; if (property === 'NEGATIVE_INFINITY') { problem.node = parent; - problem.data.identifier = '-Infinity'; + problem.data.description = '-Infinity'; problem.fix = function * (fixer) { yield fixer.replaceText(parent, 'Number.NEGATIVE_INFINITY'); yield * fixSpaceAroundKeyword(fixer, parent, sourceCode); }; + + yield problem; + continue; + } + + const fix = fixer => replaceReferenceIdentifier(node, `Number.${property}`, fixer, sourceCode); + const isSafeToFix = globalObjects[name]; + + if (isSafeToFix) { + problem.fix = fix; } else { - problem.fix = fixer => replaceReferenceIdentifier(node, `Number.${property}`, fixer, sourceCode); + problem.suggest = [ + { + messageId: MESSAGE_ID_SUGGESTION, + data: problem.data, + fix, + }, + ]; } yield problem; @@ -115,7 +99,6 @@ const create = context => { const sourceCode = context.getSourceCode(); const tracker = new ReferenceTracker(context.getScope()); - yield * checkMethods({sourceCode, tracker}); yield * checkProperties({sourceCode, tracker, checkInfinity}); }, }; diff --git a/test/prefer-number-properties.mjs b/test/prefer-number-properties.mjs index 61efa8ed8d..68384b1b71 100644 --- a/test/prefer-number-properties.mjs +++ b/test/prefer-number-properties.mjs @@ -3,9 +3,8 @@ import {getTester} from './utils/test.mjs'; const {test} = getTester(import.meta); -const METHOD_ERROR_MESSAGE_ID = 'method-error'; -const METHOD_SUGGESTION_MESSAGE_ID = 'method-suggestion'; -const PROPERTY_ERROR_MESSAGE_ID = 'property-error'; +const MESSAGE_ID_ERROR = 'error'; +const MESSAGE_ID_SUGGESTION = 'suggestion'; const methods = { parseInt: { @@ -30,17 +29,19 @@ const createError = (name, suggestionOutput) => { const {safe} = methods[name]; const error = { - messageId: METHOD_ERROR_MESSAGE_ID, + messageId: MESSAGE_ID_ERROR, data: { - name, + description: name, + property: name, }, }; const suggestions = safe ? undefined : [ { - messageId: METHOD_SUGGESTION_MESSAGE_ID, + messageId: MESSAGE_ID_SUGGESTION, data: { - name, + description: name, + property: name, }, output: suggestionOutput, }, @@ -75,12 +76,6 @@ test({ 'Number.isNaN(10);', 'Number.isFinite(10);', - // Not call - ...Object.keys(methods), - - // New - ...Object.values(methods).map(({code}) => `new ${code}`), - // Shadowed ...Object.entries(methods).map(([name, {code}]) => outdent` const ${name} = function() {}; @@ -161,9 +156,9 @@ test({ // `NaN` and `Infinity` const errorNaN = [ { - messageId: PROPERTY_ERROR_MESSAGE_ID, + messageId: MESSAGE_ID_ERROR, data: { - identifier: 'NaN', + description: 'NaN', property: 'NaN', }, }, @@ -401,5 +396,15 @@ test.snapshot({ 'self.parseFloat(foo);', 'globalThis.NaN', '-globalThis.Infinity', + + // Not a call + outdent` + const options = { + normalize: parseFloat, + parseInt, + }; + + run(foo, options); + `, ], }); diff --git a/test/snapshots/prefer-number-properties.mjs.md b/test/snapshots/prefer-number-properties.mjs.md index 1d1515616f..93c1778e7d 100644 --- a/test/snapshots/prefer-number-properties.mjs.md +++ b/test/snapshots/prefer-number-properties.mjs.md @@ -583,10 +583,10 @@ Generated by [AVA](https://avajs.dev). `␊ > 1 | globalThis.isNaN(foo);␊ - | ^^^^^^^^^^^^^^^^ Prefer \`Number.isNaN()\` over \`isNaN()\`.␊ + | ^^^^^^^^^^^^^^^^ Prefer \`Number.isNaN\` over \`isNaN\`.␊ ␊ --------------------------------------------------------------------------------␊ - Suggestion 1/1: Replace \`isNaN()\` with \`Number.isNaN()\`.␊ + Suggestion 1/1: Replace \`isNaN\` with \`Number.isNaN\`.␊ 1 | Number.isNaN(foo);␊ ` @@ -597,10 +597,10 @@ Generated by [AVA](https://avajs.dev). `␊ > 1 | global.isNaN(foo);␊ - | ^^^^^^^^^^^^ Prefer \`Number.isNaN()\` over \`isNaN()\`.␊ + | ^^^^^^^^^^^^ Prefer \`Number.isNaN\` over \`isNaN\`.␊ ␊ --------------------------------------------------------------------------------␊ - Suggestion 1/1: Replace \`isNaN()\` with \`Number.isNaN()\`.␊ + Suggestion 1/1: Replace \`isNaN\` with \`Number.isNaN\`.␊ 1 | Number.isNaN(foo);␊ ` @@ -611,10 +611,10 @@ Generated by [AVA](https://avajs.dev). `␊ > 1 | window.isNaN(foo);␊ - | ^^^^^^^^^^^^ Prefer \`Number.isNaN()\` over \`isNaN()\`.␊ + | ^^^^^^^^^^^^ Prefer \`Number.isNaN\` over \`isNaN\`.␊ ␊ --------------------------------------------------------------------------------␊ - Suggestion 1/1: Replace \`isNaN()\` with \`Number.isNaN()\`.␊ + Suggestion 1/1: Replace \`isNaN\` with \`Number.isNaN\`.␊ 1 | Number.isNaN(foo);␊ ` @@ -625,10 +625,10 @@ Generated by [AVA](https://avajs.dev). `␊ > 1 | self.isNaN(foo);␊ - | ^^^^^^^^^^ Prefer \`Number.isNaN()\` over \`isNaN()\`.␊ + | ^^^^^^^^^^ Prefer \`Number.isNaN\` over \`isNaN\`.␊ ␊ --------------------------------------------------------------------------------␊ - Suggestion 1/1: Replace \`isNaN()\` with \`Number.isNaN()\`.␊ + Suggestion 1/1: Replace \`isNaN\` with \`Number.isNaN\`.␊ 1 | Number.isNaN(foo);␊ ` @@ -645,7 +645,7 @@ Generated by [AVA](https://avajs.dev). `␊ > 1 | globalThis.parseFloat(foo);␊ - | ^^^^^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat()\` over \`parseFloat()\`.␊ + | ^^^^^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat\` over \`parseFloat\`.␊ ` ## Invalid #40 @@ -661,7 +661,7 @@ Generated by [AVA](https://avajs.dev). `␊ > 1 | global.parseFloat(foo);␊ - | ^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat()\` over \`parseFloat()\`.␊ + | ^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat\` over \`parseFloat\`.␊ ` ## Invalid #41 @@ -677,7 +677,7 @@ Generated by [AVA](https://avajs.dev). `␊ > 1 | window.parseFloat(foo);␊ - | ^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat()\` over \`parseFloat()\`.␊ + | ^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat\` over \`parseFloat\`.␊ ` ## Invalid #42 @@ -693,7 +693,7 @@ Generated by [AVA](https://avajs.dev). `␊ > 1 | self.parseFloat(foo);␊ - | ^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat()\` over \`parseFloat()\`.␊ + | ^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat\` over \`parseFloat\`.␊ ` ## Invalid #43 @@ -727,3 +727,46 @@ Generated by [AVA](https://avajs.dev). > 1 | -globalThis.Infinity␊ | ^^^^^^^^^^^^^^^^^^^^ Prefer \`Number.NEGATIVE_INFINITY\` over \`-Infinity\`.␊ ` + +## Invalid #45 + 1 | const options = { + 2 | normalize: parseFloat, + 3 | parseInt, + 4 | }; + 5 | + 6 | run(foo, options); + +> Output + + `␊ + 1 | const options = {␊ + 2 | normalize: Number.parseFloat,␊ + 3 | parseInt: Number.parseInt,␊ + 4 | };␊ + 5 |␊ + 6 | run(foo, options);␊ + ` + +> Error 1/2 + + `␊ + 1 | const options = {␊ + > 2 | normalize: parseFloat,␊ + | ^^^^^^^^^^ Prefer \`Number.parseFloat\` over \`parseFloat\`.␊ + 3 | parseInt,␊ + 4 | };␊ + 5 |␊ + 6 | run(foo, options);␊ + ` + +> Error 2/2 + + `␊ + 1 | const options = {␊ + 2 | normalize: parseFloat,␊ + > 3 | parseInt,␊ + | ^^^^^^^^ Prefer \`Number.parseInt\` over \`parseInt\`.␊ + 4 | };␊ + 5 |␊ + 6 | run(foo, options);␊ + ` diff --git a/test/snapshots/prefer-number-properties.mjs.snap b/test/snapshots/prefer-number-properties.mjs.snap index b87e45ab44..e38e01730a 100644 Binary files a/test/snapshots/prefer-number-properties.mjs.snap and b/test/snapshots/prefer-number-properties.mjs.snap differ