1 | 'use strict';
|
2 | const {isParenthesized} = require('eslint-utils');
|
3 | const getDocumentationUrl = require('./utils/get-documentation-url');
|
4 | const methodSelector = require('./utils/method-selector');
|
5 |
|
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 | [
|
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 |
|
59 | const ignoredCallee = [
|
60 | 'Promise',
|
61 | 'React.children',
|
62 | 'lodash',
|
63 | 'underscore',
|
64 | '_',
|
65 | 'Async',
|
66 | 'async'
|
67 | ];
|
68 |
|
69 | const toSelector = name => {
|
70 | const splitted = name.split('.');
|
71 | return `[callee.${'object.'.repeat(splitted.length)}name!="${splitted.shift()}"]`;
|
72 | };
|
73 |
|
74 |
|
75 | const ignoredCalleeSelector = [
|
76 |
|
77 | '[callee.object.type!="ThisExpression"]',
|
78 | ...ignoredCallee.map(name => toSelector(name))
|
79 | ].join('');
|
80 |
|
81 | function 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 |
|
130 | const 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 |
|
139 | const 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 |
|
163 | module.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 | };
|