2025-04-17 07:44:37 -04:00

269 lines
7.0 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;
},
};