Skip to content

Commit 06ed7ee

Browse files
ankeetmainifiskersindresorhus
committed
Add no-reduce rule (#704)
Co-authored-by: fisker Cheung <[email protected]> Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 8b4c502 commit 06ed7ee

File tree

10 files changed

+332
-39
lines changed

10 files changed

+332
-39
lines changed

docs/rules/no-reduce.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Disallow `Array#reduce()` and `Array#reduceRight()`
2+
3+
`Array#reduce()` and `Array#reduceRight()` usually [result in hard-to-read code](https://twitter.com/jaffathecake/status/1213077702300852224). 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.
4+
5+
Use `eslint-disable` comment if you really need to use it.
6+
7+
This rule is not fixable.
8+
9+
## Fail
10+
11+
```js
12+
array.reduce(reducer, initialValue);
13+
```
14+
15+
```js
16+
array.reduceRight(reducer, initialValue);
17+
```
18+
19+
```js
20+
array.reduce(reducer);
21+
```
22+
23+
```js
24+
[].reduce.call(array, reducer);
25+
```
26+
27+
```js
28+
[].reduce.apply(array, [reducer, initialValue]);
29+
```
30+
31+
```js
32+
Array.prototype.reduce.call(array, reducer);
33+
```
34+
35+
## Pass
36+
37+
```js
38+
// eslint-disable-next-line
39+
array.reduce(reducer, initialValue);
40+
```
41+
42+
```js
43+
let result = initialValue;
44+
45+
for (const element of array) {
46+
result += element;
47+
}
48+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ module.exports = {
4040
'unicorn/no-new-buffer': 'error',
4141
'unicorn/no-null': 'error',
4242
'unicorn/no-process-exit': 'error',
43+
'unicorn/no-reduce': 'error',
4344
'unicorn/no-unreadable-array-destructuring': 'error',
4445
'unicorn/no-unsafe-regex': 'off',
4546
'unicorn/no-unused-properties': 'off',

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ Configure it in `package.json`.
5656
"unicorn/no-new-buffer": "error",
5757
"unicorn/no-null": "error",
5858
"unicorn/no-process-exit": "error",
59+
"unicorn/no-reduce": "error",
5960
"unicorn/no-unreadable-array-destructuring": "error",
6061
"unicorn/no-unsafe-regex": "off",
6162
"unicorn/no-unused-properties": "off",
@@ -115,6 +116,7 @@ Configure it in `package.json`.
115116
- [no-new-buffer](docs/rules/no-new-buffer.md) - Enforce the use of `Buffer.from()` and `Buffer.alloc()` instead of the deprecated `new Buffer()`. *(fixable)*
116117
- [no-null](docs/rules/no-null.md) - Disallow the use of the `null` literal.
117118
- [no-process-exit](docs/rules/no-process-exit.md) - Disallow `process.exit()`.
119+
- [no-reduce](docs/rules/no-reduce.md) - Disallow `Array#reduce()` and `Array#reduceRight()`.
118120
- [no-unreadable-array-destructuring](docs/rules/no-unreadable-array-destructuring.md) - Disallow unreadable array destructuring.
119121
- [no-unsafe-regex](docs/rules/no-unsafe-regex.md) - Disallow unsafe regular expressions.
120122
- [no-unused-properties](docs/rules/no-unused-properties.md) - Disallow unused object properties.

rules/expiring-todo-comments.js

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const semver = require('semver');
44
const ci = require('ci-info');
55
const baseRule = require('eslint/lib/rules/no-warning-comments');
66
const getDocumentationUrl = require('./utils/get-documentation-url');
7+
const {flatten} = require('lodash');
78

89
// `unicorn/` prefix is added to avoid conflicts with core rule
910
const MESSAGE_ID_AVOID_MULTIPLE_DATES = 'unicorn/avoidMultipleDates';
@@ -50,17 +51,21 @@ function parseTodoWithArguments(string, {terms}) {
5051

5152
const {rawArguments} = result.groups;
5253

53-
return rawArguments
54+
const parsedArguments = rawArguments
5455
.split(',')
55-
.map(argument => parseArgument(argument.trim()))
56-
.reduce((groups, argument) => {
57-
if (!groups[argument.type]) {
58-
groups[argument.type] = [];
59-
}
56+
.map(argument => parseArgument(argument.trim()));
57+
58+
return createArgumentGroup(parsedArguments);
59+
}
60+
61+
function createArgumentGroup(arguments_) {
62+
const groups = {};
63+
for (const {value, type} of arguments_) {
64+
groups[type] = groups[type] || [];
65+
groups[type].push(value);
66+
}
6067

61-
groups[argument.type].push(argument.value);
62-
return groups;
63-
}, {});
68+
return groups;
6469
}
6570

6671
function parseArgument(argumentString) {
@@ -220,24 +225,23 @@ const create = context => {
220225

221226
const sourceCode = context.getSourceCode();
222227
const comments = sourceCode.getAllComments();
223-
const unusedComments = comments
224-
.filter(token => token.type !== 'Shebang')
225-
// Block comments come as one.
226-
// Split for situations like this:
227-
// /*
228-
// * TODO [2999-01-01]: Validate this
229-
// * TODO [2999-01-01]: And this
230-
// * TODO [2999-01-01]: Also this
231-
// */
232-
.map(comment =>
233-
comment.value.split('\n').map(line => ({
234-
...comment,
235-
value: line
236-
}))
237-
)
238-
// Flatten
239-
.reduce((accumulator, array) => accumulator.concat(array), [])
240-
.filter(comment => processComment(comment));
228+
const unusedComments = flatten(
229+
comments
230+
.filter(token => token.type !== 'Shebang')
231+
// Block comments come as one.
232+
// Split for situations like this:
233+
// /*
234+
// * TODO [2999-01-01]: Validate this
235+
// * TODO [2999-01-01]: And this
236+
// * TODO [2999-01-01]: Also this
237+
// */
238+
.map(comment =>
239+
comment.value.split('\n').map(line => ({
240+
...comment,
241+
value: line
242+
}))
243+
)
244+
).filter(comment => processComment(comment));
241245

242246
// This is highly dependable on ESLint's `no-warning-comments` implementation.
243247
// What we do is patch the parts we know the rule will use, `getAllComments`.

rules/no-for-loop.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22
const getDocumentationUrl = require('./utils/get-documentation-url');
33
const isLiteralValue = require('./utils/is-literal-value');
4+
const {flatten} = require('lodash');
45

56
const defaultElementName = 'element';
67
const isLiteralZero = node => isLiteralValue(node, 0);
@@ -256,9 +257,7 @@ const getReferencesInChildScopes = (scope, name) => {
256257
const references = scope.references.filter(reference => reference.identifier.name === name);
257258
return [
258259
...references,
259-
...scope.childScopes
260-
.map(s => getReferencesInChildScopes(s, name))
261-
.reduce((accumulator, scopeReferences) => [...accumulator, ...scopeReferences], [])
260+
...flatten(scope.childScopes.map(s => getReferencesInChildScopes(s, name)))
262261
];
263262
};
264263

rules/no-reduce.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
'use strict';
2+
const methodSelector = require('./utils/method-selector');
3+
const getDocumentationUrl = require('./utils/get-documentation-url');
4+
5+
const MESSAGE_ID_REDUCE = 'reduce';
6+
const MESSAGE_ID_REDUCE_RIGHT = 'reduceRight';
7+
8+
const ignoredFirstArgumentSelector = `:not(${
9+
[
10+
'[arguments.0.type="Literal"]',
11+
'[arguments.0.type="Identifier"][arguments.0.name="undefined"]'
12+
].join(',')
13+
})`;
14+
15+
const PROTOTYPE_SELECTOR = [
16+
methodSelector({names: ['call', 'apply']}),
17+
ignoredFirstArgumentSelector,
18+
'[callee.object.type="MemberExpression"]',
19+
'[callee.object.computed=false]',
20+
`:matches(${
21+
['reduce', 'reduceRight'].map(method => `[callee.object.property.name="${method}"]`).join(', ')
22+
})`,
23+
'[callee.object.property.type="Identifier"]',
24+
`:matches(${
25+
[
26+
// `[].reduce`
27+
[
28+
'type="ArrayExpression"',
29+
'elements.length=0'
30+
],
31+
// `Array.prototype.reduce`
32+
[
33+
'type="MemberExpression"',
34+
'computed=false',
35+
'property.type="Identifier"',
36+
'property.name="prototype"',
37+
'object.type="Identifier"',
38+
'object.name="Array"'
39+
]
40+
].map(
41+
selectors => selectors
42+
.map(selector => `[callee.object.object.${selector}]`)
43+
.join('')
44+
).join(', ')
45+
})`
46+
].join('');
47+
48+
const METHOD_SELECTOR = [
49+
methodSelector({names: ['reduce', 'reduceRight'], min: 1, max: 2}),
50+
ignoredFirstArgumentSelector
51+
].join('');
52+
53+
const create = context => {
54+
return {
55+
[METHOD_SELECTOR](node) {
56+
// For arr.reduce()
57+
context.report({node: node.callee.property, messageId: node.callee.property.name});
58+
},
59+
[PROTOTYPE_SELECTOR](node) {
60+
// For cases [].reduce.call() and Array.prototype.reduce.call()
61+
context.report({node: node.callee.object.property, messageId: node.callee.object.property.name});
62+
}
63+
};
64+
};
65+
66+
module.exports = {
67+
create,
68+
meta: {
69+
type: 'suggestion',
70+
docs: {
71+
url: getDocumentationUrl(__filename)
72+
},
73+
messages: {
74+
[MESSAGE_ID_REDUCE]: '`Array#reduce()` is not allowed',
75+
[MESSAGE_ID_REDUCE_RIGHT]: '`Array#reduceRight()` is not allowed'
76+
}
77+
}
78+
};

rules/prefer-add-event-listener.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22
const getDocumentationUrl = require('./utils/get-documentation-url');
33
const domEventsJson = require('./utils/dom-events.json');
4+
const {flatten} = require('lodash');
45

56
const message = 'Prefer `{{replacement}}` over `{{method}}`.{{extra}}';
67
const extraMessages = {
@@ -9,7 +10,7 @@ const extraMessages = {
910
};
1011

1112
const nestedEvents = Object.values(domEventsJson);
12-
const eventTypes = new Set(nestedEvents.reduce((accumulatorEvents, events) => accumulatorEvents.concat(events), []));
13+
const eventTypes = new Set(flatten(nestedEvents));
1314
const getEventMethodName = memberExpression => memberExpression.property.name;
1415
const getEventTypeName = eventMethodName => eventMethodName.slice('on'.length);
1516

rules/utils/cartesian-product-samples.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
11
'use strict';
22

3+
const getTotal = combinations => {
4+
let total = 1;
5+
for (const {length} of combinations) {
6+
total *= length;
7+
}
8+
9+
return total;
10+
};
11+
312
module.exports = (combinations, length = Infinity) => {
4-
const total = combinations.reduce((total, {length}) => total * length, 1);
13+
const total = getTotal(combinations);
514

615
const samples = Array.from({length: Math.min(total, length)}, (_, sampleIndex) => {
716
let indexRemaining = sampleIndex;
8-
return combinations.reduceRight((combination, items) => {
17+
const combination = [];
18+
for (let i = combinations.length - 1; i >= 0; i--) {
19+
const items = combinations[i];
920
const {length} = items;
1021
const index = indexRemaining % length;
1122
indexRemaining = (indexRemaining - index) / length;
12-
return [items[index], ...combination];
13-
}, []);
23+
combination.unshift(items[index]);
24+
}
25+
26+
return combination;
1427
});
1528

1629
return {

test/lint/lint.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,26 @@ const eslint = new ESLint({
2222
}
2323
});
2424

25+
const sum = (collection, fieldName) => {
26+
let result = 0;
27+
for (const item of collection) {
28+
result += item[fieldName];
29+
}
30+
31+
return result;
32+
};
33+
2534
(async function () {
2635
const results = await eslint.lintFiles(files);
2736

2837
if (fix) {
2938
await ESLint.outputFixes(results);
3039
}
3140

32-
const errorCount = results.reduce((total, {errorCount}) => total + errorCount, 0);
33-
const warningCount = results.reduce((total, {warningCount}) => total + warningCount, 0);
34-
const fixableErrorCount = results.reduce((total, {fixableErrorCount}) => total + fixableErrorCount, 0);
35-
const fixableWarningCount = results.reduce((total, {fixableWarningCount}) => total + fixableWarningCount, 0);
41+
const errorCount = sum(results, 'errorCount');
42+
const warningCount = sum(results, 'warningCount');
43+
const fixableErrorCount = sum(results, 'fixableErrorCount');
44+
const fixableWarningCount = sum(results, 'fixableWarningCount');
3645

3746
const hasFixable = fixableErrorCount || fixableWarningCount;
3847

0 commit comments

Comments
 (0)