diff --git a/docs/rules/use-isnan.md b/docs/rules/use-isnan.md index 7aa73f40067..426eaca5e6d 100644 --- a/docs/rules/use-isnan.md +++ b/docs/rules/use-isnan.md @@ -40,3 +40,66 @@ if (!isNaN(foo)) { // ... } ``` + +## Options + +This rule has an object option, with one option: + +* `"enforceForSwitchCase"` when set to `true` disallows `case NaN` and `switch(NaN)` in `switch` statements. Default is `false`, meaning +that this rule by default does not warn about `case NaN` or `switch(NaN)`. + +### enforceForSwitchCase + +The `switch` statement internally uses the `===` comparison to match the expression's value to a case clause. +Therefore, it can never match `case NaN`. Also, `switch(NaN)` can never match a case clause. + +Set `"enforceForSwitchCase"` to `true` if you want this rule to report `case NaN` and `switch(NaN)` in `switch` statements. + +Examples of **incorrect** code for this rule with `"enforceForSwitchCase"` option set to `true`: + +```js +/*eslint use-isnan: ["error", {"enforceForSwitchCase": true}]*/ + +switch (foo) { + case NaN: + bar(); + break; + case 1: + baz(); + break; + // ... +} + +switch (NaN) { + case a: + bar(); + break; + case b: + baz(); + break; + // ... +} +``` + +Examples of **correct** code for this rule with `"enforceForSwitchCase"` option set to `true`: + +```js +/*eslint use-isnan: ["error", {"enforceForSwitchCase": true}]*/ + +if (Number.isNaN(foo)) { + bar(); +} else { + switch (foo) { + case 1: + baz(); + break; + // ... + } +} + +if (Number.isNaN(a)) { + bar(); +} else if (Number.isNaN(b)) { + baz(); +} // ... +``` diff --git a/lib/rules/use-isnan.js b/lib/rules/use-isnan.js index 877c02754ae..b2eb84b7b37 100644 --- a/lib/rules/use-isnan.js +++ b/lib/rules/use-isnan.js @@ -5,6 +5,19 @@ "use strict"; +//------------------------------------------------------------------------------ +// 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) { + return Boolean(node) && node.type === "Identifier" && node.name === "NaN"; +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -20,21 +33,69 @@ module.exports = { url: "https://eslint.org/docs/rules/use-isnan" }, - schema: [], + schema: [ + { + type: "object", + properties: { + enforceForSwitchCase: { + type: "boolean", + default: false + } + }, + additionalProperties: false + } + ], + messages: { - useIsNaN: "Use the isNaN function to compare with NaN." + 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." } }, create(context) { - return { - BinaryExpression(node) { - if (/^(?:[<>]|[!=]=)=?$/u.test(node.operator) && (node.left.name === "NaN" || node.right.name === "NaN")) { - context.report({ node, messageId: "useIsNaN" }); + const enforceForSwitchCase = context.options[0] && context.options[0].enforceForSwitchCase; + + /** + * Checks the given `BinaryExpression` node. + * @param {ASTNode} node The node to check. + * @returns {void} + */ + function checkBinaryExpression(node) { + if ( + /^(?:[<>]|[!=]=)=?$/u.test(node.operator) && + (isNaNIdentifier(node.left) || isNaNIdentifier(node.right)) + ) { + context.report({ node, messageId: "comparisonWithNaN" }); + } + } + + /** + * Checks the discriminant and all case clauses of the given `SwitchStatement` node. + * @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" }); } } + } + + const listeners = { + BinaryExpression: checkBinaryExpression }; + if (enforceForSwitchCase) { + listeners.SwitchStatement = checkSwitchStatement; + } + + return listeners; } }; diff --git a/tests/lib/rules/use-isnan.js b/tests/lib/rules/use-isnan.js index e9d8b99a787..82bad95cb61 100644 --- a/tests/lib/rules/use-isnan.js +++ b/tests/lib/rules/use-isnan.js @@ -18,7 +18,7 @@ const rule = require("../../../lib/rules/use-isnan"), const ruleTester = new RuleTester(); -const error = { messageId: "useIsNaN", type: "BinaryExpression" }; +const comparisonError = { messageId: "comparisonWithNaN", type: "BinaryExpression" }; ruleTester.run("use-isnan", rule, { valid: [ @@ -35,72 +35,217 @@ ruleTester.run("use-isnan", rule, { "foo(2 * NaN)", "foo(NaN / 2)", "foo(2 / NaN)", - "var x; if (x = NaN) { }" + "var x; if (x = NaN) { }", + + //------------------------------------------------------------------------------ + // enforceForSwitchCase + //------------------------------------------------------------------------------ + + "switch(NaN) { case foo: break; }", + "switch(foo) { case NaN: break; }", + { + code: "switch(NaN) { case foo: break; }", + options: [{}] + }, + { + code: "switch(foo) { case NaN: break; }", + options: [{}] + }, + { + code: "switch(NaN) { case foo: break; }", + options: [{ enforceForSwitchCase: false }] + }, + { + code: "switch(foo) { case NaN: break; }", + options: [{ enforceForSwitchCase: false }] + }, + { + code: "switch(NaN) { case NaN: break; }", + options: [{ enforceForSwitchCase: false }] + }, + { + code: "switch(foo) { case bar: break; case NaN: break; default: break; }", + options: [{ enforceForSwitchCase: false }] + }, + { + code: "switch(foo) {}", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(foo) { case bar: NaN; }", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(foo) { default: NaN; }", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(Nan) {}", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch('NaN') { default: break; }", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(foo(NaN)) {}", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(foo.NaN) {}", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(foo) { case Nan: break }", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(foo) { case 'NaN': break }", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(foo) { case foo(NaN): break }", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(foo) { case foo.NaN: break }", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(foo) { case bar: break; case 1: break; default: break; }", + options: [{ enforceForSwitchCase: true }] + } ], invalid: [ { code: "123 == NaN;", - errors: [error] + errors: [comparisonError] }, { code: "123 === NaN;", - errors: [error] + errors: [comparisonError] }, { code: "NaN === \"abc\";", - errors: [error] + errors: [comparisonError] }, { code: "NaN == \"abc\";", - errors: [error] + errors: [comparisonError] }, { code: "123 != NaN;", - errors: [error] + errors: [comparisonError] }, { code: "123 !== NaN;", - errors: [error] + errors: [comparisonError] }, { code: "NaN !== \"abc\";", - errors: [error] + errors: [comparisonError] }, { code: "NaN != \"abc\";", - errors: [error] + errors: [comparisonError] }, { code: "NaN < \"abc\";", - errors: [error] + errors: [comparisonError] }, { code: "\"abc\" < NaN;", - errors: [error] + errors: [comparisonError] }, { code: "NaN > \"abc\";", - errors: [error] + errors: [comparisonError] }, { code: "\"abc\" > NaN;", - errors: [error] + errors: [comparisonError] }, { code: "NaN <= \"abc\";", - errors: [error] + errors: [comparisonError] }, { code: "\"abc\" <= NaN;", - errors: [error] + errors: [comparisonError] }, { code: "NaN >= \"abc\";", - errors: [error] + errors: [comparisonError] }, { code: "\"abc\" >= NaN;", - errors: [error] + errors: [comparisonError] + }, + + //------------------------------------------------------------------------------ + // enforceForSwitchCase + //------------------------------------------------------------------------------ + + { + code: "switch(NaN) {}", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "switchNaN", type: "SwitchStatement", column: 1 }] + }, + { + code: "switch(NaN) { case foo: break; }", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "switchNaN", type: "SwitchStatement", column: 1 }] + }, + { + code: "switch(NaN) { default: break; }", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "switchNaN", type: "SwitchStatement", column: 1 }] + }, + { + code: "switch(NaN) { case foo: break; default: break; }", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "switchNaN", type: "SwitchStatement", column: 1 }] + }, + { + code: "switch(foo) { case NaN: }", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "caseNaN", type: "SwitchCase", column: 15 }] + }, + { + code: "switch(foo) { case NaN: break; }", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "caseNaN", type: "SwitchCase", column: 15 }] + }, + { + code: "switch(foo) { case (NaN): break; }", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "caseNaN", type: "SwitchCase", column: 15 }] + }, + { + code: "switch(foo) { case bar: break; case NaN: break; default: break; }", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "caseNaN", type: "SwitchCase", column: 32 }] + }, + { + code: "switch(foo) { case bar: case NaN: default: break; }", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "caseNaN", type: "SwitchCase", column: 25 }] + }, + { + code: "switch(foo) { case bar: break; case NaN: break; case baz: break; case NaN: break; }", + options: [{ enforceForSwitchCase: true }], + errors: [ + { messageId: "caseNaN", type: "SwitchCase", column: 32 }, + { messageId: "caseNaN", type: "SwitchCase", column: 66 } + ] + }, + { + code: "switch(NaN) { case NaN: break; }", + options: [{ enforceForSwitchCase: true }], + errors: [ + { messageId: "switchNaN", type: "SwitchStatement", column: 1 }, + { messageId: "caseNaN", type: "SwitchCase", column: 15 } + ] } ] });