Skip to content

Commit 80c4af2

Browse files
authored
prefer-number-properties: Detect usage via global object (#1832)
1 parent aee56fd commit 80c4af2

4 files changed

+265
-153
lines changed

rules/prefer-number-properties.js

+84-88
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
'use strict';
2-
const isShadowed = require('./utils/is-shadowed.js');
3-
const {
4-
referenceIdentifierSelector,
5-
callExpressionSelector,
6-
} = require('./selectors/index.js');
2+
const {ReferenceTracker} = require('eslint-utils');
73
const {replaceReferenceIdentifier} = require('./fix/index.js');
84
const {fixSpaceAroundKeyword} = require('./fix/index.js');
95

@@ -25,102 +21,102 @@ const methods = {
2521
isFinite: false,
2622
};
2723

28-
const methodsSelector = [
29-
callExpressionSelector(Object.keys(methods)),
30-
' > ',
31-
'.callee',
32-
].join('');
33-
34-
const propertiesSelector = referenceIdentifierSelector(['NaN', 'Infinity']);
35-
3624
const isNegative = node => {
3725
const {parent} = node;
3826
return parent && parent.type === 'UnaryExpression' && parent.operator === '-' && parent.argument === node;
3927
};
4028

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;
63+
}
64+
}
65+
66+
function * checkProperties({sourceCode, tracker, checkInfinity}) {
67+
const properties = checkInfinity ? ['NaN', 'Infinity'] : ['NaN'];
68+
const traceMap = Object.fromEntries(
69+
properties.map(name => [name, {[ReferenceTracker.READ]: true}]),
70+
);
71+
72+
for (const {node, path: [name]} of tracker.iterateGlobalReferences(traceMap)) {
73+
const {parent} = node;
74+
75+
let property = name;
76+
if (name === 'Infinity') {
77+
property = isNegative(node) ? 'NEGATIVE_INFINITY' : 'POSITIVE_INFINITY';
78+
}
79+
80+
const problem = {
81+
node,
82+
messageId: PROPERTY_ERROR_MESSAGE_ID,
83+
data: {
84+
identifier: name,
85+
property,
86+
},
87+
};
88+
89+
if (property === 'NEGATIVE_INFINITY') {
90+
problem.node = parent;
91+
problem.data.identifier = '-Infinity';
92+
problem.fix = function * (fixer) {
93+
yield fixer.replaceText(parent, 'Number.NEGATIVE_INFINITY');
94+
yield * fixSpaceAroundKeyword(fixer, parent, sourceCode);
95+
};
96+
} else {
97+
problem.fix = fixer => replaceReferenceIdentifier(node, `Number.${property}`, fixer, sourceCode);
98+
}
99+
100+
yield problem;
101+
}
102+
}
103+
41104
/** @param {import('eslint').Rule.RuleContext} context */
42105
const create = context => {
43-
const sourceCode = context.getSourceCode();
44-
const options = {
106+
const {
107+
checkInfinity,
108+
} = {
45109
checkInfinity: true,
46110
...context.options[0],
47111
};
48112

49-
// Cache `NaN` and `Infinity` in `foo = {NaN, Infinity}`
50-
const reported = new WeakSet();
51-
52113
return {
53-
[methodsSelector](node) {
54-
if (isShadowed(context.getScope(), node)) {
55-
return;
56-
}
57-
58-
const {name} = node;
59-
const isSafe = methods[name];
60-
61-
const problem = {
62-
node,
63-
messageId: METHOD_ERROR_MESSAGE_ID,
64-
data: {
65-
name,
66-
},
67-
};
68-
69-
const fix = fixer => replaceReferenceIdentifier(node, `Number.${name}`, fixer, sourceCode);
70-
71-
if (isSafe) {
72-
problem.fix = fix;
73-
} else {
74-
problem.suggest = [
75-
{
76-
messageId: METHOD_SUGGESTION_MESSAGE_ID,
77-
data: {
78-
name,
79-
},
80-
fix,
81-
},
82-
];
83-
}
84-
85-
return problem;
86-
},
87-
[propertiesSelector](node) {
88-
if (reported.has(node) || isShadowed(context.getScope(), node)) {
89-
return;
90-
}
91-
92-
const {name, parent} = node;
93-
if (name === 'Infinity' && !options.checkInfinity) {
94-
return;
95-
}
96-
97-
let property = name;
98-
if (name === 'Infinity') {
99-
property = isNegative(node) ? 'NEGATIVE_INFINITY' : 'POSITIVE_INFINITY';
100-
}
101-
102-
const problem = {
103-
node,
104-
messageId: PROPERTY_ERROR_MESSAGE_ID,
105-
data: {
106-
identifier: name,
107-
property,
108-
},
109-
};
114+
* 'Program:exit'() {
115+
const sourceCode = context.getSourceCode();
116+
const tracker = new ReferenceTracker(context.getScope());
110117

111-
if (property === 'NEGATIVE_INFINITY') {
112-
problem.node = parent;
113-
problem.data.identifier = '-Infinity';
114-
problem.fix = function * (fixer) {
115-
yield fixer.replaceText(parent, 'Number.NEGATIVE_INFINITY');
116-
yield * fixSpaceAroundKeyword(fixer, parent, sourceCode);
117-
};
118-
} else {
119-
problem.fix = fixer => replaceReferenceIdentifier(node, `Number.${property}`, fixer, sourceCode);
120-
}
121-
122-
reported.add(node);
123-
return problem;
118+
yield * checkMethods({sourceCode, tracker});
119+
yield * checkProperties({sourceCode, tracker, checkInfinity});
124120
},
125121
};
126122
};

test/prefer-number-properties.mjs

+16-4
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,11 @@ test.typescript({
346346
});
347347

348348
test.snapshot({
349-
valid: [],
349+
valid: [
350+
'const foo = ++Infinity;',
351+
'const foo = --Infinity;',
352+
'const foo = -(--Infinity);',
353+
],
350354
invalid: [
351355
'const foo = {[NaN]: 1}',
352356
'const foo = {[NaN]() {}}',
@@ -369,12 +373,9 @@ test.snapshot({
369373
'const foo = -Infinity.toString();',
370374
'const foo = (-Infinity).toString();',
371375
'const foo = +Infinity;',
372-
'const foo = ++Infinity;',
373376
'const foo = +-Infinity;',
374377
'const foo = -Infinity;',
375-
'const foo = --Infinity;',
376378
'const foo = -(-Infinity);',
377-
'const foo = -(--Infinity);',
378379
'const foo = 1 - Infinity;',
379380
'const foo = 1 - -Infinity;',
380381
'const isPositiveZero = value => value === 0 && 1 / value === Infinity;',
@@ -389,5 +390,16 @@ test.snapshot({
389390

390391
// Space after keywords
391392
'function foo() {return-Infinity}',
393+
394+
'globalThis.isNaN(foo);',
395+
'global.isNaN(foo);',
396+
'window.isNaN(foo);',
397+
'self.isNaN(foo);',
398+
'globalThis.parseFloat(foo);',
399+
'global.parseFloat(foo);',
400+
'window.parseFloat(foo);',
401+
'self.parseFloat(foo);',
402+
'globalThis.NaN',
403+
'-globalThis.Infinity',
392404
],
393405
});

0 commit comments

Comments
 (0)