Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add suggestions to use-isnan in indexOf & lastIndexOf calls #18063

Merged
merged 12 commits into from
Feb 14, 2024
37 changes: 33 additions & 4 deletions lib/rules/use-isnan.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ module.exports = {
caseNaN: "'case NaN' can never match. Use Number.isNaN before the switch.",
indexOfNaN: "Array prototype method '{{ methodName }}' cannot find NaN.",
replaceWithIsNaN: "Replace with Number.isNaN.",
replaceWithCastingAndIsNaN: "Replace with Number.isNaN cast to a Number."
replaceWithCastingAndIsNaN: "Replace with Number.isNaN and cast to a Number.",
replaceWithFindIndex: "Replace with Array.prototype.{{ methodName }}."
}
},

Expand Down Expand Up @@ -126,10 +127,10 @@ module.exports = {
const NaNNode = isNaNIdentifier(node.left) ? node.left : node.right;

const isSequenceExpression = NaNNode.type === "SequenceExpression";
const isFixable = fixableOperators.has(node.operator) && !isSequenceExpression;
const isSuggestable = fixableOperators.has(node.operator) && !isSequenceExpression;
const isCastable = castableOperators.has(node.operator);

if (isFixable) {
if (isSuggestable) {
suggestedFixes.push({
messageId: "replaceWithIsNaN",
fix: getBinaryExpressionFixer(node, value => `Number.isNaN(${value})`)
Expand Down Expand Up @@ -184,7 +185,35 @@ module.exports = {
node.arguments.length === 1 &&
isNaNIdentifier(node.arguments[0])
) {
context.report({ node, messageId: "indexOfNaN", data: { methodName } });

/*
* To retain side effects, it's essential to address `NaN` beforehand, which
* is not possible with fixes like `arr.findIndex(Number.isNaN)`.
*/
const isSuggestable = node.arguments[0].type !== "SequenceExpression";
const suggestedFixes = [];

if (isSuggestable) {
const shouldWrap = callee.computed;
const findIndexMethod = methodName === "indexOf" ? "findIndex" : "findLastIndex";
const propertyName = shouldWrap ? `"${findIndexMethod}"` : findIndexMethod;

suggestedFixes.push({
messageId: "replaceWithFindIndex",
data: { methodName: findIndexMethod },
fix: fixer => [
fixer.replaceText(callee.property, propertyName),
fixer.replaceText(node.arguments[0], "Number.isNaN")
]
});
}

context.report({
node,
messageId: "indexOfNaN",
data: { methodName },
suggest: suggestedFixes
});
}
}
}
Expand Down
230 changes: 208 additions & 22 deletions tests/lib/rules/use-isnan.js
Original file line number Diff line number Diff line change
Expand Up @@ -987,122 +987,308 @@ ruleTester.run("use-isnan", rule, {
{
code: "foo.indexOf(NaN)",
options: [{ enforceForIndexOf: true }],
errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "indexOf" } }]
errors: [{
messageId: "indexOfNaN",
type: "CallExpression",
data: { methodName: "indexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findIndex" },
output: "foo.findIndex(Number.isNaN)"
}]
}]
},
{
code: "foo.lastIndexOf(NaN)",
options: [{ enforceForIndexOf: true }],
errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }]
errors: [{
messageId: "indexOfNaN",
type: "CallExpression",
data: { methodName: "lastIndexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findLastIndex" },
output: "foo.findLastIndex(Number.isNaN)"
}]
}]
},
{
code: "foo['indexOf'](NaN)",
options: [{ enforceForIndexOf: true }],
errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "indexOf" } }]
errors: [{
messageId: "indexOfNaN",
type: "CallExpression",
data: { methodName: "indexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findIndex" },
output: 'foo["findIndex"](Number.isNaN)'
}]
}]
},
{
code: "foo[`indexOf`](NaN)",
options: [{ enforceForIndexOf: true }],
errors: [{
messageId: "indexOfNaN",
type: "CallExpression",
data: { methodName: "indexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findIndex" },
output: 'foo["findIndex"](Number.isNaN)'
}]
}]
},
{
code: "foo['lastIndexOf'](NaN)",
options: [{ enforceForIndexOf: true }],
errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }]
errors: [{
messageId: "indexOfNaN",
type: "CallExpression",
data: { methodName: "lastIndexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findLastIndex" },
output: 'foo["findLastIndex"](Number.isNaN)'
}]
}]
},
{
code: "foo().indexOf(NaN)",
options: [{ enforceForIndexOf: true }],
errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "indexOf" } }]
errors: [{
messageId: "indexOfNaN",
type: "CallExpression",
data: { methodName: "indexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findIndex" },
output: "foo().findIndex(Number.isNaN)"
}]
}]
},
{
code: "foo.bar.lastIndexOf(NaN)",
options: [{ enforceForIndexOf: true }],
errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }]
errors: [{
messageId: "indexOfNaN",
type: "CallExpression",
data: { methodName: "lastIndexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findLastIndex" },
output: "foo.bar.findLastIndex(Number.isNaN)"
}]
}]
},
{
code: "foo.indexOf?.(NaN)",
options: [{ enforceForIndexOf: true }],
languageOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }]
errors: [{
messageId: "indexOfNaN",
data: { methodName: "indexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findIndex" },
output: "foo.findIndex?.(Number.isNaN)"
}]
}]
},
{
code: "foo?.indexOf(NaN)",
options: [{ enforceForIndexOf: true }],
languageOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }]
errors: [{
messageId: "indexOfNaN",
data: { methodName: "indexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findIndex" },
output: "foo?.findIndex(Number.isNaN)"
}]
}]
},
{
code: "(foo?.indexOf)(NaN)",
options: [{ enforceForIndexOf: true }],
languageOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }]
errors: [{
messageId: "indexOfNaN",
data: { methodName: "indexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findIndex" },
output: "(foo?.findIndex)(Number.isNaN)"
}]
}]
},
{
code: "foo.indexOf(Number.NaN)",
options: [{ enforceForIndexOf: true }],
errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "indexOf" } }]
errors: [{
messageId: "indexOfNaN",
type: "CallExpression",
data: { methodName: "indexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findIndex" },
output: "foo.findIndex(Number.isNaN)"
}]
}]
},
{
code: "foo.lastIndexOf(Number.NaN)",
options: [{ enforceForIndexOf: true }],
errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }]
errors: [{
messageId: "indexOfNaN",
type: "CallExpression",
data: { methodName: "lastIndexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findLastIndex" },
output: "foo.findLastIndex(Number.isNaN)"
}]
}]
},
{
code: "foo['indexOf'](Number.NaN)",
options: [{ enforceForIndexOf: true }],
errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "indexOf" } }]
errors: [{
messageId: "indexOfNaN",
type: "CallExpression",
data: { methodName: "indexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findIndex" },
output: 'foo["findIndex"](Number.isNaN)'
}]
}]
},
{
code: "foo['lastIndexOf'](Number.NaN)",
options: [{ enforceForIndexOf: true }],
errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }]
errors: [{
messageId: "indexOfNaN",
type: "CallExpression",
data: { methodName: "lastIndexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findLastIndex" },
output: 'foo["findLastIndex"](Number.isNaN)'
}]
}]
},
{
code: "foo().indexOf(Number.NaN)",
options: [{ enforceForIndexOf: true }],
errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "indexOf" } }]
errors: [{
messageId: "indexOfNaN",
type: "CallExpression",
data: { methodName: "indexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findIndex" },
output: "foo().findIndex(Number.isNaN)"
}]
}]
},
{
code: "foo.bar.lastIndexOf(Number.NaN)",
options: [{ enforceForIndexOf: true }],
errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }]
errors: [{
messageId: "indexOfNaN",
type: "CallExpression",
data: { methodName: "lastIndexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findLastIndex" },
output: "foo.bar.findLastIndex(Number.isNaN)"
}]
}]
},
{
code: "foo.indexOf?.(Number.NaN)",
options: [{ enforceForIndexOf: true }],
languageOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }]
errors: [{
messageId: "indexOfNaN",
data: { methodName: "indexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findIndex" },
output: "foo.findIndex?.(Number.isNaN)"
}]
}]
},
{
code: "foo?.indexOf(Number.NaN)",
options: [{ enforceForIndexOf: true }],
languageOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }]
errors: [{
messageId: "indexOfNaN",
data: { methodName: "indexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findIndex" },
output: "foo?.findIndex(Number.isNaN)"
}]
}]
},
{
code: "(foo?.indexOf)(Number.NaN)",
options: [{ enforceForIndexOf: true }],
languageOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }]
errors: [{
messageId: "indexOfNaN",
data: { methodName: "indexOf" },
suggestions: [{
messageId: "replaceWithFindIndex",
data: { methodName: "findIndex" },
output: "(foo?.findIndex)(Number.isNaN)"
}]
}]
},
{
code: "foo.indexOf((1, NaN))",
options: [{ enforceForIndexOf: true }],
languageOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }]
errors: [{
messageId: "indexOfNaN",
data: { methodName: "indexOf" },
suggestions: []
}]
},
{
code: "foo.indexOf((1, Number.NaN))",
options: [{ enforceForIndexOf: true }],
languageOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }]
errors: [{
messageId: "indexOfNaN",
data: { methodName: "indexOf" },
suggestions: []
}]
},
{
code: "foo.lastIndexOf((1, NaN))",
options: [{ enforceForIndexOf: true }],
languageOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "indexOfNaN", data: { methodName: "lastIndexOf" } }]
errors: [{
messageId: "indexOfNaN",
data: { methodName: "lastIndexOf" },
suggestions: []
}]
},
{
code: "foo.lastIndexOf((1, Number.NaN))",
options: [{ enforceForIndexOf: true }],
languageOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "indexOfNaN", data: { methodName: "lastIndexOf" } }]
errors: [{
messageId: "indexOfNaN",
data: { methodName: "lastIndexOf" },
suggestions: []
}]
}
]
});