Skip to content

Commit

Permalink
Add prefer-find rule
Browse files Browse the repository at this point in the history
  • Loading branch information
fisker committed May 17, 2020
1 parent c9edea7 commit 01bd77e
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 1 deletion.
21 changes: 21 additions & 0 deletions docs/rules/prefer-find.md
@@ -0,0 +1,21 @@
# Prefer `.find(…)` over first element of `.filter(…)` result

[`Array#find`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find) breaks the loop as soon as it finds a match.

This rule is fixable.

## Fail

```js
const item = array.filter(x => x === '🦄')[0];
```

```js
const item = array.filter(x => x === '🦄').shift();
```

## Pass

```js
const item = array.find(x => x === '🦄');
```
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -50,6 +50,7 @@ module.exports = {
'unicorn/prefer-add-event-listener': 'error',
'unicorn/prefer-dataset': 'error',
'unicorn/prefer-event-key': 'error',
'unicorn/prefer-find': 'error',
'unicorn/prefer-flat-map': 'error',
'unicorn/prefer-includes': 'error',
'unicorn/prefer-modern-dom-apis': 'error',
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Expand Up @@ -66,6 +66,7 @@ Configure it in `package.json`.
"unicorn/prefer-add-event-listener": "error",
"unicorn/prefer-dataset": "error",
"unicorn/prefer-event-key": "error",
"unicorn/prefer-find": "error",
"unicorn/prefer-flat-map": "error",
"unicorn/prefer-includes": "error",
"unicorn/prefer-modern-dom-apis": "error",
Expand Down Expand Up @@ -126,6 +127,7 @@ Configure it in `package.json`.
- [prefer-add-event-listener](docs/rules/prefer-add-event-listener.md) - Prefer `.addEventListener()` and `.removeEventListener()` over `on`-functions. *(partly fixable)*
- [prefer-dataset](docs/rules/prefer-dataset.md) - Prefer using `.dataset` on DOM elements over `.setAttribute(…)`. *(fixable)*
- [prefer-event-key](docs/rules/prefer-event-key.md) - Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. *(partly fixable)*
- [prefer-find](docs/rules/prefer-find.md) - Prefer `.find(…)` over first element of `.filter(…)` result. *(fixable)*
- [prefer-flat-map](docs/rules/prefer-flat-map.md) - Prefer `.flatMap(…)` over `.map(…).flat()`. *(fixable)*
- [prefer-includes](docs/rules/prefer-includes.md) - Prefer `.includes()` over `.indexOf()` when checking for existence or non-existence. *(fixable)*
- [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)*
Expand Down
72 changes: 72 additions & 0 deletions rules/prefer-find.js
@@ -0,0 +1,72 @@
'use strict';
const getDocumentationUrl = require('./utils/get-documentation-url');
const methodSelector = require('./utils/method-selector');

const MESSAGE_ID_ZERO_INDEX = 'prefer-find-over-filter-zero-index';
const MESSAGE_ID_SHIFT = 'prefer-find-over-filter-shift';

const zeroIndexSelector = [
'MemberExpression',
'[computed=true]',
'[property.type="Literal"]',
'[property.raw="0"]',
methodSelector({
name: 'filter',
min: 1,
max: 2,
property: 'object'
})
].join('');

const shiftSelector = [
methodSelector({
name: 'shift',
length: 0
}),
methodSelector({
name: 'filter',
min: 1,
max: 2,
property: 'callee.object'
})
].join('');

const create = context => {
return {
[zeroIndexSelector](node) {
context.report({
node,
messageId: MESSAGE_ID_ZERO_INDEX,
fix: fixer => [
fixer.replaceText(node.object.callee.property, 'find'),
fixer.removeRange([node.object.range[1], node.range[1]])
]
});
},
[shiftSelector](node) {
context.report({
node,
messageId: MESSAGE_ID_SHIFT,
fix: fixer => [
fixer.replaceText(node.callee.object.callee.property, 'find'),
fixer.removeRange([node.callee.object.range[1], node.range[1]])
]
});
}
};
};

module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
url: getDocumentationUrl(__filename)
},
fixable: 'code',
messages: {
[MESSAGE_ID_ZERO_INDEX]: 'Prefer `.find(…)` over `.filter(…)[0]`.',
[MESSAGE_ID_SHIFT]: 'Prefer `.find(…)` over `.filter(…).shift()`.'
}
}
};
2 changes: 1 addition & 1 deletion rules/utils/method-selector.js
Expand Up @@ -25,7 +25,7 @@ module.exports = options => {
];

if (name) {
selector.push(`[callee.property.name="${name}"]`);
selector.push(`[${prefix}callee.property.name="${name}"]`);
}

if (Array.isArray(names) && names.length !== 0) {
Expand Down
113 changes: 113 additions & 0 deletions test/prefer-find.js
@@ -0,0 +1,113 @@
import test from 'ava';
import {outdent} from 'outdent';
import avaRuleTester from 'eslint-ava-rule-tester';
import rule from '../rules/prefer-find';

const MESSAGE_ID_ZERO_INDEX = 'prefer-find-over-filter-zero-index';
const MESSAGE_ID_SHIFT = 'prefer-find-over-filter-shift';

const ruleTester = avaRuleTester(test, {
parserOptions: {
ecmaVersion: 2020
}
});

ruleTester.run('prefer-find', rule, {
valid: [
'array.find(foo)',

// Test `[0]`
'array.filter(foo)',
'array.filter(foo)[+0]',
'array.filter(foo)[-0]',
'array.filter(foo)[1-1]',
'array.filter(foo)["0"]',
'array.filter(foo).first',

// Test `.shift()`
// Not `CallExpression`
'array.filter(foo).shift',
// Not `MemberExpression`
'shift(array.filter(foo))',
// `callee.property` is not a `Identifier`
'array.filter(foo)["shift"]()',
// Computed
'array.filter(foo)[shift]()',
// Not `shift`
'array.filter(foo).notShift()',
// More or less argument(s)
'array.filter(foo).shift(extraArgument)',
'array.filter(foo).shift(...[])',

// Test `.filter()`
// Not `CallExpression`
'array.filter[0]',
'array.filter.shift()',
// Not `MemberExpression`
'filter(foo)[0]',
'filter(foo).shift()',
// `callee.property` is not a `Identifier`
'array["filter"](foo)[0]',
'array["filter"](foo).shift()',
// Computed
'array[filter](foo)[0]',
'array[filter](foo).shift()',
// Not `filter`
'array.notFilter(foo)[0]',
'array.notFilter(foo).shift()',
// More or less argument(s)
'array.filter()[0]',
'array.filter(foo, thisArgument, extraArgument)[0]',
'array.filter(...foo)[0]',
'array.filter().shift()',
'array.filter(foo, thisArgument, extraArgument).shift()',
'array.filter(...foo).shift()'
],
invalid: [
{
code: 'array.filter(foo)[0]',
output: 'array.find(foo)',
errors: [{messageId: MESSAGE_ID_ZERO_INDEX}]
},
{
code: 'array.filter(foo).shift()',
output: 'array.find(foo)',
errors: [{messageId: MESSAGE_ID_SHIFT}]
},
{
code: 'array.filter(foo, thisArgument)[0]',
output: 'array.find(foo, thisArgument)',
errors: [{messageId: MESSAGE_ID_ZERO_INDEX}]
},
{
code: 'array.filter(foo, thisArgument).shift()',
output: 'array.find(foo, thisArgument)',
errors: [{messageId: MESSAGE_ID_SHIFT}]
},
{
code: outdent`
const item = array
// comment 1
.filter(
// comment 2
x => x === '🦄'
)
// comment 3
.shift()
// comment 4
;
`,
output: outdent`
const item = array
// comment 1
.find(
// comment 2
x => x === '🦄'
)
// comment 4
;
`,
errors: [{messageId: MESSAGE_ID_SHIFT}]
}
]
});

0 comments on commit 01bd77e

Please sign in to comment.