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 | const {notFunctionSelector} = require('./utils/not-function');
|
6 |
|
7 | const ERROR_WITH_NAME_MESSAGE_ID = 'error-with-name';
|
8 | const ERROR_WITHOUT_NAME_MESSAGE_ID = 'error-without-name';
|
9 | const REPLACE_WITH_NAME_MESSAGE_ID = 'replace-with-name';
|
10 | const REPLACE_WITHOUT_NAME_MESSAGE_ID = 'replace-without-name';
|
11 |
|
12 | const 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 |
|
60 | const ignoredCallee = [
|
61 | 'Promise',
|
62 | 'React.children',
|
63 | 'Children',
|
64 | 'lodash',
|
65 | 'underscore',
|
66 | '_',
|
67 | 'Async',
|
68 | 'async'
|
69 | ];
|
70 |
|
71 | const toSelector = name => {
|
72 | const splitted = name.split('.');
|
73 | return `[callee.${'object.'.repeat(splitted.length)}name!="${splitted.shift()}"]`;
|
74 | };
|
75 |
|
76 |
|
77 | const ignoredCalleeSelector = [
|
78 |
|
79 | '[callee.object.type!="ThisExpression"]',
|
80 | ...ignoredCallee.map(name => toSelector(name))
|
81 | ].join('');
|
82 |
|
83 | function 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 |
|
132 | const ignoredFirstArgumentSelector = [
|
133 | notFunctionSelector('arguments.0'),
|
134 | '[arguments.0.type!="FunctionExpression"]',
|
135 | '[arguments.0.type!="ArrowFunctionExpression"]'
|
136 | ].join('');
|
137 |
|
138 | const 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 |
|
162 | module.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 | };
|