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

main
Last change on this file was 0c6b92a, checked in by stefan toskovski <stefantoska84@…>, 7 months ago

Pred finalna verzija

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