Skip to content

Commit

Permalink
Add allowSimpleOperations option to no-array-reduce rule (#1418)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
Co-authored-by: fisker Cheung <lionkay@gmail.com>
  • Loading branch information
3 people committed Aug 2, 2021
1 parent efdd90e commit 153eb2c
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 26 deletions.
28 changes: 27 additions & 1 deletion docs/rules/no-array-reduce.md
@@ -1,6 +1,8 @@
# Disallow `Array#reduce()` and `Array#reduceRight()`

`Array#reduce()` and `Array#reduceRight()` usually result in [hard-to-read](https://twitter.com/jaffathecake/status/1213077702300852224) and [less performant](https://www.richsnapp.com/article/2019/06-09-reduce-spread-anti-pattern) code. In almost every case, it can be replaced by `.map`, `.filter`, or a `for-of` loop. It's only somewhat useful in the rare case of summing up numbers.
`Array#reduce()` and `Array#reduceRight()` usually result in [hard-to-read](https://twitter.com/jaffathecake/status/1213077702300852224) and [less performant](https://www.richsnapp.com/article/2019/06-09-reduce-spread-anti-pattern) code. In almost every case, it can be replaced by `.map`, `.filter`, or a `for-of` loop.

It's only somewhat useful in the rare case of summing up numbers, which is allowed by default.

Use `eslint-disable` comment if you really need to use it.

Expand Down Expand Up @@ -39,10 +41,34 @@ Array.prototype.reduce.call(array, reducer);
array.reduce(reducer, initialValue);
```

```js
array.reduce((total, value) => total + value);
```

```js
let result = initialValue;

for (const element of array) {
result += element;
}
```
## Options

### allowSimpleOperations

Type: `boolean`\
Default: `true`

Allow simple operations (like addition, subtraction, etc.) in a `reduce` call.

Set it to `false` to disable reduce completely.

```js
// eslint unicorn/no-array-reduce: ["error", {"allowSimpleOperations": true}]
array.reduce((total, item) => total + item) // Passes
```

```js
// eslint unicorn/no-array-reduce: ["error", {"allowSimpleOperations": false}]
array.reduce((total, item) => total + item) // Fails
```
39 changes: 37 additions & 2 deletions rules/no-array-reduce.js
@@ -1,4 +1,5 @@
'use strict';
const {get} = require('lodash');
const {methodCallSelector} = require('./selectors/index.js');
const {arrayPrototypeMethodSelector, notFunctionSelector, matches} = require('./selectors/index.js');

Expand Down Expand Up @@ -34,14 +35,47 @@ const selector = matches([
].join(''),
]);

const create = () => {
const schema = [
{
type: 'object',
properties: {
allowSimpleOperations: {
type: 'boolean',
default: true,
},
},
},
];

const create = context => {
const {allowSimpleOperations} = {allowSimpleOperations: true, ...context.options[0]};

return {
[selector](node) {
return {
const callback = get(node, 'parent.parent.arguments[0]', {});
const problem = {
node,
messageId: MESSAGE_ID,
data: {method: node.name},
};

if (!allowSimpleOperations) {
return problem;
}

if (callback.type === 'ArrowFunctionExpression' && callback.body.type === 'BinaryExpression') {
return;
}

if ((callback.type === 'ArrowFunctionExpression' || callback.type === 'FunctionExpression') &&
callback.body.type === 'BlockStatement' &&
callback.body.body.length === 1 &&
callback.body.body[0].type === 'ReturnStatement' &&
callback.body.body[0].argument.type === 'BinaryExpression') {
return;
}

return problem;
},
};
};
Expand All @@ -53,6 +87,7 @@ module.exports = {
docs: {
description: 'Disallow `Array#reduce()` and `Array#reduceRight()`.',
},
schema,
messages,
},
};
111 changes: 88 additions & 23 deletions test/no-array-reduce.mjs
Expand Up @@ -83,42 +83,107 @@ test({
// We are not checking arguments length

// `reduce-like`
'arr.reducex(foo)',
'arr.xreduce(foo)',
'[].reducex.call(arr, foo)',
'[].xreduce.call(arr, foo)',
'Array.prototype.reducex.call(arr, foo)',
'Array.prototype.xreduce.call(arr, foo)',
'array.reducex(foo)',
'array.xreduce(foo)',
'[].reducex.call(array, foo)',
'[].xreduce.call(array, foo)',
'Array.prototype.reducex.call(array, foo)',
'Array.prototype.xreduce.call(array, foo)',

// Second argument is not a function
...notFunctionTypes.map(data => `Array.prototype.reduce.call(foo, ${data})`),

].flatMap(code => [code, code.replace('reduce', 'reduceRight')]),
// Option: allowSimpleOperations
'array.reduce((total, item) => total + item)',
'array.reduce((total, item) => { return total - item })',
'array.reduce(function (total, item) { return total * item })',
'array.reduce((total, item) => total + item, 0)',
'array.reduce((total, item) => { return total - item }, 0 )',
'array.reduce(function (total, item) { return total * item }, 0)',
outdent`
array.reduce((total, item) => {
return (total / item) * 100;
}, 0);
`,
'array.reduce((total, item) => { return total + item }, 0)',
].flatMap(testCase => [testCase, testCase.replace('reduce', 'reduceRight')]),
invalid: [
'arr.reduce((total, item) => total + item)',
'arr.reduce((total, item) => total + item, 0)',
'arr.reduce(function (total, item) { return total + item }, 0)',
'arr.reduce(function (total, item) { return total + item })',
'arr.reduce((str, item) => str += item, "")',
'array.reduce((str, item) => str += item, "")',
outdent`
arr.reduce((obj, item) => {
array.reduce((obj, item) => {
obj[item] = null;
return obj;
}, {})
`,
'arr.reduce((obj, item) => ({ [item]: null }), {})',
'array.reduce((obj, item) => ({ [item]: null }), {})',
outdent`
const hyphenate = (str, char) => \`\${str}-\${char}\`;
["a", "b", "c"].reduce(hyphenate);
`,
'[].reduce.call(arr, (s, i) => s + i)',
'[].reduce.call(arr, sum);',
'[].reduce.call(array, (s, i) => s + i)',
'[].reduce.call(array, sum);',
'[].reduce.call(sum);',
'Array.prototype.reduce.call(arr, (s, i) => s + i)',
'Array.prototype.reduce.call(arr, sum);',
'[].reduce.apply(arr, [(s, i) => s + i])',
'[].reduce.apply(arr, [sum]);',
'Array.prototype.reduce.apply(arr, [(s, i) => s + i])',
'Array.prototype.reduce.apply(arr, [sum]);',
].flatMap(code => [{code, errors: errorsReduce}, {code: code.replace('reduce', 'reduceRight'), errors: errorsReduceRight}]),
'Array.prototype.reduce.call(array, (s, i) => s + i)',
'Array.prototype.reduce.call(array, sum);',
'[].reduce.apply(array, [(s, i) => s + i])',
'[].reduce.apply(array, [sum]);',
'Array.prototype.reduce.apply(array, [(s, i) => s + i])',
'Array.prototype.reduce.apply(array, [sum]);',
outdent`
array.reduce((total, item) => {
return total + doComplicatedThings(item);
function doComplicatedThings(item) {
return item + 1;
}
}, 0);
`,

// Option: allowSimpleOperations
{
code: 'array.reduce((total, item) => total + item)',
options: [{allowSimpleOperations: false}],
},
{
code: 'array.reduce((total, item) => { return total - item })',
options: [{allowSimpleOperations: false}],
},
{
code: 'array.reduce(function (total, item) { return total * item })',
options: [{allowSimpleOperations: false}],
},
{
code: 'array.reduce((total, item) => total + item, 0)',
options: [{allowSimpleOperations: false}],
},
{
code: 'array.reduce((total, item) => { return total - item }, 0 )',
options: [{allowSimpleOperations: false}],
},
{
code: 'array.reduce(function (total, item) { return total * item }, 0)',
options: [{allowSimpleOperations: false}],
},
{
code: outdent`
array.reduce((total, item) => {
return (total / item) * 100;
}, 0);
`,
options: [{allowSimpleOperations: false}],
},
].flatMap(testCase => {
const {code, options} = testCase;

if (options) {
return [
{code, errors: errorsReduce, options},
{code: code.replace('reduce', 'reduceRight'), errors: errorsReduceRight, options},
];
}

return [
{code: testCase, errors: errorsReduce},
{code: testCase.replace('reduce', 'reduceRight'), errors: errorsReduceRight},
];
}),
});

0 comments on commit 153eb2c

Please sign in to comment.