use-isnan.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. /**
  2. * @fileoverview Rule to flag comparisons to the value NaN
  3. * @author James Allardice
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Helpers
  12. //------------------------------------------------------------------------------
  13. /**
  14. * Determines if the given node is a NaN `Identifier` node.
  15. * @param {ASTNode|null} node The node to check.
  16. * @returns {boolean} `true` if the node is 'NaN' identifier.
  17. */
  18. function isNaNIdentifier(node) {
  19. if (!node) {
  20. return false;
  21. }
  22. const nodeToCheck = node.type === "SequenceExpression"
  23. ? node.expressions.at(-1)
  24. : node;
  25. return (
  26. astUtils.isSpecificId(nodeToCheck, "NaN") ||
  27. astUtils.isSpecificMemberAccess(nodeToCheck, "Number", "NaN")
  28. );
  29. }
  30. //------------------------------------------------------------------------------
  31. // Rule Definition
  32. //------------------------------------------------------------------------------
  33. /** @type {import('../shared/types').Rule} */
  34. module.exports = {
  35. meta: {
  36. hasSuggestions: true,
  37. type: "problem",
  38. docs: {
  39. description: "Require calls to `isNaN()` when checking for `NaN`",
  40. recommended: true,
  41. url: "https://eslint.org/docs/latest/rules/use-isnan"
  42. },
  43. schema: [
  44. {
  45. type: "object",
  46. properties: {
  47. enforceForSwitchCase: {
  48. type: "boolean",
  49. default: true
  50. },
  51. enforceForIndexOf: {
  52. type: "boolean",
  53. default: false
  54. }
  55. },
  56. additionalProperties: false
  57. }
  58. ],
  59. messages: {
  60. comparisonWithNaN: "Use the isNaN function to compare with NaN.",
  61. switchNaN: "'switch(NaN)' can never match a case clause. Use Number.isNaN instead of the switch.",
  62. caseNaN: "'case NaN' can never match. Use Number.isNaN before the switch.",
  63. indexOfNaN: "Array prototype method '{{ methodName }}' cannot find NaN.",
  64. replaceWithIsNaN: "Replace with Number.isNaN.",
  65. replaceWithCastingAndIsNaN: "Replace with Number.isNaN and cast to a Number.",
  66. replaceWithFindIndex: "Replace with Array.prototype.{{ methodName }}."
  67. }
  68. },
  69. create(context) {
  70. const enforceForSwitchCase = !context.options[0] || context.options[0].enforceForSwitchCase;
  71. const enforceForIndexOf = context.options[0] && context.options[0].enforceForIndexOf;
  72. const sourceCode = context.sourceCode;
  73. const fixableOperators = new Set(["==", "===", "!=", "!=="]);
  74. const castableOperators = new Set(["==", "!="]);
  75. /**
  76. * Get a fixer for a binary expression that compares to NaN.
  77. * @param {ASTNode} node The node to fix.
  78. * @param {function(string): string} wrapValue A function that wraps the compared value with a fix.
  79. * @returns {function(Fixer): Fix} The fixer function.
  80. */
  81. function getBinaryExpressionFixer(node, wrapValue) {
  82. return fixer => {
  83. const comparedValue = isNaNIdentifier(node.left) ? node.right : node.left;
  84. const shouldWrap = comparedValue.type === "SequenceExpression";
  85. const shouldNegate = node.operator[0] === "!";
  86. const negation = shouldNegate ? "!" : "";
  87. let comparedValueText = sourceCode.getText(comparedValue);
  88. if (shouldWrap) {
  89. comparedValueText = `(${comparedValueText})`;
  90. }
  91. const fixedValue = wrapValue(comparedValueText);
  92. return fixer.replaceText(node, `${negation}${fixedValue}`);
  93. };
  94. }
  95. /**
  96. * Checks the given `BinaryExpression` node for `foo === NaN` and other comparisons.
  97. * @param {ASTNode} node The node to check.
  98. * @returns {void}
  99. */
  100. function checkBinaryExpression(node) {
  101. if (
  102. /^(?:[<>]|[!=]=)=?$/u.test(node.operator) &&
  103. (isNaNIdentifier(node.left) || isNaNIdentifier(node.right))
  104. ) {
  105. const suggestedFixes = [];
  106. const NaNNode = isNaNIdentifier(node.left) ? node.left : node.right;
  107. const isSequenceExpression = NaNNode.type === "SequenceExpression";
  108. const isSuggestable = fixableOperators.has(node.operator) && !isSequenceExpression;
  109. const isCastable = castableOperators.has(node.operator);
  110. if (isSuggestable) {
  111. suggestedFixes.push({
  112. messageId: "replaceWithIsNaN",
  113. fix: getBinaryExpressionFixer(node, value => `Number.isNaN(${value})`)
  114. });
  115. if (isCastable) {
  116. suggestedFixes.push({
  117. messageId: "replaceWithCastingAndIsNaN",
  118. fix: getBinaryExpressionFixer(node, value => `Number.isNaN(Number(${value}))`)
  119. });
  120. }
  121. }
  122. context.report({
  123. node,
  124. messageId: "comparisonWithNaN",
  125. suggest: suggestedFixes
  126. });
  127. }
  128. }
  129. /**
  130. * Checks the discriminant and all case clauses of the given `SwitchStatement` node for `switch(NaN)` and `case NaN:`
  131. * @param {ASTNode} node The node to check.
  132. * @returns {void}
  133. */
  134. function checkSwitchStatement(node) {
  135. if (isNaNIdentifier(node.discriminant)) {
  136. context.report({ node, messageId: "switchNaN" });
  137. }
  138. for (const switchCase of node.cases) {
  139. if (isNaNIdentifier(switchCase.test)) {
  140. context.report({ node: switchCase, messageId: "caseNaN" });
  141. }
  142. }
  143. }
  144. /**
  145. * Checks the given `CallExpression` node for `.indexOf(NaN)` and `.lastIndexOf(NaN)`.
  146. * @param {ASTNode} node The node to check.
  147. * @returns {void}
  148. */
  149. function checkCallExpression(node) {
  150. const callee = astUtils.skipChainExpression(node.callee);
  151. if (callee.type === "MemberExpression") {
  152. const methodName = astUtils.getStaticPropertyName(callee);
  153. if (
  154. (methodName === "indexOf" || methodName === "lastIndexOf") &&
  155. node.arguments.length <= 2 &&
  156. isNaNIdentifier(node.arguments[0])
  157. ) {
  158. /*
  159. * To retain side effects, it's essential to address `NaN` beforehand, which
  160. * is not possible with fixes like `arr.findIndex(Number.isNaN)`.
  161. */
  162. const isSuggestable = node.arguments[0].type !== "SequenceExpression" && !node.arguments[1];
  163. const suggestedFixes = [];
  164. if (isSuggestable) {
  165. const shouldWrap = callee.computed;
  166. const findIndexMethod = methodName === "indexOf" ? "findIndex" : "findLastIndex";
  167. const propertyName = shouldWrap ? `"${findIndexMethod}"` : findIndexMethod;
  168. suggestedFixes.push({
  169. messageId: "replaceWithFindIndex",
  170. data: { methodName: findIndexMethod },
  171. fix: fixer => [
  172. fixer.replaceText(callee.property, propertyName),
  173. fixer.replaceText(node.arguments[0], "Number.isNaN")
  174. ]
  175. });
  176. }
  177. context.report({
  178. node,
  179. messageId: "indexOfNaN",
  180. data: { methodName },
  181. suggest: suggestedFixes
  182. });
  183. }
  184. }
  185. }
  186. const listeners = {
  187. BinaryExpression: checkBinaryExpression
  188. };
  189. if (enforceForSwitchCase) {
  190. listeners.SwitchStatement = checkSwitchStatement;
  191. }
  192. if (enforceForIndexOf) {
  193. listeners.CallExpression = checkCallExpression;
  194. }
  195. return listeners;
  196. }
  197. };