use-isnan.js 7.0 KB

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