UNPKG

13.3 kBJavaScriptView Raw
1/**
2 * @fileoverview Enforce component methods order
3 * @author Yannick Croissant
4 */
5
6'use strict';
7
8const has = require('has');
9const entries = require('object.entries');
10const arrayIncludes = require('array-includes');
11
12const Components = require('../util/Components');
13const astUtil = require('../util/ast');
14const docsUrl = require('../util/docsUrl');
15
16const defaultConfig = {
17 order: [
18 'static-methods',
19 'lifecycle',
20 'everything-else',
21 'render'
22 ],
23 groups: {
24 lifecycle: [
25 'displayName',
26 'propTypes',
27 'contextTypes',
28 'childContextTypes',
29 'mixins',
30 'statics',
31 'defaultProps',
32 'constructor',
33 'getDefaultProps',
34 'state',
35 'getInitialState',
36 'getChildContext',
37 'getDerivedStateFromProps',
38 'componentWillMount',
39 'UNSAFE_componentWillMount',
40 'componentDidMount',
41 'componentWillReceiveProps',
42 'UNSAFE_componentWillReceiveProps',
43 'shouldComponentUpdate',
44 'componentWillUpdate',
45 'UNSAFE_componentWillUpdate',
46 'getSnapshotBeforeUpdate',
47 'componentDidUpdate',
48 'componentDidCatch',
49 'componentWillUnmount'
50 ]
51 }
52};
53
54/**
55 * Get the methods order from the default config and the user config
56 * @param {Object} userConfig The user configuration.
57 * @returns {Array} Methods order
58 */
59function getMethodsOrder(userConfig) {
60 userConfig = userConfig || {};
61
62 const groups = Object.assign({}, defaultConfig.groups, userConfig.groups);
63 const order = userConfig.order || defaultConfig.order;
64
65 let config = [];
66 let entry;
67 for (let i = 0, j = order.length; i < j; i++) {
68 entry = order[i];
69 if (has(groups, entry)) {
70 config = config.concat(groups[entry]);
71 } else {
72 config.push(entry);
73 }
74 }
75
76 return config;
77}
78
79// ------------------------------------------------------------------------------
80// Rule Definition
81// ------------------------------------------------------------------------------
82
83module.exports = {
84 meta: {
85 docs: {
86 description: 'Enforce component methods order',
87 category: 'Stylistic Issues',
88 recommended: false,
89 url: docsUrl('sort-comp')
90 },
91
92 schema: [{
93 type: 'object',
94 properties: {
95 order: {
96 type: 'array',
97 items: {
98 type: 'string'
99 }
100 },
101 groups: {
102 type: 'object',
103 patternProperties: {
104 '^.*$': {
105 type: 'array',
106 items: {
107 type: 'string'
108 }
109 }
110 }
111 }
112 },
113 additionalProperties: false
114 }]
115 },
116
117 create: Components.detect((context, components) => {
118 const errors = {};
119
120 const MISPOSITION_MESSAGE = '{{propA}} should be placed {{position}} {{propB}}';
121
122 const methodsOrder = getMethodsOrder(context.options[0]);
123
124 // --------------------------------------------------------------------------
125 // Public
126 // --------------------------------------------------------------------------
127
128 const regExpRegExp = /\/(.*)\/([gimsuy]*)/;
129
130 /**
131 * Get indexes of the matching patterns in methods order configuration
132 * @param {Object} method - Method metadata.
133 * @returns {Array} The matching patterns indexes. Return [Infinity] if there is no match.
134 */
135 function getRefPropIndexes(method) {
136 const methodGroupIndexes = [];
137
138 methodsOrder.forEach((currentGroup, groupIndex) => {
139 if (currentGroup === 'getters') {
140 if (method.getter) {
141 methodGroupIndexes.push(groupIndex);
142 }
143 } else if (currentGroup === 'setters') {
144 if (method.setter) {
145 methodGroupIndexes.push(groupIndex);
146 }
147 } else if (currentGroup === 'type-annotations') {
148 if (method.typeAnnotation) {
149 methodGroupIndexes.push(groupIndex);
150 }
151 } else if (currentGroup === 'static-variables') {
152 if (method.staticVariable) {
153 methodGroupIndexes.push(groupIndex);
154 }
155 } else if (currentGroup === 'static-methods') {
156 if (method.staticMethod) {
157 methodGroupIndexes.push(groupIndex);
158 }
159 } else if (currentGroup === 'instance-variables') {
160 if (method.instanceVariable) {
161 methodGroupIndexes.push(groupIndex);
162 }
163 } else if (currentGroup === 'instance-methods') {
164 if (method.instanceMethod) {
165 methodGroupIndexes.push(groupIndex);
166 }
167 } else if (arrayIncludes([
168 'displayName',
169 'propTypes',
170 'contextTypes',
171 'childContextTypes',
172 'mixins',
173 'statics',
174 'defaultProps',
175 'constructor',
176 'getDefaultProps',
177 'state',
178 'getInitialState',
179 'getChildContext',
180 'getDerivedStateFromProps',
181 'componentWillMount',
182 'UNSAFE_componentWillMount',
183 'componentDidMount',
184 'componentWillReceiveProps',
185 'UNSAFE_componentWillReceiveProps',
186 'shouldComponentUpdate',
187 'componentWillUpdate',
188 'UNSAFE_componentWillUpdate',
189 'getSnapshotBeforeUpdate',
190 'componentDidUpdate',
191 'componentDidCatch',
192 'componentWillUnmount',
193 'render'
194 ], currentGroup)) {
195 if (currentGroup === method.name) {
196 methodGroupIndexes.push(groupIndex);
197 }
198 } else {
199 // Is the group a regex?
200 const isRegExp = currentGroup.match(regExpRegExp);
201 if (isRegExp) {
202 const isMatching = new RegExp(isRegExp[1], isRegExp[2]).test(method.name);
203 if (isMatching) {
204 methodGroupIndexes.push(groupIndex);
205 }
206 } else if (currentGroup === method.name) {
207 methodGroupIndexes.push(groupIndex);
208 }
209 }
210 });
211
212 // No matching pattern, return 'everything-else' index
213 if (methodGroupIndexes.length === 0) {
214 const everythingElseIndex = methodsOrder.indexOf('everything-else');
215
216 if (everythingElseIndex !== -1) {
217 methodGroupIndexes.push(everythingElseIndex);
218 } else {
219 // No matching pattern and no 'everything-else' group
220 methodGroupIndexes.push(Infinity);
221 }
222 }
223
224 return methodGroupIndexes;
225 }
226
227 /**
228 * Get properties name
229 * @param {Object} node - Property.
230 * @returns {String} Property name.
231 */
232 function getPropertyName(node) {
233 if (node.kind === 'get') {
234 return 'getter functions';
235 }
236
237 if (node.kind === 'set') {
238 return 'setter functions';
239 }
240
241 return astUtil.getPropertyName(node);
242 }
243
244 /**
245 * Store a new error in the error list
246 * @param {Object} propA - Mispositioned property.
247 * @param {Object} propB - Reference property.
248 */
249 function storeError(propA, propB) {
250 // Initialize the error object if needed
251 if (!errors[propA.index]) {
252 errors[propA.index] = {
253 node: propA.node,
254 score: 0,
255 closest: {
256 distance: Infinity,
257 ref: {
258 node: null,
259 index: 0
260 }
261 }
262 };
263 }
264 // Increment the prop score
265 errors[propA.index].score++;
266 // Stop here if we already have pushed another node at this position
267 if (getPropertyName(errors[propA.index].node) !== getPropertyName(propA.node)) {
268 return;
269 }
270 // Stop here if we already have a closer reference
271 if (Math.abs(propA.index - propB.index) > errors[propA.index].closest.distance) {
272 return;
273 }
274 // Update the closest reference
275 errors[propA.index].closest.distance = Math.abs(propA.index - propB.index);
276 errors[propA.index].closest.ref.node = propB.node;
277 errors[propA.index].closest.ref.index = propB.index;
278 }
279
280 /**
281 * Dedupe errors, only keep the ones with the highest score and delete the others
282 */
283 function dedupeErrors() {
284 for (const i in errors) {
285 if (has(errors, i)) {
286 const index = errors[i].closest.ref.index;
287 if (errors[index]) {
288 if (errors[i].score > errors[index].score) {
289 delete errors[index];
290 } else {
291 delete errors[i];
292 }
293 }
294 }
295 }
296 }
297
298 /**
299 * Report errors
300 */
301 function reportErrors() {
302 dedupeErrors();
303
304 entries(errors).forEach((entry) => {
305 const nodeA = entry[1].node;
306 const nodeB = entry[1].closest.ref.node;
307 const indexA = entry[0];
308 const indexB = entry[1].closest.ref.index;
309
310 context.report({
311 node: nodeA,
312 message: MISPOSITION_MESSAGE,
313 data: {
314 propA: getPropertyName(nodeA),
315 propB: getPropertyName(nodeB),
316 position: indexA < indexB ? 'before' : 'after'
317 }
318 });
319 });
320 }
321
322 /**
323 * Compare two properties and find out if they are in the right order
324 * @param {Array} propertiesInfos Array containing all the properties metadata.
325 * @param {Object} propA First property name and metadata
326 * @param {Object} propB Second property name.
327 * @returns {Object} Object containing a correct true/false flag and the correct indexes for the two properties.
328 */
329 function comparePropsOrder(propertiesInfos, propA, propB) {
330 let i;
331 let j;
332 let k;
333 let l;
334 let refIndexA;
335 let refIndexB;
336
337 // Get references indexes (the correct position) for given properties
338 const refIndexesA = getRefPropIndexes(propA);
339 const refIndexesB = getRefPropIndexes(propB);
340
341 // Get current indexes for given properties
342 const classIndexA = propertiesInfos.indexOf(propA);
343 const classIndexB = propertiesInfos.indexOf(propB);
344
345 // Loop around the references indexes for the 1st property
346 for (i = 0, j = refIndexesA.length; i < j; i++) {
347 refIndexA = refIndexesA[i];
348
349 // Loop around the properties for the 2nd property (for comparison)
350 for (k = 0, l = refIndexesB.length; k < l; k++) {
351 refIndexB = refIndexesB[k];
352
353 if (
354 // Comparing the same properties
355 refIndexA === refIndexB
356 // 1st property is placed before the 2nd one in reference and in current component
357 || refIndexA < refIndexB && classIndexA < classIndexB
358 // 1st property is placed after the 2nd one in reference and in current component
359 || refIndexA > refIndexB && classIndexA > classIndexB
360 ) {
361 return {
362 correct: true,
363 indexA: classIndexA,
364 indexB: classIndexB
365 };
366 }
367 }
368 }
369
370 // We did not find any correct match between reference and current component
371 return {
372 correct: false,
373 indexA: refIndexA,
374 indexB: refIndexB
375 };
376 }
377
378 /**
379 * Check properties order from a properties list and store the eventual errors
380 * @param {Array} properties Array containing all the properties.
381 */
382 function checkPropsOrder(properties) {
383 const propertiesInfos = properties.map((node) => ({
384 name: getPropertyName(node),
385 getter: node.kind === 'get',
386 setter: node.kind === 'set',
387 staticVariable: node.static
388 && node.type === 'ClassProperty'
389 && (!node.value || !astUtil.isFunctionLikeExpression(node.value)),
390 staticMethod: node.static
391 && (node.type === 'ClassProperty' || node.type === 'MethodDefinition')
392 && node.value
393 && (astUtil.isFunctionLikeExpression(node.value)),
394 instanceVariable: !node.static
395 && node.type === 'ClassProperty'
396 && (!node.value || !astUtil.isFunctionLikeExpression(node.value)),
397 instanceMethod: !node.static
398 && node.type === 'ClassProperty'
399 && node.value
400 && (astUtil.isFunctionLikeExpression(node.value)),
401 typeAnnotation: !!node.typeAnnotation && node.value === null
402 }));
403
404 // Loop around the properties
405 propertiesInfos.forEach((propA, i) => {
406 // Loop around the properties a second time (for comparison)
407 propertiesInfos.forEach((propB, k) => {
408 if (i === k) {
409 return;
410 }
411
412 // Compare the properties order
413 const order = comparePropsOrder(propertiesInfos, propA, propB);
414
415 if (!order.correct) {
416 // Store an error if the order is incorrect
417 storeError({
418 node: properties[i],
419 index: order.indexA
420 }, {
421 node: properties[k],
422 index: order.indexB
423 });
424 }
425 });
426 });
427 }
428
429 return {
430 'Program:exit'() {
431 const list = components.list();
432 Object.keys(list).forEach((component) => {
433 const properties = astUtil.getComponentProperties(list[component].node);
434 checkPropsOrder(properties);
435 });
436
437 reportErrors();
438 }
439 };
440 }),
441
442 defaultConfig
443};