2025-04-19 23:12:19 -04:00

252 lines
7.5 KiB
JavaScript

/**
* @fileoverview Rule to flag comparisons to the value NaN
* @author James Allardice
*/
'use strict';
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require('./utils/ast-utils');
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Determines if the given node is a NaN `Identifier` node.
* @param {ASTNode|null} node The node to check.
* @returns {boolean} `true` if the node is 'NaN' identifier.
*/
function isNaNIdentifier(node) {
if (!node) {
return false;
}
const nodeToCheck =
node.type === 'SequenceExpression' ? node.expressions.at(-1) : node;
return (
astUtils.isSpecificId(nodeToCheck, 'NaN') ||
astUtils.isSpecificMemberAccess(nodeToCheck, 'Number', 'NaN')
);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
module.exports = {
meta: {
hasSuggestions: true,
type: 'problem',
docs: {
description: 'Require calls to `isNaN()` when checking for `NaN`',
recommended: true,
url: 'https://eslint.org/docs/latest/rules/use-isnan',
},
schema: [
{
type: 'object',
properties: {
enforceForSwitchCase: {
type: 'boolean',
},
enforceForIndexOf: {
type: 'boolean',
},
},
additionalProperties: false,
},
],
defaultOptions: [
{
enforceForIndexOf: false,
enforceForSwitchCase: true,
},
],
messages: {
comparisonWithNaN: 'Use the isNaN function to compare with NaN.',
switchNaN:
"'switch(NaN)' can never match a case clause. Use Number.isNaN instead of the switch.",
caseNaN:
"'case NaN' can never match. Use Number.isNaN before the switch.",
indexOfNaN: "Array prototype method '{{ methodName }}' cannot find NaN.",
replaceWithIsNaN: 'Replace with Number.isNaN.',
replaceWithCastingAndIsNaN:
'Replace with Number.isNaN and cast to a Number.',
replaceWithFindIndex: 'Replace with Array.prototype.{{ methodName }}.',
},
},
create(context) {
const [{ enforceForIndexOf, enforceForSwitchCase }] = context.options;
const sourceCode = context.sourceCode;
const fixableOperators = new Set(['==', '===', '!=', '!==']);
const castableOperators = new Set(['==', '!=']);
/**
* Get a fixer for a binary expression that compares to NaN.
* @param {ASTNode} node The node to fix.
* @param {function(string): string} wrapValue A function that wraps the compared value with a fix.
* @returns {function(Fixer): Fix} The fixer function.
*/
function getBinaryExpressionFixer(node, wrapValue) {
return (fixer) => {
const comparedValue =
isNaNIdentifier(node.left) ? node.right : node.left;
const shouldWrap = comparedValue.type === 'SequenceExpression';
const shouldNegate = node.operator[0] === '!';
const negation = shouldNegate ? '!' : '';
let comparedValueText = sourceCode.getText(comparedValue);
if (shouldWrap) {
comparedValueText = `(${comparedValueText})`;
}
const fixedValue = wrapValue(comparedValueText);
return fixer.replaceText(node, `${negation}${fixedValue}`);
};
}
/**
* Checks the given `BinaryExpression` node for `foo === NaN` and other comparisons.
* @param {ASTNode} node The node to check.
* @returns {void}
*/
function checkBinaryExpression(node) {
if (
/^(?:[<>]|[!=]=)=?$/u.test(node.operator) &&
(isNaNIdentifier(node.left) || isNaNIdentifier(node.right))
) {
const suggestedFixes = [];
const NaNNode = isNaNIdentifier(node.left) ? node.left : node.right;
const isSequenceExpression = NaNNode.type === 'SequenceExpression';
const isSuggestable =
fixableOperators.has(node.operator) && !isSequenceExpression;
const isCastable = castableOperators.has(node.operator);
if (isSuggestable) {
suggestedFixes.push({
messageId: 'replaceWithIsNaN',
fix: getBinaryExpressionFixer(
node,
(value) => `Number.isNaN(${value})`
),
});
if (isCastable) {
suggestedFixes.push({
messageId: 'replaceWithCastingAndIsNaN',
fix: getBinaryExpressionFixer(
node,
(value) => `Number.isNaN(Number(${value}))`
),
});
}
}
context.report({
node,
messageId: 'comparisonWithNaN',
suggest: suggestedFixes,
});
}
}
/**
* Checks the discriminant and all case clauses of the given `SwitchStatement` node for `switch(NaN)` and `case NaN:`
* @param {ASTNode} node The node to check.
* @returns {void}
*/
function checkSwitchStatement(node) {
if (isNaNIdentifier(node.discriminant)) {
context.report({ node, messageId: 'switchNaN' });
}
for (const switchCase of node.cases) {
if (isNaNIdentifier(switchCase.test)) {
context.report({ node: switchCase, messageId: 'caseNaN' });
}
}
}
/**
* Checks the given `CallExpression` node for `.indexOf(NaN)` and `.lastIndexOf(NaN)`.
* @param {ASTNode} node The node to check.
* @returns {void}
*/
function checkCallExpression(node) {
const callee = astUtils.skipChainExpression(node.callee);
if (callee.type === 'MemberExpression') {
const methodName = astUtils.getStaticPropertyName(callee);
if (
(methodName === 'indexOf' || methodName === 'lastIndexOf') &&
node.arguments.length <= 2 &&
isNaNIdentifier(node.arguments[0])
) {
/*
* To retain side effects, it's essential to address `NaN` beforehand, which
* is not possible with fixes like `arr.findIndex(Number.isNaN)`.
*/
const isSuggestable =
node.arguments[0].type !== 'SequenceExpression' &&
!node.arguments[1];
const suggestedFixes = [];
if (isSuggestable) {
const shouldWrap = callee.computed;
const findIndexMethod =
methodName === 'indexOf' ? 'findIndex' : 'findLastIndex';
const propertyName =
shouldWrap ? `"${findIndexMethod}"` : findIndexMethod;
suggestedFixes.push({
messageId: 'replaceWithFindIndex',
data: { methodName: findIndexMethod },
fix: (fixer) => [
fixer.replaceText(callee.property, propertyName),
fixer.replaceText(node.arguments[0], 'Number.isNaN'),
],
});
}
context.report({
node,
messageId: 'indexOfNaN',
data: { methodName },
suggest: suggestedFixes,
});
}
}
}
const listeners = {
BinaryExpression: checkBinaryExpression,
};
if (enforceForSwitchCase) {
listeners.SwitchStatement = checkSwitchStatement;
}
if (enforceForIndexOf) {
listeners.CallExpression = checkCallExpression;
}
return listeners;
},
};