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