Skip to content

Commit ee3a2e5

Browse files
prefer-includes: Add Array#some() check (#1097)
Co-authored-by: Fabrice TIERCELIN <[email protected]>
1 parent 5a931dd commit ee3a2e5

File tree

10 files changed

+650
-258
lines changed

10 files changed

+650
-258
lines changed

docs/rules/prefer-includes.md

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,108 @@
1-
# Prefer `.includes()` over `.indexOf()` when checking for existence or non-existence
1+
# Prefer `.includes()` over `.indexOf()` and `Array#some()` when checking for existence or non-existence
22

3-
All built-ins have `.includes()` in addition to `.indexOf()`. Prefer `.includes()` over comparing the value of `.indexOf()`
3+
All built-ins have `.includes()` in addition to `.indexOf()`. Prefer `.includes()` over comparing the value of `.indexOf()`.
44

5-
This rule is fixable.
5+
[`Array#some()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some) is intended for more complex needs. If you are just looking for the index where the given item is present, the code can be simplified to use [`Array#includes()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes). 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.
66

7+
This rule is fixable, unless the search expression in `Array#some()` has side effects.
78

89
## Fail
910

1011
```js
1112
[].indexOf('foo') !== -1;
13+
```
14+
15+
```js
1216
x.indexOf('foo') != -1;
17+
```
18+
19+
```js
1320
str.indexOf('foo') > -1;
21+
```
22+
23+
```js
1424
'foobar'.indexOf('foo') >= 0;
25+
```
26+
27+
```js
1528
x.indexOf('foo') === -1
1629
```
1730

31+
```js
32+
const isFound = foo.some(x => x === 'foo');
33+
```
34+
35+
```js
36+
const isFound = foo.some(x => 'foo' === x);
37+
```
38+
39+
```js
40+
const isFound = foo.some(x => {
41+
return x === 'foo';
42+
});
43+
```
1844

1945
## Pass
2046

2147
```js
2248
const str = 'foobar';
49+
```
50+
51+
```js
2352
str.indexOf('foo') !== -n;
53+
```
54+
55+
```js
2456
str.indexOf('foo') !== 1;
57+
```
58+
59+
```js
2560
!str.indexOf('foo') === 1;
61+
```
62+
63+
```js
2664
!str.indexOf('foo') === -n;
65+
```
66+
67+
```js
2768
str.includes('foo');
69+
```
70+
71+
```js
2872
[1,2,3].includes(4);
2973
```
74+
75+
```js
76+
const isFound = foo.includes('foo');
77+
```
78+
79+
```js
80+
const isFound = foo.some(x => x == undefined);
81+
```
82+
83+
```js
84+
const isFound = foo.some(x => x !== 'foo');
85+
```
86+
87+
```js
88+
const isFound = foo.some((x, index) => x === index);
89+
```
90+
91+
```js
92+
const isFound = foo.some(x => (x === 'foo') && isValid());
93+
```
94+
95+
```js
96+
const isFound = foo.some(x => y === 'foo');
97+
```
98+
99+
```js
100+
const isFound = foo.some(x => y.x === 'foo');
101+
```
102+
103+
```js
104+
const isFound = foo.some(x => {
105+
const bar = getBar();
106+
return x === bar;
107+
});
108+
```

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ Configure it in `package.json`.
162162
- [prefer-dom-node-dataset](docs/rules/prefer-dom-node-dataset.md) - Prefer using `.dataset` on DOM elements over `.setAttribute(…)`. *(fixable)*
163163
- [prefer-dom-node-remove](docs/rules/prefer-dom-node-remove.md) - Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`. *(fixable)*
164164
- [prefer-dom-node-text-content](docs/rules/prefer-dom-node-text-content.md) - Prefer `.textContent` over `.innerText`. *(fixable)*
165-
- [prefer-includes](docs/rules/prefer-includes.md) - Prefer `.includes()` over `.indexOf()` when checking for existence or non-existence. *(fixable)*
165+
- [prefer-includes](docs/rules/prefer-includes.md) - Prefer `.includes()` over `.indexOf()` and `Array#some()` when checking for existence or non-existence. *(partly fixable)*
166166
- [prefer-keyboard-event-key](docs/rules/prefer-keyboard-event-key.md) - Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. *(partly fixable)*
167167
- [prefer-math-trunc](docs/rules/prefer-math-trunc.md) - Enforce the use of `Math.trunc` instead of bitwise operators. *(partly fixable)*
168168
- [prefer-modern-dom-apis](docs/rules/prefer-modern-dom-apis.md) - Prefer `.before()` over `.insertBefore()`, `.replaceWith()` over `.replaceChild()`, prefer one of `.before()`, `.after()`, `.append()` or `.prepend()` over `insertAdjacentText()` and `insertAdjacentElement()`. *(fixable)*

rules/prefer-array-index-of.js

Lines changed: 6 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,13 @@
11
'use strict';
2-
const {hasSideEffect, isParenthesized, findVariable} = require('eslint-utils');
32
const getDocumentationUrl = require('./utils/get-documentation-url');
4-
const methodSelector = require('./utils/method-selector');
5-
const isFunctionSelfUsedInside = require('./utils/is-function-self-used-inside');
3+
const simpleArraySearchRule = require('./shared/simple-array-search-rule');
64

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-
const create = context => {
55-
const sourceCode = context.getSourceCode();
56-
const {scopeManager} = sourceCode;
57-
58-
return {
59-
[selector](node) {
60-
const [callback] = node.arguments;
61-
const binaryExpression = callback.body.type === 'BinaryExpression' ?
62-
callback.body :
63-
callback.body.body[0].argument;
64-
const [parameter] = callback.params;
65-
const {left, right} = binaryExpression;
66-
const {name} = parameter;
67-
68-
let searchValueNode;
69-
let parameterInBinaryExpression;
70-
if (isIdentifierNamed(left, name)) {
71-
searchValueNode = right;
72-
parameterInBinaryExpression = left;
73-
} else if (isIdentifierNamed(right, name)) {
74-
searchValueNode = left;
75-
parameterInBinaryExpression = right;
76-
} else {
77-
return;
78-
}
5+
const {messages, createListeners} = simpleArraySearchRule({
6+
method: 'findIndex',
7+
replacement: 'indexOf'
8+
});
799

80-
const callbackScope = scopeManager.acquire(callback);
81-
if (
82-
// `parameter` is used somewhere else
83-
findVariable(callbackScope, parameter).references.some(({identifier}) => identifier !== parameterInBinaryExpression) ||
84-
isFunctionSelfUsedInside(callback, callbackScope)
85-
) {
86-
return;
87-
}
88-
89-
const method = node.callee.property;
90-
const problem = {
91-
node: method,
92-
messageId: MESSAGE_ID_FIND_INDEX,
93-
suggest: []
94-
};
95-
96-
const fix = function * (fixer) {
97-
let text = sourceCode.getText(searchValueNode);
98-
if (isParenthesized(searchValueNode, sourceCode) && !isParenthesized(callback, sourceCode)) {
99-
text = `(${text})`;
100-
}
101-
102-
yield fixer.replaceText(method, 'indexOf');
103-
yield fixer.replaceText(callback, text);
104-
};
105-
106-
if (hasSideEffect(searchValueNode, sourceCode)) {
107-
problem.suggest.push({messageId: MESSAGE_ID_REPLACE, fix});
108-
} else {
109-
problem.fix = fix;
110-
}
111-
112-
context.report(problem);
113-
}
114-
};
115-
};
10+
const create = context => createListeners(context);
11611

11712
module.exports = {
11813
create,

rules/prefer-includes.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const getDocumentationUrl = require('./utils/get-documentation-url');
33
const isMethodNamed = require('./utils/is-method-named');
44
const isLiteralValue = require('./utils/is-literal-value');
5+
const simpleArraySearchRule = require('./shared/simple-array-search-rule');
56

67
const MESSAGE_ID = 'prefer-includes';
78
const messages = {
@@ -37,6 +38,11 @@ const report = (context, node, target, argumentsNodes) => {
3738
});
3839
};
3940

41+
const includesOverSomeRule = simpleArraySearchRule({
42+
method: 'some',
43+
replacement: 'includes'
44+
});
45+
4046
const create = context => ({
4147
BinaryExpression: node => {
4248
const {left, right, operator} = node;
@@ -69,7 +75,8 @@ const create = context => ({
6975
argumentsNodes
7076
);
7177
}
72-
}
78+
},
79+
...includesOverSomeRule.createListeners(context)
7380
});
7481

7582
module.exports = {
@@ -80,6 +87,9 @@ module.exports = {
8087
url: getDocumentationUrl(__filename)
8188
},
8289
fixable: 'code',
83-
messages
90+
messages: {
91+
...messages,
92+
...includesOverSomeRule.messages
93+
}
8494
}
8595
};

0 commit comments

Comments
 (0)