source: imaps-frontend/node_modules/eslint-plugin-react/lib/rules/jsx-sort-props.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: 17.8 KB
Line 
1/**
2 * @fileoverview Enforce props alphabetical sorting
3 * @author Ilya Volodin, Yannick Croissant
4 */
5
6'use strict';
7
8const propName = require('jsx-ast-utils/propName');
9const includes = require('array-includes');
10const toSorted = require('array.prototype.tosorted');
11
12const docsUrl = require('../util/docsUrl');
13const jsxUtil = require('../util/jsx');
14const report = require('../util/report');
15const eslintUtil = require('../util/eslint');
16
17const getText = eslintUtil.getText;
18const getSourceCode = eslintUtil.getSourceCode;
19
20// ------------------------------------------------------------------------------
21// Rule Definition
22// ------------------------------------------------------------------------------
23
24function isCallbackPropName(name) {
25 return /^on[A-Z]/.test(name);
26}
27
28function isMultilineProp(node) {
29 return node.loc.start.line !== node.loc.end.line;
30}
31
32const messages = {
33 noUnreservedProps: 'A customized reserved first list must only contain a subset of React reserved props. Remove: {{unreservedWords}}',
34 listIsEmpty: 'A customized reserved first list must not be empty',
35 listReservedPropsFirst: 'Reserved props must be listed before all other props',
36 listCallbacksLast: 'Callbacks must be listed after all other props',
37 listShorthandFirst: 'Shorthand props must be listed before all other props',
38 listShorthandLast: 'Shorthand props must be listed after all other props',
39 listMultilineFirst: 'Multiline props must be listed before all other props',
40 listMultilineLast: 'Multiline props must be listed after all other props',
41 sortPropsByAlpha: 'Props should be sorted alphabetically',
42};
43
44const RESERVED_PROPS_LIST = [
45 'children',
46 'dangerouslySetInnerHTML',
47 'key',
48 'ref',
49];
50
51function isReservedPropName(name, list) {
52 return list.indexOf(name) >= 0;
53}
54
55let attributeMap;
56// attributeMap = { end: endrange, hasComment: true||false if comment in between nodes exists, it needs to be sorted to end }
57
58function shouldSortToEnd(node) {
59 const attr = attributeMap.get(node);
60 return !!attr && !!attr.hasComment;
61}
62
63function contextCompare(a, b, options) {
64 let aProp = propName(a);
65 let bProp = propName(b);
66
67 const aSortToEnd = shouldSortToEnd(a);
68 const bSortToEnd = shouldSortToEnd(b);
69 if (aSortToEnd && !bSortToEnd) {
70 return 1;
71 }
72 if (!aSortToEnd && bSortToEnd) {
73 return -1;
74 }
75
76 if (options.reservedFirst) {
77 const aIsReserved = isReservedPropName(aProp, options.reservedList);
78 const bIsReserved = isReservedPropName(bProp, options.reservedList);
79 if (aIsReserved && !bIsReserved) {
80 return -1;
81 }
82 if (!aIsReserved && bIsReserved) {
83 return 1;
84 }
85 }
86
87 if (options.callbacksLast) {
88 const aIsCallback = isCallbackPropName(aProp);
89 const bIsCallback = isCallbackPropName(bProp);
90 if (aIsCallback && !bIsCallback) {
91 return 1;
92 }
93 if (!aIsCallback && bIsCallback) {
94 return -1;
95 }
96 }
97
98 if (options.shorthandFirst || options.shorthandLast) {
99 const shorthandSign = options.shorthandFirst ? -1 : 1;
100 if (!a.value && b.value) {
101 return shorthandSign;
102 }
103 if (a.value && !b.value) {
104 return -shorthandSign;
105 }
106 }
107
108 if (options.multiline !== 'ignore') {
109 const multilineSign = options.multiline === 'first' ? -1 : 1;
110 const aIsMultiline = isMultilineProp(a);
111 const bIsMultiline = isMultilineProp(b);
112 if (aIsMultiline && !bIsMultiline) {
113 return multilineSign;
114 }
115 if (!aIsMultiline && bIsMultiline) {
116 return -multilineSign;
117 }
118 }
119
120 if (options.noSortAlphabetically) {
121 return 0;
122 }
123
124 const actualLocale = options.locale === 'auto' ? undefined : options.locale;
125
126 if (options.ignoreCase) {
127 aProp = aProp.toLowerCase();
128 bProp = bProp.toLowerCase();
129 return aProp.localeCompare(bProp, actualLocale);
130 }
131 if (aProp === bProp) {
132 return 0;
133 }
134 if (options.locale === 'auto') {
135 return aProp < bProp ? -1 : 1;
136 }
137 return aProp.localeCompare(bProp, actualLocale);
138}
139
140/**
141 * Create an array of arrays where each subarray is composed of attributes
142 * that are considered sortable.
143 * @param {Array<JSXSpreadAttribute|JSXAttribute>} attributes
144 * @param {Object} context The context of the rule
145 * @return {Array<Array<JSXAttribute>>}
146 */
147function getGroupsOfSortableAttributes(attributes, context) {
148 const sourceCode = getSourceCode(context);
149
150 const sortableAttributeGroups = [];
151 let groupCount = 0;
152 function addtoSortableAttributeGroups(attribute) {
153 sortableAttributeGroups[groupCount - 1].push(attribute);
154 }
155
156 for (let i = 0; i < attributes.length; i++) {
157 const attribute = attributes[i];
158 const nextAttribute = attributes[i + 1];
159 const attributeline = attribute.loc.start.line;
160 let comment = [];
161 try {
162 comment = sourceCode.getCommentsAfter(attribute);
163 } catch (e) { /**/ }
164 const lastAttr = attributes[i - 1];
165 const attrIsSpread = attribute.type === 'JSXSpreadAttribute';
166
167 // If we have no groups or if the last attribute was JSXSpreadAttribute
168 // then we start a new group. Append attributes to the group until we
169 // come across another JSXSpreadAttribute or exhaust the array.
170 if (
171 !lastAttr
172 || (lastAttr.type === 'JSXSpreadAttribute' && !attrIsSpread)
173 ) {
174 groupCount += 1;
175 sortableAttributeGroups[groupCount - 1] = [];
176 }
177 if (!attrIsSpread) {
178 if (comment.length === 0) {
179 attributeMap.set(attribute, { end: attribute.range[1], hasComment: false });
180 addtoSortableAttributeGroups(attribute);
181 } else {
182 const firstComment = comment[0];
183 const commentline = firstComment.loc.start.line;
184 if (comment.length === 1) {
185 if (attributeline + 1 === commentline && nextAttribute) {
186 attributeMap.set(attribute, { end: nextAttribute.range[1], hasComment: true });
187 addtoSortableAttributeGroups(attribute);
188 i += 1;
189 } else if (attributeline === commentline) {
190 if (firstComment.type === 'Block' && nextAttribute) {
191 attributeMap.set(attribute, { end: nextAttribute.range[1], hasComment: true });
192 i += 1;
193 } else if (firstComment.type === 'Block') {
194 attributeMap.set(attribute, { end: firstComment.range[1], hasComment: true });
195 } else {
196 attributeMap.set(attribute, { end: firstComment.range[1], hasComment: false });
197 }
198 addtoSortableAttributeGroups(attribute);
199 }
200 } else if (comment.length > 1 && attributeline + 1 === comment[1].loc.start.line && nextAttribute) {
201 const commentNextAttribute = sourceCode.getCommentsAfter(nextAttribute);
202 attributeMap.set(attribute, { end: nextAttribute.range[1], hasComment: true });
203 if (
204 commentNextAttribute.length === 1
205 && nextAttribute.loc.start.line === commentNextAttribute[0].loc.start.line
206 ) {
207 attributeMap.set(attribute, { end: commentNextAttribute[0].range[1], hasComment: true });
208 }
209 addtoSortableAttributeGroups(attribute);
210 i += 1;
211 }
212 }
213 }
214 }
215 return sortableAttributeGroups;
216}
217
218function generateFixerFunction(node, context, reservedList) {
219 const attributes = node.attributes.slice(0);
220 const configuration = context.options[0] || {};
221 const ignoreCase = configuration.ignoreCase || false;
222 const callbacksLast = configuration.callbacksLast || false;
223 const shorthandFirst = configuration.shorthandFirst || false;
224 const shorthandLast = configuration.shorthandLast || false;
225 const multiline = configuration.multiline || 'ignore';
226 const noSortAlphabetically = configuration.noSortAlphabetically || false;
227 const reservedFirst = configuration.reservedFirst || false;
228 const locale = configuration.locale || 'auto';
229
230 // Sort props according to the context. Only supports ignoreCase.
231 // Since we cannot safely move JSXSpreadAttribute (due to potential variable overrides),
232 // we only consider groups of sortable attributes.
233 const options = {
234 ignoreCase,
235 callbacksLast,
236 shorthandFirst,
237 shorthandLast,
238 multiline,
239 noSortAlphabetically,
240 reservedFirst,
241 reservedList,
242 locale,
243 };
244 const sortableAttributeGroups = getGroupsOfSortableAttributes(attributes, context);
245 const sortedAttributeGroups = sortableAttributeGroups
246 .slice(0)
247 .map((group) => toSorted(group, (a, b) => contextCompare(a, b, options)));
248
249 return function fixFunction(fixer) {
250 const fixers = [];
251 let source = getText(context);
252
253 sortableAttributeGroups.forEach((sortableGroup, ii) => {
254 sortableGroup.forEach((attr, jj) => {
255 const sortedAttr = sortedAttributeGroups[ii][jj];
256 const sortedAttrText = source.slice(sortedAttr.range[0], attributeMap.get(sortedAttr).end);
257 fixers.push({
258 range: [attr.range[0], attributeMap.get(attr).end],
259 text: sortedAttrText,
260 });
261 });
262 });
263
264 fixers.sort((a, b) => b.range[0] - a.range[0]);
265
266 const firstFixer = fixers[0];
267 const lastFixer = fixers[fixers.length - 1];
268 const rangeStart = lastFixer ? lastFixer.range[0] : 0;
269 const rangeEnd = firstFixer ? firstFixer.range[1] : -0;
270
271 fixers.forEach((fix) => {
272 source = `${source.slice(0, fix.range[0])}${fix.text}${source.slice(fix.range[1])}`;
273 });
274
275 return fixer.replaceTextRange([rangeStart, rangeEnd], source.slice(rangeStart, rangeEnd));
276 };
277}
278
279/**
280 * Checks if the `reservedFirst` option is valid
281 * @param {Object} context The context of the rule
282 * @param {Boolean|Array<String>} reservedFirst The `reservedFirst` option
283 * @return {Function|undefined} If an error is detected, a function to generate the error message, otherwise, `undefined`
284 */
285// eslint-disable-next-line consistent-return
286function validateReservedFirstConfig(context, reservedFirst) {
287 if (reservedFirst) {
288 if (Array.isArray(reservedFirst)) {
289 // Only allow a subset of reserved words in customized lists
290 const nonReservedWords = reservedFirst.filter((word) => !isReservedPropName(
291 word,
292 RESERVED_PROPS_LIST
293 ));
294
295 if (reservedFirst.length === 0) {
296 return function Report(decl) {
297 report(context, messages.listIsEmpty, 'listIsEmpty', {
298 node: decl,
299 });
300 };
301 }
302 if (nonReservedWords.length > 0) {
303 return function Report(decl) {
304 report(context, messages.noUnreservedProps, 'noUnreservedProps', {
305 node: decl,
306 data: {
307 unreservedWords: nonReservedWords.toString(),
308 },
309 });
310 };
311 }
312 }
313 }
314}
315
316const reportedNodeAttributes = new WeakMap();
317/**
318 * Check if the current node attribute has already been reported with the same error type
319 * if that's the case then we don't report a new error
320 * otherwise we report the error
321 * @param {Object} nodeAttribute The node attribute to be reported
322 * @param {string} errorType The error type to be reported
323 * @param {Object} node The parent node for the node attribute
324 * @param {Object} context The context of the rule
325 * @param {Array<String>} reservedList The list of reserved props
326 */
327function reportNodeAttribute(nodeAttribute, errorType, node, context, reservedList) {
328 const errors = reportedNodeAttributes.get(nodeAttribute) || [];
329
330 if (includes(errors, errorType)) {
331 return;
332 }
333
334 errors.push(errorType);
335
336 reportedNodeAttributes.set(nodeAttribute, errors);
337
338 report(context, messages[errorType], errorType, {
339 node: nodeAttribute.name,
340 fix: generateFixerFunction(node, context, reservedList),
341 });
342}
343
344/** @type {import('eslint').Rule.RuleModule} */
345module.exports = {
346 meta: {
347 docs: {
348 description: 'Enforce props alphabetical sorting',
349 category: 'Stylistic Issues',
350 recommended: false,
351 url: docsUrl('jsx-sort-props'),
352 },
353 fixable: 'code',
354
355 messages,
356
357 schema: [{
358 type: 'object',
359 properties: {
360 // Whether callbacks (prefixed with "on") should be listed at the very end,
361 // after all other props. Supersedes shorthandLast.
362 callbacksLast: {
363 type: 'boolean',
364 },
365 // Whether shorthand properties (without a value) should be listed first
366 shorthandFirst: {
367 type: 'boolean',
368 },
369 // Whether shorthand properties (without a value) should be listed last
370 shorthandLast: {
371 type: 'boolean',
372 },
373 // Whether multiline properties should be listed first or last
374 multiline: {
375 enum: ['ignore', 'first', 'last'],
376 default: 'ignore',
377 },
378 ignoreCase: {
379 type: 'boolean',
380 },
381 // Whether alphabetical sorting should be enforced
382 noSortAlphabetically: {
383 type: 'boolean',
384 },
385 reservedFirst: {
386 type: ['array', 'boolean'],
387 },
388 locale: {
389 type: 'string',
390 default: 'auto',
391 },
392 },
393 additionalProperties: false,
394 }],
395 },
396
397 create(context) {
398 const configuration = context.options[0] || {};
399 const ignoreCase = configuration.ignoreCase || false;
400 const callbacksLast = configuration.callbacksLast || false;
401 const shorthandFirst = configuration.shorthandFirst || false;
402 const shorthandLast = configuration.shorthandLast || false;
403 const multiline = configuration.multiline || 'ignore';
404 const noSortAlphabetically = configuration.noSortAlphabetically || false;
405 const reservedFirst = configuration.reservedFirst || false;
406 const reservedFirstError = validateReservedFirstConfig(context, reservedFirst);
407 const reservedList = Array.isArray(reservedFirst) ? reservedFirst : RESERVED_PROPS_LIST;
408 const locale = configuration.locale || 'auto';
409
410 return {
411 Program() {
412 attributeMap = new WeakMap();
413 },
414
415 JSXOpeningElement(node) {
416 // `dangerouslySetInnerHTML` is only "reserved" on DOM components
417 const nodeReservedList = reservedFirst && !jsxUtil.isDOMComponent(node) ? reservedList.filter((prop) => prop !== 'dangerouslySetInnerHTML') : reservedList;
418
419 node.attributes.reduce((memo, decl, idx, attrs) => {
420 if (decl.type === 'JSXSpreadAttribute') {
421 return attrs[idx + 1];
422 }
423
424 let previousPropName = propName(memo);
425 let currentPropName = propName(decl);
426 const previousValue = memo.value;
427 const currentValue = decl.value;
428 const previousIsCallback = isCallbackPropName(previousPropName);
429 const currentIsCallback = isCallbackPropName(currentPropName);
430
431 if (ignoreCase) {
432 previousPropName = previousPropName.toLowerCase();
433 currentPropName = currentPropName.toLowerCase();
434 }
435
436 if (reservedFirst) {
437 if (reservedFirstError) {
438 reservedFirstError(decl);
439 return memo;
440 }
441
442 const previousIsReserved = isReservedPropName(previousPropName, nodeReservedList);
443 const currentIsReserved = isReservedPropName(currentPropName, nodeReservedList);
444
445 if (previousIsReserved && !currentIsReserved) {
446 return decl;
447 }
448 if (!previousIsReserved && currentIsReserved) {
449 reportNodeAttribute(decl, 'listReservedPropsFirst', node, context, nodeReservedList);
450
451 return memo;
452 }
453 }
454
455 if (callbacksLast) {
456 if (!previousIsCallback && currentIsCallback) {
457 // Entering the callback prop section
458 return decl;
459 }
460 if (previousIsCallback && !currentIsCallback) {
461 // Encountered a non-callback prop after a callback prop
462 reportNodeAttribute(memo, 'listCallbacksLast', node, context, nodeReservedList);
463
464 return memo;
465 }
466 }
467
468 if (shorthandFirst) {
469 if (currentValue && !previousValue) {
470 return decl;
471 }
472 if (!currentValue && previousValue) {
473 reportNodeAttribute(decl, 'listShorthandFirst', node, context, nodeReservedList);
474
475 return memo;
476 }
477 }
478
479 if (shorthandLast) {
480 if (!currentValue && previousValue) {
481 return decl;
482 }
483 if (currentValue && !previousValue) {
484 reportNodeAttribute(memo, 'listShorthandLast', node, context, nodeReservedList);
485
486 return memo;
487 }
488 }
489
490 const previousIsMultiline = isMultilineProp(memo);
491 const currentIsMultiline = isMultilineProp(decl);
492 if (multiline === 'first') {
493 if (previousIsMultiline && !currentIsMultiline) {
494 // Exiting the multiline prop section
495 return decl;
496 }
497 if (!previousIsMultiline && currentIsMultiline) {
498 // Encountered a non-multiline prop before a multiline prop
499 reportNodeAttribute(decl, 'listMultilineFirst', node, context, nodeReservedList);
500
501 return memo;
502 }
503 } else if (multiline === 'last') {
504 if (!previousIsMultiline && currentIsMultiline) {
505 // Entering the multiline prop section
506 return decl;
507 }
508 if (previousIsMultiline && !currentIsMultiline) {
509 // Encountered a non-multiline prop after a multiline prop
510 reportNodeAttribute(memo, 'listMultilineLast', node, context, nodeReservedList);
511
512 return memo;
513 }
514 }
515
516 if (
517 !noSortAlphabetically
518 && (
519 (ignoreCase || locale !== 'auto')
520 ? previousPropName.localeCompare(currentPropName, locale === 'auto' ? undefined : locale) > 0
521 : previousPropName > currentPropName
522 )
523 ) {
524 reportNodeAttribute(decl, 'sortPropsByAlpha', node, context, nodeReservedList);
525
526 return memo;
527 }
528
529 return decl;
530 }, node.attributes[0]);
531 },
532 };
533 },
534};
Note: See TracBrowser for help on using the repository browser.