diff --git a/lib/rules/use-isnan.js b/lib/rules/use-isnan.js index 21dc3952902..b00a701c6bd 100644 --- a/lib/rules/use-isnan.js +++ b/lib/rules/use-isnan.js @@ -34,6 +34,7 @@ function isNaNIdentifier(node) { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + hasSuggestions: true, type: "problem", docs: { @@ -63,7 +64,9 @@ module.exports = { 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." + indexOfNaN: "Array prototype method '{{ methodName }}' cannot find NaN.", + replaceWithIsNaN: "Replace with Number.isNaN.", + replaceWithCastingAndIsNaN: "Replace with Number.isNaN cast to a Number." } }, @@ -71,6 +74,35 @@ module.exports = { const enforceForSwitchCase = !context.options[0] || context.options[0].enforceForSwitchCase; const enforceForIndexOf = context.options[0] && context.options[0].enforceForIndexOf; + 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. @@ -82,7 +114,29 @@ module.exports = { /^(?:[<>]|[!=]=)=?$/u.test(node.operator) && (isNaNIdentifier(node.left) || isNaNIdentifier(node.right)) ) { - context.report({ node, messageId: "comparisonWithNaN" }); + const suggestedFixes = []; + const isFixable = fixableOperators.has(node.operator); + const isCastable = castableOperators.has(node.operator); + + if (isFixable) { + 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 + }); } } diff --git a/tests/lib/rules/use-isnan.js b/tests/lib/rules/use-isnan.js index 8c036bc3971..26c887a1975 100644 --- a/tests/lib/rules/use-isnan.js +++ b/tests/lib/rules/use-isnan.js @@ -349,140 +349,403 @@ ruleTester.run("use-isnan", rule, { invalid: [ { code: "123 == NaN;", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: "Number.isNaN(123);" + }, + { + messageId: "replaceWithCastingAndIsNaN", + output: "Number.isNaN(Number(123));" + } + ] + }] }, { code: "123 === NaN;", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: "Number.isNaN(123);" + } + ] + }] }, { code: "NaN === \"abc\";", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: 'Number.isNaN("abc");' + } + ] + }] }, { code: "NaN == \"abc\";", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: 'Number.isNaN("abc");' + }, + { + messageId: "replaceWithCastingAndIsNaN", + output: 'Number.isNaN(Number("abc"));' + } + ] + }] }, { code: "123 != NaN;", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: "!Number.isNaN(123);" + }, + { + messageId: "replaceWithCastingAndIsNaN", + output: "!Number.isNaN(Number(123));" + } + ] + }] }, { code: "123 !== NaN;", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: "!Number.isNaN(123);" + } + ] + }] }, { code: "NaN !== \"abc\";", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: '!Number.isNaN("abc");' + } + ] + }] }, { code: "NaN != \"abc\";", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: '!Number.isNaN("abc");' + }, + { + messageId: "replaceWithCastingAndIsNaN", + output: '!Number.isNaN(Number("abc"));' + } + ] + }] }, { code: "NaN < \"abc\";", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [] + }] }, { code: "\"abc\" < NaN;", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [] + }] }, { code: "NaN > \"abc\";", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [] + }] }, { code: "\"abc\" > NaN;", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [] + }] }, { code: "NaN <= \"abc\";", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [] + }] }, { code: "\"abc\" <= NaN;", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [] + }] }, { code: "NaN >= \"abc\";", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [] + }] }, { code: "\"abc\" >= NaN;", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [] + }] }, { code: "123 == Number.NaN;", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: "Number.isNaN(123);" + }, + { + messageId: "replaceWithCastingAndIsNaN", + output: "Number.isNaN(Number(123));" + } + ] + }] }, { code: "123 === Number.NaN;", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: "Number.isNaN(123);" + } + ] + }] }, { code: "Number.NaN === \"abc\";", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: "Number.isNaN(\"abc\");" + } + ] + }] }, { code: "Number.NaN == \"abc\";", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: 'Number.isNaN("abc");' + }, + { + messageId: "replaceWithCastingAndIsNaN", + output: 'Number.isNaN(Number("abc"));' + } + ] + }] }, { code: "123 != Number.NaN;", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: "!Number.isNaN(123);" + }, + { + messageId: "replaceWithCastingAndIsNaN", + output: "!Number.isNaN(Number(123));" + } + ] + }] }, { code: "123 !== Number.NaN;", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: "!Number.isNaN(123);" + } + ] + }] }, { code: "Number.NaN !== \"abc\";", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: '!Number.isNaN("abc");' + } + ] + }] }, { code: "Number.NaN != \"abc\";", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: '!Number.isNaN("abc");' + }, + { + messageId: "replaceWithCastingAndIsNaN", + output: '!Number.isNaN(Number("abc"));' + } + ] + }] }, { code: "Number.NaN < \"abc\";", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [] + }] }, { code: "\"abc\" < Number.NaN;", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [] + }] }, { code: "Number.NaN > \"abc\";", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [] + }] }, { code: "\"abc\" > Number.NaN;", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [] + }] }, { code: "Number.NaN <= \"abc\";", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [] + }] }, { code: "\"abc\" <= Number.NaN;", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [] + }] }, { code: "Number.NaN >= \"abc\";", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [] + }] }, { code: "\"abc\" >= Number.NaN;", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [] + }] }, { code: "x === Number?.NaN;", languageOptions: { ecmaVersion: 2020 }, - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: "Number.isNaN(x);" + } + ] + }] + }, + { + code: "x !== Number?.NaN;", + languageOptions: { ecmaVersion: 2020 }, + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: "!Number.isNaN(x);" + } + ] + }] }, { code: "x === Number['NaN'];", - errors: [comparisonError] + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: "Number.isNaN(x);" + } + ] + }] + }, + { + code: `/* just + adding */ x /* some */ === /* comments */ NaN; // here`, + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: `/* just + adding */ Number.isNaN(x); // here` + } + ] + }] + }, + { + code: "(1, 2) === NaN;", + errors: [{ + ...comparisonError, + suggestions: [ + { + messageId: "replaceWithIsNaN", + output: "Number.isNaN((1, 2));" + } + ] + }] }, //------------------------------------------------------------------------------