Skip to content

Commit 51d7e06

Browse files
authored
prefer-number-properties: Check any use of global functions (#1834)
1 parent 80c4af2 commit 51d7e06

4 files changed

+110
-79
lines changed

rules/prefer-number-properties.js

+35-52
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,21 @@ const {ReferenceTracker} = require('eslint-utils');
33
const {replaceReferenceIdentifier} = require('./fix/index.js');
44
const {fixSpaceAroundKeyword} = require('./fix/index.js');
55

6-
const METHOD_ERROR_MESSAGE_ID = 'method-error';
7-
const METHOD_SUGGESTION_MESSAGE_ID = 'method-suggestion';
8-
const PROPERTY_ERROR_MESSAGE_ID = 'property-error';
6+
const MESSAGE_ID_ERROR = 'error';
7+
const MESSAGE_ID_SUGGESTION = 'suggestion';
98
const messages = {
10-
[METHOD_ERROR_MESSAGE_ID]: 'Prefer `Number.{{name}}()` over `{{name}}()`.',
11-
[METHOD_SUGGESTION_MESSAGE_ID]: 'Replace `{{name}}()` with `Number.{{name}}()`.',
12-
[PROPERTY_ERROR_MESSAGE_ID]: 'Prefer `Number.{{property}}` over `{{identifier}}`.',
9+
[MESSAGE_ID_ERROR]: 'Prefer `Number.{{property}}` over `{{description}}`.',
10+
[MESSAGE_ID_SUGGESTION]: 'Replace `{{description}}` with `Number.{{property}}`.',
1311
};
1412

15-
const methods = {
16-
// Safe
13+
const globalObjects = {
14+
// Safe to replace with `Number` properties
1715
parseInt: true,
1816
parseFloat: true,
19-
// Unsafe
17+
NaN: true,
18+
Infinity: true,
19+
20+
// Unsafe to replace with `Number` properties
2021
isNaN: false,
2122
isFinite: false,
2223
};
@@ -26,47 +27,14 @@ const isNegative = node => {
2627
return parent && parent.type === 'UnaryExpression' && parent.operator === '-' && parent.argument === node;
2728
};
2829

29-
function * checkMethods({sourceCode, tracker}) {
30-
const traceMap = Object.fromEntries(
31-
Object.keys(methods).map(name => [name, {[ReferenceTracker.CALL]: true}]),
32-
);
33-
34-
for (const {node: callExpression, path: [name]} of tracker.iterateGlobalReferences(traceMap)) {
35-
const node = callExpression.callee;
36-
const isSafe = methods[name];
37-
38-
const problem = {
39-
node,
40-
messageId: METHOD_ERROR_MESSAGE_ID,
41-
data: {
42-
name,
43-
},
44-
};
45-
46-
const fix = fixer => replaceReferenceIdentifier(node, `Number.${name}`, fixer, sourceCode);
47-
48-
if (isSafe) {
49-
problem.fix = fix;
50-
} else {
51-
problem.suggest = [
52-
{
53-
messageId: METHOD_SUGGESTION_MESSAGE_ID,
54-
data: {
55-
name,
56-
},
57-
fix,
58-
},
59-
];
60-
}
61-
62-
yield problem;
30+
function * checkProperties({sourceCode, tracker, checkInfinity}) {
31+
let names = Object.keys(globalObjects);
32+
if (!checkInfinity) {
33+
names = names.filter(name => name !== 'Infinity');
6334
}
64-
}
6535

66-
function * checkProperties({sourceCode, tracker, checkInfinity}) {
67-
const properties = checkInfinity ? ['NaN', 'Infinity'] : ['NaN'];
6836
const traceMap = Object.fromEntries(
69-
properties.map(name => [name, {[ReferenceTracker.READ]: true}]),
37+
names.map(name => [name, {[ReferenceTracker.READ]: true}]),
7038
);
7139

7240
for (const {node, path: [name]} of tracker.iterateGlobalReferences(traceMap)) {
@@ -79,22 +47,38 @@ function * checkProperties({sourceCode, tracker, checkInfinity}) {
7947

8048
const problem = {
8149
node,
82-
messageId: PROPERTY_ERROR_MESSAGE_ID,
50+
messageId: MESSAGE_ID_ERROR,
8351
data: {
84-
identifier: name,
52+
description: name,
8553
property,
8654
},
8755
};
8856

8957
if (property === 'NEGATIVE_INFINITY') {
9058
problem.node = parent;
91-
problem.data.identifier = '-Infinity';
59+
problem.data.description = '-Infinity';
9260
problem.fix = function * (fixer) {
9361
yield fixer.replaceText(parent, 'Number.NEGATIVE_INFINITY');
9462
yield * fixSpaceAroundKeyword(fixer, parent, sourceCode);
9563
};
64+
65+
yield problem;
66+
continue;
67+
}
68+
69+
const fix = fixer => replaceReferenceIdentifier(node, `Number.${property}`, fixer, sourceCode);
70+
const isSafeToFix = globalObjects[name];
71+
72+
if (isSafeToFix) {
73+
problem.fix = fix;
9674
} else {
97-
problem.fix = fixer => replaceReferenceIdentifier(node, `Number.${property}`, fixer, sourceCode);
75+
problem.suggest = [
76+
{
77+
messageId: MESSAGE_ID_SUGGESTION,
78+
data: problem.data,
79+
fix,
80+
},
81+
];
9882
}
9983

10084
yield problem;
@@ -115,7 +99,6 @@ const create = context => {
11599
const sourceCode = context.getSourceCode();
116100
const tracker = new ReferenceTracker(context.getScope());
117101

118-
yield * checkMethods({sourceCode, tracker});
119102
yield * checkProperties({sourceCode, tracker, checkInfinity});
120103
},
121104
};

test/prefer-number-properties.mjs

+20-15
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import {getTester} from './utils/test.mjs';
33

44
const {test} = getTester(import.meta);
55

6-
const METHOD_ERROR_MESSAGE_ID = 'method-error';
7-
const METHOD_SUGGESTION_MESSAGE_ID = 'method-suggestion';
8-
const PROPERTY_ERROR_MESSAGE_ID = 'property-error';
6+
const MESSAGE_ID_ERROR = 'error';
7+
const MESSAGE_ID_SUGGESTION = 'suggestion';
98

109
const methods = {
1110
parseInt: {
@@ -30,17 +29,19 @@ const createError = (name, suggestionOutput) => {
3029
const {safe} = methods[name];
3130

3231
const error = {
33-
messageId: METHOD_ERROR_MESSAGE_ID,
32+
messageId: MESSAGE_ID_ERROR,
3433
data: {
35-
name,
34+
description: name,
35+
property: name,
3636
},
3737
};
3838

3939
const suggestions = safe ? undefined : [
4040
{
41-
messageId: METHOD_SUGGESTION_MESSAGE_ID,
41+
messageId: MESSAGE_ID_SUGGESTION,
4242
data: {
43-
name,
43+
description: name,
44+
property: name,
4445
},
4546
output: suggestionOutput,
4647
},
@@ -75,12 +76,6 @@ test({
7576
'Number.isNaN(10);',
7677
'Number.isFinite(10);',
7778

78-
// Not call
79-
...Object.keys(methods),
80-
81-
// New
82-
...Object.values(methods).map(({code}) => `new ${code}`),
83-
8479
// Shadowed
8580
...Object.entries(methods).map(([name, {code}]) => outdent`
8681
const ${name} = function() {};
@@ -161,9 +156,9 @@ test({
161156
// `NaN` and `Infinity`
162157
const errorNaN = [
163158
{
164-
messageId: PROPERTY_ERROR_MESSAGE_ID,
159+
messageId: MESSAGE_ID_ERROR,
165160
data: {
166-
identifier: 'NaN',
161+
description: 'NaN',
167162
property: 'NaN',
168163
},
169164
},
@@ -401,5 +396,15 @@ test.snapshot({
401396
'self.parseFloat(foo);',
402397
'globalThis.NaN',
403398
'-globalThis.Infinity',
399+
400+
// Not a call
401+
outdent`
402+
const options = {
403+
normalize: parseFloat,
404+
parseInt,
405+
};
406+
407+
run(foo, options);
408+
`,
404409
],
405410
});

test/snapshots/prefer-number-properties.mjs.md

+55-12
Original file line numberDiff line numberDiff line change
@@ -583,10 +583,10 @@ Generated by [AVA](https://avajs.dev).
583583
584584
`␊
585585
> 1 | globalThis.isNaN(foo);␊
586-
| ^^^^^^^^^^^^^^^^ Prefer \`Number.isNaN()\` over \`isNaN()\`.␊
586+
| ^^^^^^^^^^^^^^^^ Prefer \`Number.isNaN\` over \`isNaN\`.␊
587587
588588
--------------------------------------------------------------------------------␊
589-
Suggestion 1/1: Replace \`isNaN()\` with \`Number.isNaN()\`.␊
589+
Suggestion 1/1: Replace \`isNaN\` with \`Number.isNaN\`.␊
590590
1 | Number.isNaN(foo);␊
591591
`
592592

@@ -597,10 +597,10 @@ Generated by [AVA](https://avajs.dev).
597597
598598
`␊
599599
> 1 | global.isNaN(foo);␊
600-
| ^^^^^^^^^^^^ Prefer \`Number.isNaN()\` over \`isNaN()\`.␊
600+
| ^^^^^^^^^^^^ Prefer \`Number.isNaN\` over \`isNaN\`.␊
601601
602602
--------------------------------------------------------------------------------␊
603-
Suggestion 1/1: Replace \`isNaN()\` with \`Number.isNaN()\`.␊
603+
Suggestion 1/1: Replace \`isNaN\` with \`Number.isNaN\`.␊
604604
1 | Number.isNaN(foo);␊
605605
`
606606

@@ -611,10 +611,10 @@ Generated by [AVA](https://avajs.dev).
611611
612612
`␊
613613
> 1 | window.isNaN(foo);␊
614-
| ^^^^^^^^^^^^ Prefer \`Number.isNaN()\` over \`isNaN()\`.␊
614+
| ^^^^^^^^^^^^ Prefer \`Number.isNaN\` over \`isNaN\`.␊
615615
616616
--------------------------------------------------------------------------------␊
617-
Suggestion 1/1: Replace \`isNaN()\` with \`Number.isNaN()\`.␊
617+
Suggestion 1/1: Replace \`isNaN\` with \`Number.isNaN\`.␊
618618
1 | Number.isNaN(foo);␊
619619
`
620620

@@ -625,10 +625,10 @@ Generated by [AVA](https://avajs.dev).
625625
626626
`␊
627627
> 1 | self.isNaN(foo);␊
628-
| ^^^^^^^^^^ Prefer \`Number.isNaN()\` over \`isNaN()\`.␊
628+
| ^^^^^^^^^^ Prefer \`Number.isNaN\` over \`isNaN\`.␊
629629
630630
--------------------------------------------------------------------------------␊
631-
Suggestion 1/1: Replace \`isNaN()\` with \`Number.isNaN()\`.␊
631+
Suggestion 1/1: Replace \`isNaN\` with \`Number.isNaN\`.␊
632632
1 | Number.isNaN(foo);␊
633633
`
634634

@@ -645,7 +645,7 @@ Generated by [AVA](https://avajs.dev).
645645
646646
`␊
647647
> 1 | globalThis.parseFloat(foo);␊
648-
| ^^^^^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat()\` over \`parseFloat()\`.␊
648+
| ^^^^^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat\` over \`parseFloat\`.␊
649649
`
650650

651651
## Invalid #40
@@ -661,7 +661,7 @@ Generated by [AVA](https://avajs.dev).
661661
662662
`␊
663663
> 1 | global.parseFloat(foo);␊
664-
| ^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat()\` over \`parseFloat()\`.␊
664+
| ^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat\` over \`parseFloat\`.␊
665665
`
666666

667667
## Invalid #41
@@ -677,7 +677,7 @@ Generated by [AVA](https://avajs.dev).
677677
678678
`␊
679679
> 1 | window.parseFloat(foo);␊
680-
| ^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat()\` over \`parseFloat()\`.␊
680+
| ^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat\` over \`parseFloat\`.␊
681681
`
682682

683683
## Invalid #42
@@ -693,7 +693,7 @@ Generated by [AVA](https://avajs.dev).
693693
694694
`␊
695695
> 1 | self.parseFloat(foo);␊
696-
| ^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat()\` over \`parseFloat()\`.␊
696+
| ^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat\` over \`parseFloat\`.␊
697697
`
698698

699699
## Invalid #43
@@ -727,3 +727,46 @@ Generated by [AVA](https://avajs.dev).
727727
> 1 | -globalThis.Infinity␊
728728
| ^^^^^^^^^^^^^^^^^^^^ Prefer \`Number.NEGATIVE_INFINITY\` over \`-Infinity\`.␊
729729
`
730+
731+
## Invalid #45
732+
1 | const options = {
733+
2 | normalize: parseFloat,
734+
3 | parseInt,
735+
4 | };
736+
5 |
737+
6 | run(foo, options);
738+
739+
> Output
740+
741+
`␊
742+
1 | const options = {␊
743+
2 | normalize: Number.parseFloat,␊
744+
3 | parseInt: Number.parseInt,␊
745+
4 | };␊
746+
5 |␊
747+
6 | run(foo, options);␊
748+
`
749+
750+
> Error 1/2
751+
752+
`␊
753+
1 | const options = {␊
754+
> 2 | normalize: parseFloat,␊
755+
| ^^^^^^^^^^ Prefer \`Number.parseFloat\` over \`parseFloat\`.␊
756+
3 | parseInt,␊
757+
4 | };␊
758+
5 |␊
759+
6 | run(foo, options);␊
760+
`
761+
762+
> Error 2/2
763+
764+
`␊
765+
1 | const options = {␊
766+
2 | normalize: parseFloat,␊
767+
> 3 | parseInt,␊
768+
| ^^^^^^^^ Prefer \`Number.parseInt\` over \`parseInt\`.␊
769+
4 | };␊
770+
5 |␊
771+
6 | run(foo, options);␊
772+
`
144 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)