UNPKG

4.08 kBJavaScriptView Raw
1'use strict';
2const {isParenthesized} = require('eslint-utils');
3const getDocumentationUrl = require('./utils/get-documentation-url');
4const methodSelector = require('./utils/method-selector');
5
6const ERROR_WITH_NAME_MESSAGE_ID = 'error-with-name';
7const ERROR_WITHOUT_NAME_MESSAGE_ID = 'error-without-name';
8const REPLACE_WITH_NAME_MESSAGE_ID = 'replace-with-name';
9const REPLACE_WITHOUT_NAME_MESSAGE_ID = 'replace-without-name';
10
11const iteratorMethods = [
12 ['every'],
13 [
14 'filter', {
15 extraSelector: '[callee.object.name!="Vue"]'
16 }
17 ],
18 ['find'],
19 ['findIndex'],
20 ['flatMap'],
21 ['forEach'],
22 ['map'],
23 [
24 'reduce', {
25 parameters: [
26 'accumulator',
27 'element',
28 'index',
29 'array'
30 ],
31 minParameters: 2,
32 ignore: []
33 }
34 ],
35 [
36 'reduceRight', {
37 parameters: [
38 'accumulator',
39 'element',
40 'index',
41 'array'
42 ],
43 minParameters: 2,
44 ignore: []
45 }
46 ],
47 ['some']
48].map(([method, options]) => {
49 options = {
50 parameters: ['element', 'index', 'array'],
51 ignore: ['Boolean'],
52 minParameters: 1,
53 extraSelector: '',
54 ...options
55 };
56 return [method, options];
57});
58
59const ignoredCallee = [
60 'Promise',
61 'React.children',
62 'lodash',
63 'underscore',
64 '_',
65 'Async',
66 'async'
67];
68
69const toSelector = name => {
70 const splitted = name.split('.');
71 return `[callee.${'object.'.repeat(splitted.length)}name!="${splitted.shift()}"]`;
72};
73
74// Select all the call expressions except the ones present in the blacklist
75const ignoredCalleeSelector = [
76 // `this.{map, filter, …}()`
77 '[callee.object.type!="ThisExpression"]',
78 ...ignoredCallee.map(name => toSelector(name))
79].join('');
80
81function check(context, node, method, options) {
82 const {type} = node;
83
84 const name = type === 'Identifier' ? node.name : '';
85
86 if (type === 'Identifier' && options.ignore.includes(name)) {
87 return;
88 }
89
90 const problem = {
91 node,
92 messageId: name ? ERROR_WITH_NAME_MESSAGE_ID : ERROR_WITHOUT_NAME_MESSAGE_ID,
93 data: {
94 name,
95 method
96 },
97 suggest: []
98 };
99
100 const {parameters, minParameters} = options;
101 for (let parameterLength = minParameters; parameterLength <= parameters.length; parameterLength++) {
102 const suggestionParameters = parameters.slice(0, parameterLength).join(', ');
103
104 const suggest = {
105 messageId: name ? REPLACE_WITH_NAME_MESSAGE_ID : REPLACE_WITHOUT_NAME_MESSAGE_ID,
106 data: {
107 name,
108 parameters: suggestionParameters
109 },
110 fix: fixer => {
111 const sourceCode = context.getSourceCode();
112 let nodeText = sourceCode.getText(node);
113 if (isParenthesized(node, sourceCode) || type === 'ConditionalExpression') {
114 nodeText = `(${nodeText})`;
115 }
116
117 return fixer.replaceText(
118 node,
119 `(${suggestionParameters}) => ${nodeText}(${suggestionParameters})`
120 );
121 }
122 };
123
124 problem.suggest.push(suggest);
125 }
126
127 context.report(problem);
128}
129
130const ignoredFirstArgumentSelector = `:not(${
131 [
132 '[arguments.0.type="FunctionExpression"]',
133 '[arguments.0.type="ArrowFunctionExpression"]',
134 '[arguments.0.type="Literal"]',
135 '[arguments.0.type="Identifier"][arguments.0.name="undefined"]'
136 ].join(',')
137})`;
138
139const create = context => {
140 const sourceCode = context.getSourceCode();
141 const rules = {};
142
143 for (const [method, options] of iteratorMethods) {
144 const selector = [
145 methodSelector({
146 name: method,
147 min: 1,
148 max: 2
149 }),
150 options.extraSelector,
151 ignoredCalleeSelector,
152 ignoredFirstArgumentSelector
153 ].join('');
154 rules[selector] = node => {
155 const [iterator] = node.arguments;
156 check(context, iterator, method, options, sourceCode);
157 };
158 }
159
160 return rules;
161};
162
163module.exports = {
164 create,
165 meta: {
166 type: 'problem',
167 docs: {
168 url: getDocumentationUrl(__filename)
169 },
170 messages: {
171 [ERROR_WITH_NAME_MESSAGE_ID]: 'Do not pass function `{{name}}` directly to `.{{method}}(…)`.',
172 [ERROR_WITHOUT_NAME_MESSAGE_ID]: 'Do not pass function directly to `.{{method}}(…)`.',
173 [REPLACE_WITH_NAME_MESSAGE_ID]: 'Replace function `{{name}}` with `… => {{name}}({{parameters}})`.',
174 [REPLACE_WITHOUT_NAME_MESSAGE_ID]: 'Replace function with `… => …({{parameters}})`.'
175 }
176 }
177};