Skip to content

Commit 517a782

Browse files
Fabrice-TIERCELINfiskersindresorhus
authored
Add prefer-array-index-of rule (#920)
Co-authored-by: fisker <[email protected]> Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 7b74b40 commit 517a782

File tree

7 files changed

+600
-0
lines changed

7 files changed

+600
-0
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Prefer `Array#indexOf()` over `Array#findIndex()` when looking for the index of an item
2+
3+
[`Array#findIndex()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex) is intended for more complex needs. If you are just looking for the index where the given item is present, then the code can be simplified to use [`Array#indexOf()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf). This applies to any search with a literal, a variable, or any expression that doesn't have any explicit side effects. However, if the expression you are looking for relies on an item related to the function (its arguments, the function self, etc.), the case is still valid.
4+
5+
This rule is fixable, unless the search expression has side effects.
6+
7+
## Fail
8+
9+
```js
10+
const index = foo.findIndex(x => x === 'foo');
11+
```
12+
13+
```js
14+
const index = foo.findIndex(x => 'foo' === x);
15+
```
16+
17+
```js
18+
const index = foo.findIndex(x => {
19+
return x === 'foo';
20+
});
21+
```
22+
23+
## Pass
24+
25+
```js
26+
const index = foo.indexOf('foo');
27+
```
28+
29+
```js
30+
const index = foo.findIndex(x => x == undefined);
31+
```
32+
33+
```js
34+
const index = foo.findIndex(x => x !== 'foo');
35+
```
36+
37+
```js
38+
const index = foo.findIndex((x, index) => x === index);
39+
```
40+
41+
```js
42+
const index = foo.findIndex(x => (x === 'foo') && isValid());
43+
```
44+
45+
```js
46+
const index = foo.findIndex(x => y === 'foo');
47+
```
48+
49+
```js
50+
const index = foo.findIndex(x => y.x === 'foo');
51+
```
52+
53+
```js
54+
const index = foo.findIndex(x => {
55+
const bar = getBar();
56+
return x === bar;
57+
});
58+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ module.exports = {
8181
'unicorn/prefer-array-find': 'error',
8282
// TODO: Enable this by default when targeting Node.js 12.
8383
'unicorn/prefer-array-flat-map': 'off',
84+
'unicorn/prefer-array-index-of': 'error',
8485
'unicorn/prefer-array-some': 'error',
8586
'unicorn/prefer-date-now': 'error',
8687
'unicorn/prefer-default-parameters': 'error',

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Configure it in `package.json`.
7373
"unicorn/prefer-add-event-listener": "error",
7474
"unicorn/prefer-array-find": "error",
7575
"unicorn/prefer-array-flat-map": "error",
76+
"unicorn/prefer-array-index-of": "error",
7677
"unicorn/prefer-array-some": "error",
7778
"unicorn/prefer-date-now": "error",
7879
"unicorn/prefer-default-parameters": "error",
@@ -147,6 +148,7 @@ Configure it in `package.json`.
147148
- [prefer-add-event-listener](docs/rules/prefer-add-event-listener.md) - Prefer `.addEventListener()` and `.removeEventListener()` over `on`-functions. *(partly fixable)*
148149
- [prefer-array-find](docs/rules/prefer-array-find.md) - Prefer `.find(…)` over the first element from `.filter(…)`. *(partly fixable)*
149150
- [prefer-array-flat-map](docs/rules/prefer-array-flat-map.md) - Prefer `.flatMap(…)` over `.map(…).flat()`. *(fixable)*
151+
- [prefer-array-index-of](docs/rules/prefer-array-index-of.md) - Prefer `Array#indexOf()` over `Array#findIndex()` when looking for the index of an item. *(partly fixable)*
150152
- [prefer-array-some](docs/rules/prefer-array-some.md) - Prefer `.some(…)` over `.find(…)`.
151153
- [prefer-date-now](docs/rules/prefer-date-now.md) - Prefer `Date.now()` to get the number of milliseconds since the Unix Epoch. *(fixable)*
152154
- [prefer-default-parameters](docs/rules/prefer-default-parameters.md) - Prefer default parameters over reassignment. *(fixable)*

rules/prefer-array-index-of.js

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
'use strict';
2+
const {hasSideEffect, isParenthesized, findVariable} = require('eslint-utils');
3+
const getDocumentationUrl = require('./utils/get-documentation-url');
4+
const methodSelector = require('./utils/method-selector');
5+
const getVariableIdentifiers = require('./utils/get-variable-identifiers');
6+
7+
const MESSAGE_ID_FIND_INDEX = 'findIndex';
8+
const MESSAGE_ID_REPLACE = 'replaceFindIndex';
9+
const messages = {
10+
[MESSAGE_ID_FIND_INDEX]: 'Use `.indexOf()` instead of `.findIndex()` when looking for the index of an item.',
11+
[MESSAGE_ID_REPLACE]: 'Replace `.findIndex()` with `.indexOf()`.'
12+
};
13+
14+
const getBinaryExpressionSelector = path => [
15+
`[${path}.type="BinaryExpression"]`,
16+
`[${path}.operator="==="]`,
17+
`:matches([${path}.left.type="Identifier"], [${path}.right.type="Identifier"])`
18+
].join('');
19+
const getFunctionSelector = path => [
20+
`[${path}.generator=false]`,
21+
`[${path}.async=false]`,
22+
`[${path}.params.length=1]`,
23+
`[${path}.params.0.type="Identifier"]`
24+
].join('');
25+
const selector = [
26+
methodSelector({
27+
name: 'findIndex',
28+
length: 1
29+
}),
30+
`:matches(${
31+
[
32+
// Matches `foo.findIndex(bar => bar === baz)`
33+
[
34+
'[arguments.0.type="ArrowFunctionExpression"]',
35+
getFunctionSelector('arguments.0'),
36+
getBinaryExpressionSelector('arguments.0.body')
37+
].join(''),
38+
// Matches `foo.findIndex(bar => {return bar === baz})`
39+
// Matches `foo.findIndex(function (bar) {return bar === baz})`
40+
[
41+
':matches([arguments.0.type="ArrowFunctionExpression"], [arguments.0.type="FunctionExpression"])',
42+
getFunctionSelector('arguments.0'),
43+
'[arguments.0.body.type="BlockStatement"]',
44+
'[arguments.0.body.body.length=1]',
45+
'[arguments.0.body.body.0.type="ReturnStatement"]',
46+
getBinaryExpressionSelector('arguments.0.body.body.0.argument')
47+
].join('')
48+
].join(', ')
49+
})`
50+
].join('');
51+
52+
const isIdentifierNamed = ({type, name}, expectName) => type === 'Identifier' && name === expectName;
53+
54+
function isVariablesInCallbackUsed(scopeManager, callback, parameterInBinaryExpression) {
55+
const scope = scopeManager.acquire(callback);
56+
57+
// `parameter` is used on somewhere else
58+
const [parameter] = callback.params;
59+
if (
60+
getVariableIdentifiers(findVariable(scope, parameter))
61+
.some(identifier => identifier !== parameter && identifier !== parameterInBinaryExpression)
62+
) {
63+
return true;
64+
}
65+
66+
if (callback.type === 'FunctionExpression') {
67+
// `this` is used
68+
if (scope.thisFound) {
69+
return true;
70+
}
71+
72+
// The function name is used
73+
if (
74+
callback.id &&
75+
getVariableIdentifiers(findVariable(scope, callback.id))
76+
.some(identifier => identifier !== callback.id)
77+
) {
78+
return true;
79+
}
80+
81+
// `arguments` is used
82+
if (scope.references.some(({identifier: {name}}) => name === 'arguments')) {
83+
return true;
84+
}
85+
}
86+
}
87+
88+
const create = context => {
89+
const sourceCode = context.getSourceCode();
90+
const {scopeManager} = sourceCode;
91+
92+
return {
93+
[selector](node) {
94+
const [callback] = node.arguments;
95+
const binaryExpression = callback.body.type === 'BinaryExpression' ?
96+
callback.body :
97+
callback.body.body[0].argument;
98+
const [parameter] = callback.params;
99+
const {left, right} = binaryExpression;
100+
const {name} = parameter;
101+
102+
let searchValueNode;
103+
let parameterInBinaryExpression;
104+
if (isIdentifierNamed(left, name)) {
105+
searchValueNode = right;
106+
parameterInBinaryExpression = left;
107+
} else if (isIdentifierNamed(right, name)) {
108+
searchValueNode = left;
109+
parameterInBinaryExpression = right;
110+
} else {
111+
return;
112+
}
113+
114+
if (isVariablesInCallbackUsed(scopeManager, callback, parameterInBinaryExpression)) {
115+
return;
116+
}
117+
118+
const method = node.callee.property;
119+
const problem = {
120+
node: method,
121+
messageId: MESSAGE_ID_FIND_INDEX,
122+
suggest: []
123+
};
124+
125+
function * fix(fixer) {
126+
let text = sourceCode.getText(searchValueNode);
127+
if (isParenthesized(searchValueNode, sourceCode) && !isParenthesized(callback, sourceCode)) {
128+
text = `(${text})`;
129+
}
130+
131+
yield fixer.replaceText(method, 'indexOf');
132+
yield fixer.replaceText(callback, text);
133+
}
134+
135+
if (hasSideEffect(searchValueNode, sourceCode)) {
136+
problem.suggest.push({messageId: MESSAGE_ID_REPLACE, fix});
137+
} else {
138+
problem.fix = fix;
139+
}
140+
141+
context.report(problem);
142+
}
143+
};
144+
};
145+
146+
module.exports = {
147+
create,
148+
meta: {
149+
type: 'suggestion',
150+
docs: {
151+
url: getDocumentationUrl(__filename)
152+
},
153+
fixable: 'code',
154+
messages
155+
}
156+
};

0 commit comments

Comments
 (0)