Skip to content

Commit

Permalink
prefer-number-properties: Detect usage via global object (#1832)
Browse files Browse the repository at this point in the history
  • Loading branch information
fisker committed May 27, 2022
1 parent aee56fd commit 80c4af2
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 153 deletions.
172 changes: 84 additions & 88 deletions 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');

Expand All @@ -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});
},
};
};
Expand Down
20 changes: 16 additions & 4 deletions test/prefer-number-properties.mjs
Expand Up @@ -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]() {}}',
Expand All @@ -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;',
Expand All @@ -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',
],
});

0 comments on commit 80c4af2

Please sign in to comment.