source: imaps-frontend/node_modules/eslint-plugin-react/lib/rules/sort-comp.js

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

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