Skip to content

Commit 4ab6b8e

Browse files
authored
Rewrite no-fn-reference-in-iterator (#666)
1 parent c773c16 commit 4ab6b8e

12 files changed

+403
-147
lines changed

docs/rules/no-fn-reference-in-iterator.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ const unicorn = require('unicorn');
3030
//=> [2, 3, 5]
3131
```
3232

33-
This rule is fixable.
34-
3533

3634
## Fail
3735

index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ module.exports = {
3131
'unicorn/no-abusive-eslint-disable': 'error',
3232
'unicorn/no-array-instanceof': 'error',
3333
'unicorn/no-console-spaces': 'error',
34-
'unicorn/no-fn-reference-in-iterator': 'off',
34+
'unicorn/no-fn-reference-in-iterator': 'error',
3535
'unicorn/no-for-loop': 'error',
3636
'unicorn/no-hex-escape': 'error',
3737
'unicorn/no-keyword-prefix': 'off',

readme.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Configure it in `package.json`.
4747
"unicorn/no-abusive-eslint-disable": "error",
4848
"unicorn/no-array-instanceof": "error",
4949
"unicorn/no-console-spaces": "error",
50-
"unicorn/no-fn-reference-in-iterator": "off",
50+
"unicorn/no-fn-reference-in-iterator": "error",
5151
"unicorn/no-for-loop": "error",
5252
"unicorn/no-hex-escape": "error",
5353
"unicorn/no-keyword-prefix": "off",
@@ -105,7 +105,7 @@ Configure it in `package.json`.
105105
- [no-abusive-eslint-disable](docs/rules/no-abusive-eslint-disable.md) - Enforce specifying rules to disable in `eslint-disable` comments.
106106
- [no-array-instanceof](docs/rules/no-array-instanceof.md) - Require `Array.isArray()` instead of `instanceof Array`. *(fixable)*
107107
- [no-console-spaces](docs/rules/no-console-spaces.md) - Do not use leading/trailing space between `console.log` parameters. *(fixable)*
108-
- [no-fn-reference-in-iterator](docs/rules/no-fn-reference-in-iterator.md) - Prevent passing a function reference directly to iterator methods. *(fixable)*
108+
- [no-fn-reference-in-iterator](docs/rules/no-fn-reference-in-iterator.md) - Prevent passing a function reference directly to iterator methods.
109109
- [no-for-loop](docs/rules/no-for-loop.md) - Do not use a `for` loop that can be replaced with a `for-of` loop. *(partly fixable)*
110110
- [no-hex-escape](docs/rules/no-hex-escape.md) - Enforce the use of Unicode escapes instead of hexadecimal escapes. *(fixable)*
111111
- [no-keyword-prefix](docs/rules/no-keyword-prefix.md) - Disallow identifiers starting with `new` or `class`.

rules/custom-error-definition.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ const customErrorDefinition = (context, node) => {
108108

109109
const constructorBody = constructorBodyNode.body;
110110

111-
const superExpression = constructorBody.find(isSuperExpression);
111+
const superExpression = constructorBody.find(body => isSuperExpression(body));
112112
const messageExpressionIndex = constructorBody.findIndex(x => isAssignmentExpression(x, 'message'));
113113

114114
if (!superExpression) {

rules/expiring-todo-comments.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ const create = context => {
237237
)
238238
// Flatten
239239
.reduce((accumulator, array) => accumulator.concat(array), [])
240-
.filter(processComment);
240+
.filter(comment => processComment(comment));
241241

242242
// This is highly dependable on ESLint's `no-warning-comments` implementation.
243243
// What we do is patch the parts we know the rule will use, `getAllComments`.
Lines changed: 134 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,148 @@
11
'use strict';
2+
const {isParenthesized} = require('eslint-utils');
23
const getDocumentationUrl = require('./utils/get-documentation-url');
4+
const methodSelector = require('./utils/method-selector');
35

4-
const iteratorMethods = new Map([
5-
['map', 1],
6-
['forEach', 1],
7-
['every', 1],
8-
['filter', 1],
9-
['find', 1],
10-
['findIndex', 1],
11-
['some', 1],
12-
['reduce', 2],
13-
['reduceRight', 2]
14-
]);
15-
16-
const functionWhitelist = new Set([
17-
'Boolean'
18-
]);
19-
20-
const calleeBlacklist = [
6+
const ERROR_WITH_NAME_MESSAGE_ID = 'error-with-name';
7+
const ERROR_WITHOUT_NAME_MESSAGE_ID = 'error-without-name';
8+
const REPLACE_WITH_NAME_MESSAGE_ID = 'replace-with-name';
9+
const REPLACE_WITHOUT_NAME_MESSAGE_ID = 'replace-without-name';
10+
11+
const iteratorMethods = [
12+
['every'],
13+
['filter'],
14+
['find'],
15+
['findIndex'],
16+
['flatMap'],
17+
['forEach'],
18+
['map'],
19+
[
20+
'reduce', {
21+
parameters: [
22+
'accumulator',
23+
'element',
24+
'index',
25+
'array'
26+
],
27+
minParameters: 2,
28+
ignore: []
29+
}
30+
],
31+
[
32+
'reduceRight', {
33+
parameters: [
34+
'accumulator',
35+
'element',
36+
'index',
37+
'array'
38+
],
39+
minParameters: 2,
40+
ignore: []
41+
}
42+
],
43+
['some']
44+
].map(([method, options]) => {
45+
options = {
46+
parameters: ['element', 'index', 'array'],
47+
ignore: ['Boolean'],
48+
minParameters: 1,
49+
...options
50+
};
51+
return [method, options];
52+
});
53+
54+
const ignoredCallee = [
2155
'Promise',
2256
'React.children',
57+
'lodash',
58+
'underscore',
2359
'_',
2460
'Async',
2561
'async'
2662
];
2763

28-
const isIteratorMethod = node => node.callee.property && iteratorMethods.has(node.callee.property.name);
29-
const hasFunctionArgument = node => node.arguments.length > 0 && (node.arguments[0].type === 'Identifier' || node.arguments[0].type === 'CallExpression') && !functionWhitelist.has(node.arguments[0].name);
30-
31-
const getNumberOfArguments = node => node.callee.property && iteratorMethods.get(node.callee.property.name);
32-
const parseArgument = (context, argument) => argument.type === 'Identifier' ? argument.name : context.getSourceCode().getText(argument);
33-
34-
const fix = (context, node) => {
35-
const numberOfArguments = getNumberOfArguments(node);
36-
const argument = node.arguments[0];
37-
const argumentString = numberOfArguments === 1 ? 'x' : 'a, b';
38-
39-
return fixer => fixer.replaceText(argument, `${numberOfArguments === 1 ? argumentString : `(${argumentString})`} => ${parseArgument(context, argument)}(${argumentString})`);
40-
};
41-
4264
const toSelector = name => {
4365
const splitted = name.split('.');
4466
return `[callee.${'object.'.repeat(splitted.length)}name!="${splitted.shift()}"]`;
4567
};
4668

4769
// Select all the call expressions except the ones present in the blacklist
48-
const selector = `CallExpression${calleeBlacklist.map(toSelector).join('')}`;
49-
50-
const create = context => ({
51-
[selector]: node => {
52-
if (
53-
isIteratorMethod(node) &&
54-
hasFunctionArgument(node) &&
55-
node.arguments.length <= getNumberOfArguments(node)
56-
) {
57-
const [argument] = node.arguments;
58-
59-
context.report({
60-
node: argument,
61-
message: 'Do not pass a function reference directly to an iterator method.',
62-
fix: fix(context, node)
63-
});
64-
}
70+
const ignoredCalleeSelector = `${ignoredCallee.map(name => toSelector(name)).join('')}`;
71+
72+
function check(context, node, method, options) {
73+
const {type} = node;
74+
75+
if (type === 'FunctionExpression' || type === 'ArrowFunctionExpression') {
76+
return;
6577
}
66-
});
78+
79+
const name = type === 'Identifier' ? node.name : '';
80+
81+
if (type === 'Identifier' && options.ignore.includes(name)) {
82+
return;
83+
}
84+
85+
const problem = {
86+
node,
87+
messageId: name ? ERROR_WITH_NAME_MESSAGE_ID : ERROR_WITHOUT_NAME_MESSAGE_ID,
88+
data: {
89+
name,
90+
method
91+
},
92+
suggest: []
93+
};
94+
95+
const {parameters, minParameters} = options;
96+
for (let parameterLength = minParameters; parameterLength <= parameters.length; parameterLength++) {
97+
const suggestionParameters = parameters.slice(0, parameterLength).join(', ');
98+
99+
const suggest = {
100+
messageId: name ? REPLACE_WITH_NAME_MESSAGE_ID : REPLACE_WITHOUT_NAME_MESSAGE_ID,
101+
data: {
102+
name,
103+
parameters: suggestionParameters
104+
},
105+
fix: fixer => {
106+
const sourceCode = context.getSourceCode();
107+
let nodeText = sourceCode.getText(node);
108+
if (isParenthesized(node, sourceCode) || type === 'ConditionalExpression') {
109+
nodeText = `(${nodeText})`;
110+
}
111+
112+
return fixer.replaceText(
113+
node,
114+
`(${suggestionParameters}) => ${nodeText}(${suggestionParameters})`
115+
);
116+
}
117+
};
118+
119+
problem.suggest.push(suggest);
120+
}
121+
122+
context.report(problem);
123+
}
124+
125+
const create = context => {
126+
const sourceCode = context.getSourceCode();
127+
const rules = {};
128+
129+
for (const [method, options] of iteratorMethods) {
130+
const selector = [
131+
methodSelector({
132+
name: method,
133+
min: 1,
134+
max: 2
135+
}),
136+
ignoredCalleeSelector
137+
].join('');
138+
rules[selector] = node => {
139+
const [iterator] = node.arguments;
140+
check(context, iterator, method, options, sourceCode);
141+
};
142+
}
143+
144+
return rules;
145+
};
67146

68147
module.exports = {
69148
create,
@@ -72,6 +151,11 @@ module.exports = {
72151
docs: {
73152
url: getDocumentationUrl(__filename)
74153
},
75-
fixable: 'code'
154+
messages: {
155+
[ERROR_WITH_NAME_MESSAGE_ID]: 'Do not pass function `{{name}}` directly to `.{{method}}(…)`.',
156+
[ERROR_WITHOUT_NAME_MESSAGE_ID]: 'Do not pass function directly to `.{{method}}(…)`.',
157+
[REPLACE_WITH_NAME_MESSAGE_ID]: 'Replace function `{{name}}` with `… => {{name}}({{parameters}})`.',
158+
[REPLACE_WITHOUT_NAME_MESSAGE_ID]: 'Replace function with `… => …({{parameters}})`.'
159+
}
76160
}
77161
};

rules/no-unreadable-array-destructuring.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const isCommaFollowedWithComma = (element, index, array) =>
88
const create = context => {
99
return {
1010
'ArrayPattern[elements.length>=3]': node => {
11-
if (node.elements.some(isCommaFollowedWithComma)) {
11+
if (node.elements.some((element, index, array) => isCommaFollowedWithComma(element, index, array))) {
1212
context.report({
1313
node,
1414
message

rules/no-unused-properties.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,11 @@ const create = context => {
210210
};
211211

212212
const checkVariables = scope => {
213-
scope.variables.forEach(checkVariable);
213+
scope.variables.forEach(variable => checkVariable(variable));
214214
};
215215

216216
const checkChildScopes = scope => {
217-
scope.childScopes.forEach(checkScope);
217+
scope.childScopes.forEach(scope => checkScope(scope));
218218
};
219219

220220
const checkScope = scope => {

rules/prefer-spread.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const create = context => {
2424
node,
2525
message: 'Prefer the spread operator over `Array.from()`.',
2626
fix: fixer => {
27-
const [arrayLikeArgument, mapFn, thisArgument] = node.arguments.map(getSource);
27+
const [arrayLikeArgument, mapFn, thisArgument] = node.arguments.map(node => getSource(node));
2828
let replacement = `${
2929
needsSemicolon(sourceCode.getTokenBefore(node), sourceCode) ? ';' : ''
3030
}[...${arrayLikeArgument}]`;

rules/prevent-abbreviations.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,10 @@ const getWordReplacements = (word, {replacements, whitelist}) => {
278278

279279
let wordReplacement = [];
280280
if (replacement) {
281+
const transform = isUpperFirst(word) ? upperFirst : lowerFirst;
281282
wordReplacement = [...replacement.keys()]
282283
.filter(name => replacement.get(name))
283-
.map(isUpperFirst(word) ? upperFirst : lowerFirst);
284+
.map(name => transform(name));
284285
}
285286

286287
return wordReplacement.length > 0 ? wordReplacement.sort() : [];
@@ -391,7 +392,7 @@ const isExportedIdentifier = identifier => {
391392
};
392393

393394
const shouldFix = variable => {
394-
return !getVariableIdentifiers(variable).some(isExportedIdentifier);
395+
return !getVariableIdentifiers(variable).some(identifier => isExportedIdentifier(identifier));
395396
};
396397

397398
const isDefaultOrNamespaceImportName = identifier => {
@@ -634,11 +635,11 @@ const create = context => {
634635
};
635636

636637
const checkVariables = scope => {
637-
scope.variables.forEach(checkPossiblyWeirdClassVariable);
638+
scope.variables.forEach(variable => checkPossiblyWeirdClassVariable(variable));
638639
};
639640

640641
const checkChildScopes = scope => {
641-
scope.childScopes.forEach(checkScope);
642+
scope.childScopes.forEach(scope => checkScope(scope));
642643
};
643644

644645
const checkScope = scope => {

0 commit comments

Comments
 (0)