Skip to content

Commit 2d11d46

Browse files
authored
feat: add suggestions to use-isnan in binary expressions (#17996)
* feat: add suggestions to `use-isnan` in binary expressions * remove `>` `<` from fixes * move the array up * use Set * wip * small refactor * remove empty line * add multiple suggestions * change wording * suggest casting only in `==` and `!=`
1 parent c25b4af commit 2d11d46

2 files changed

Lines changed: 353 additions & 36 deletions

File tree

lib/rules/use-isnan.js

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ function isNaNIdentifier(node) {
3434
/** @type {import('../shared/types').Rule} */
3535
module.exports = {
3636
meta: {
37+
hasSuggestions: true,
3738
type: "problem",
3839

3940
docs: {
@@ -63,14 +64,45 @@ module.exports = {
6364
comparisonWithNaN: "Use the isNaN function to compare with NaN.",
6465
switchNaN: "'switch(NaN)' can never match a case clause. Use Number.isNaN instead of the switch.",
6566
caseNaN: "'case NaN' can never match. Use Number.isNaN before the switch.",
66-
indexOfNaN: "Array prototype method '{{ methodName }}' cannot find NaN."
67+
indexOfNaN: "Array prototype method '{{ methodName }}' cannot find NaN.",
68+
replaceWithIsNaN: "Replace with Number.isNaN.",
69+
replaceWithCastingAndIsNaN: "Replace with Number.isNaN cast to a Number."
6770
}
6871
},
6972

7073
create(context) {
7174

7275
const enforceForSwitchCase = !context.options[0] || context.options[0].enforceForSwitchCase;
7376
const enforceForIndexOf = context.options[0] && context.options[0].enforceForIndexOf;
77+
const sourceCode = context.sourceCode;
78+
79+
const fixableOperators = new Set(["==", "===", "!=", "!=="]);
80+
const castableOperators = new Set(["==", "!="]);
81+
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) ? node.right : node.left;
91+
const shouldWrap = comparedValue.type === "SequenceExpression";
92+
const shouldNegate = node.operator[0] === "!";
93+
94+
const negation = shouldNegate ? "!" : "";
95+
let comparedValueText = sourceCode.getText(comparedValue);
96+
97+
if (shouldWrap) {
98+
comparedValueText = `(${comparedValueText})`;
99+
}
100+
101+
const fixedValue = wrapValue(comparedValueText);
102+
103+
return fixer.replaceText(node, `${negation}${fixedValue}`);
104+
};
105+
}
74106

75107
/**
76108
* Checks the given `BinaryExpression` node for `foo === NaN` and other comparisons.
@@ -82,7 +114,29 @@ module.exports = {
82114
/^(?:[<>]|[!=]=)=?$/u.test(node.operator) &&
83115
(isNaNIdentifier(node.left) || isNaNIdentifier(node.right))
84116
) {
85-
context.report({ node, messageId: "comparisonWithNaN" });
117+
const suggestedFixes = [];
118+
const isFixable = fixableOperators.has(node.operator);
119+
const isCastable = castableOperators.has(node.operator);
120+
121+
if (isFixable) {
122+
suggestedFixes.push({
123+
messageId: "replaceWithIsNaN",
124+
fix: getBinaryExpressionFixer(node, value => `Number.isNaN(${value})`)
125+
});
126+
}
127+
128+
if (isCastable) {
129+
suggestedFixes.push({
130+
messageId: "replaceWithCastingAndIsNaN",
131+
fix: getBinaryExpressionFixer(node, value => `Number.isNaN(Number(${value}))`)
132+
});
133+
}
134+
135+
context.report({
136+
node,
137+
messageId: "comparisonWithNaN",
138+
suggest: suggestedFixes
139+
});
86140
}
87141
}
88142

0 commit comments

Comments
 (0)