use-isnan.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  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. },
  50. enforceForIndexOf: {
  51. type: "boolean"
  52. }
  53. },
  54. additionalProperties: false
  55. }
  56. ],
  57. defaultOptions: [{
  58. enforceForIndexOf: false,
  59. enforceForSwitchCase: true
  60. }],
  61. messages: {
  62. comparisonWithNaN: "Use the isNaN function to compare with NaN.",
  63. switchNaN: "'switch(NaN)' can never match a case clause. Use Number.isNaN instead of the switch.",
  64. caseNaN: "'case NaN' can never match. Use Number.isNaN before the switch.",
  65. indexOfNaN: "Array prototype method '{{ methodName }}' cannot find NaN.",
  66. replaceWithIsNaN: "Replace with Number.isNaN.",
  67. replaceWithCastingAndIsNaN: "Replace with Number.isNaN and cast to a Number.",
  68. replaceWithFindIndex: "Replace with Array.prototype.{{ methodName }}."
  69. }
  70. },
  71. create(context) {
  72. const [{ enforceForIndexOf, enforceForSwitchCase }] = context.options;
  73. const sourceCode = context.sourceCode;
  74. const fixableOperators = new Set(["==", "===", "!=", "!=="]);
  75. const castableOperators = new Set(["==", "!="]);
  76. /**
  77. * Get a fixer for a binary expression that compares to NaN.
  78. * @param {ASTNode} node The node to fix.
  79. * @param {function(string): string} wrapValue A function that wraps the compared value with a fix.
  80. * @returns {function(Fixer): Fix} The fixer function.
  81. */
  82. function getBinaryExpressionFixer(node, wrapValue) {
  83. return fixer => {
  84. const comparedValue = isNaNIdentifier(node.left) ? node.right : node.left;
  85. const shouldWrap = comparedValue.type === "SequenceExpression";
  86. const shouldNegate = node.operator[0] === "!";
  87. const negation = shouldNegate ? "!" : "";
  88. let comparedValueText = sourceCode.getText(comparedValue);
  89. if (shouldWrap) {
  90. comparedValueText = `(${comparedValueText})`;
  91. }
  92. const fixedValue = wrapValue(comparedValueText);
  93. return fixer.replaceText(node, `${negation}${fixedValue}`);
  94. };
  95. }
  96. /**
  97. * Checks the given `BinaryExpression` node for `foo === NaN` and other comparisons.
  98. * @param {ASTNode} node The node to check.
  99. * @returns {void}
  100. */
  101. function checkBinaryExpression(node) {
  102. if (
  103. /^(?:[<>]|[!=]=)=?$/u.test(node.operator) &&
  104. (isNaNIdentifier(node.left) || isNaNIdentifier(node.right))
  105. ) {
  106. const suggestedFixes = [];
  107. const NaNNode = isNaNIdentifier(node.left) ? node.left : node.right;
  108. const isSequenceExpression = NaNNode.type === "SequenceExpression";
  109. const isSuggestable = fixableOperators.has(node.operator) && !isSequenceExpression;
  110. const isCastable = castableOperators.has(node.operator);
  111. if (isSuggestable) {
  112. suggestedFixes.push({
  113. messageId: "replaceWithIsNaN",
  114. fix: getBinaryExpressionFixer(node, value => `Number.isNaN(${value})`)
  115. });
  116. if (isCastable) {
  117. suggestedFixes.push({
  118. messageId: "replaceWithCastingAndIsNaN",
  119. fix: getBinaryExpressionFixer(node, value => `Number.isNaN(Number(${value}))`)
  120. });
  121. }
  122. }
  123. context.report({
  124. node,
  125. messageId: "comparisonWithNaN",
  126. suggest: suggestedFixes
  127. });
  128. }
  129. }
  130. /**
  131. * Checks the discriminant and all case clauses of the given `SwitchStatement` node for `switch(NaN)` and `case NaN:`
  132. * @param {ASTNode} node The node to check.
  133. * @returns {void}
  134. */
  135. function checkSwitchStatement(node) {
  136. if (isNaNIdentifier(node.discriminant)) {
  137. context.report({ node, messageId: "switchNaN" });
  138. }
  139. for (const switchCase of node.cases) {
  140. if (isNaNIdentifier(switchCase.test)) {
  141. context.report({ node: switchCase, messageId: "caseNaN" });
  142. }
  143. }
  144. }
  145. /**
  146. * Checks the given `CallExpression` node for `.indexOf(NaN)` and `.lastIndexOf(NaN)`.
  147. * @param {ASTNode} node The node to check.
  148. * @returns {void}
  149. */
  150. function checkCallExpression(node) {
  151. const callee = astUtils.skipChainExpression(node.callee);
  152. if (callee.type === "MemberExpression") {
  153. const methodName = astUtils.getStaticPropertyName(callee);
  154. if (
  155. (methodName === "indexOf" || methodName === "lastIndexOf") &&
  156. node.arguments.length <= 2 &&
  157. isNaNIdentifier(node.arguments[0])
  158. ) {
  159. /*
  160. * To retain side effects, it's essential to address `NaN` beforehand, which
  161. * is not possible with fixes like `arr.findIndex(Number.isNaN)`.
  162. */
  163. const isSuggestable = node.arguments[0].type !== "SequenceExpression" && !node.arguments[1];
  164. const suggestedFixes = [];
  165. if (isSuggestable) {
  166. const shouldWrap = callee.computed;
  167. const findIndexMethod = methodName === "indexOf" ? "findIndex" : "findLastIndex";
  168. const propertyName = shouldWrap ? `"${findIndexMethod}"` : findIndexMethod;
  169. suggestedFixes.push({
  170. messageId: "replaceWithFindIndex",
  171. data: { methodName: findIndexMethod },
  172. fix: fixer => [
  173. fixer.replaceText(callee.property, propertyName),
  174. fixer.replaceText(node.arguments[0], "Number.isNaN")
  175. ]
  176. });
  177. }
  178. context.report({
  179. node,
  180. messageId: "indexOfNaN",
  181. data: { methodName },
  182. suggest: suggestedFixes
  183. });
  184. }
  185. }
  186. }
  187. const listeners = {
  188. BinaryExpression: checkBinaryExpression
  189. };
  190. if (enforceForSwitchCase) {
  191. listeners.SwitchStatement = checkSwitchStatement;
  192. }
  193. if (enforceForIndexOf) {
  194. listeners.CallExpression = checkCallExpression;
  195. }
  196. return listeners;
  197. }
  198. };