238 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			238 lines
		
	
	
		
			8.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('../shared/types').Rule} */
 | |
| 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;
 | |
|     }
 | |
| };
 |