Skip to content

Commit 09676ab

Browse files
committed
[New] jsx-sort-props: add customPropsFirst to support custom props list for sorting
1 parent 4ef92b4 commit 09676ab

File tree

3 files changed

+180
-23
lines changed

3 files changed

+180
-23
lines changed

docs/rules/jsx-sort-props.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Examples of **correct** code for this rule:
3535
"ignoreCase": <boolean>,
3636
"noSortAlphabetically": <boolean>,
3737
"reservedFirst": <boolean>|<array<string>>,
38+
"customPropsFirst": <array<string>>,
3839
"locale": "auto" | "any valid locale"
3940
}]
4041
...
@@ -138,6 +139,24 @@ With `reservedFirst: ["key"]`, the following will **not** warn:
138139
<Hello key={'uuid'} name="John" ref={johnRef} />
139140
```
140141

142+
### `customPropsFirst`
143+
144+
This can only be an array option.
145+
146+
When `customPropsFirst` is defined, the specified custom props must be listed before all other props, but still respecting the alphabetical order:
147+
148+
```jsx
149+
// 'jsx-sort-props': [1, { customPropsFirst: ["className", 'theme'] }]
150+
<Hello className="flex" theme="light" name="John" />
151+
```
152+
153+
If both `reservedFirst` and `customPropsFirst` are defined, reserved props are listed first, followed by custom props, and then all other props, but still respecting the alphabetical order:
154+
155+
```jsx
156+
// 'jsx-sort-props': [1, { reservedFirst: true, customPropsFirst: ["className", 'theme'] }]
157+
<Hello key={0} ref={johnRef} className="flex" theme="light" name="John" />
158+
```
159+
141160
### `locale`
142161

143162
Defaults to `"auto"`, meaning, the locale of the current environment.

lib/rules/jsx-sort-props.js

Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ function isMultilineProp(node) {
2828

2929
const messages = {
3030
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',
31+
reservedListIsEmpty: 'A customized reserved first list must not be empty',
32+
customPropsListIsEmpty: 'Custom props first list must not be empty',
3233
listReservedPropsFirst: 'Reserved props must be listed before all other props',
34+
listCustomPropsFirst: 'Custom props must be listed before all other props',
3335
listCallbacksLast: 'Callbacks must be listed after all other props',
3436
listShorthandFirst: 'Shorthand props must be listed before all other props',
3537
listShorthandLast: 'Shorthand props must be listed after all other props',
@@ -45,7 +47,7 @@ const RESERVED_PROPS_LIST = [
4547
'ref',
4648
];
4749

48-
function isReservedPropName(name, list) {
50+
function isPropNameInList(name, list) {
4951
return list.indexOf(name) >= 0;
5052
}
5153

@@ -71,8 +73,8 @@ function contextCompare(a, b, options) {
7173
}
7274

7375
if (options.reservedFirst) {
74-
const aIsReserved = isReservedPropName(aProp, options.reservedList);
75-
const bIsReserved = isReservedPropName(bProp, options.reservedList);
76+
const aIsReserved = isPropNameInList(aProp, options.reservedList);
77+
const bIsReserved = isPropNameInList(bProp, options.reservedList);
7678
if (aIsReserved && !bIsReserved) {
7779
return -1;
7880
}
@@ -81,6 +83,17 @@ function contextCompare(a, b, options) {
8183
}
8284
}
8385

86+
if (options.customPropsList) {
87+
const aIsCustom = isPropNameInList(aProp, options.customPropsList);
88+
const bIsCustom = isPropNameInList(bProp, options.customPropsList);
89+
if (aIsCustom && !bIsCustom) {
90+
return -1;
91+
}
92+
if (!aIsCustom && bIsCustom) {
93+
return 1;
94+
}
95+
}
96+
8497
if (options.callbacksLast) {
8598
const aIsCallback = propTypesSortUtil.isCallbackPropName(aProp);
8699
const bIsCallback = propTypesSortUtil.isCallbackPropName(bProp);
@@ -212,7 +225,7 @@ function getGroupsOfSortableAttributes(attributes, context) {
212225
return sortableAttributeGroups;
213226
}
214227

215-
function generateFixerFunction(node, context, reservedList) {
228+
function generateFixerFunction(node, context, reservedList, customPropsList) {
216229
const attributes = node.attributes.slice(0);
217230
const configuration = context.options[0] || {};
218231
const ignoreCase = configuration.ignoreCase || false;
@@ -222,11 +235,9 @@ function generateFixerFunction(node, context, reservedList) {
222235
const multiline = configuration.multiline || 'ignore';
223236
const noSortAlphabetically = configuration.noSortAlphabetically || false;
224237
const reservedFirst = configuration.reservedFirst || false;
238+
const customPropsFirst = configuration.customPropsFirst || false;
225239
const locale = configuration.locale || 'auto';
226240

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.
230241
const options = {
231242
ignoreCase,
232243
callbacksLast,
@@ -236,8 +247,11 @@ function generateFixerFunction(node, context, reservedList) {
236247
noSortAlphabetically,
237248
reservedFirst,
238249
reservedList,
250+
customPropsFirst,
251+
customPropsList,
239252
locale,
240253
};
254+
241255
const sortableAttributeGroups = getGroupsOfSortableAttributes(attributes, context);
242256
const sortedAttributeGroups = sortableAttributeGroups
243257
.slice(0)
@@ -284,14 +298,14 @@ function validateReservedFirstConfig(context, reservedFirst) {
284298
if (reservedFirst) {
285299
if (Array.isArray(reservedFirst)) {
286300
// Only allow a subset of reserved words in customized lists
287-
const nonReservedWords = reservedFirst.filter((word) => !isReservedPropName(
301+
const nonReservedWords = reservedFirst.filter((word) => !isPropNameInList(
288302
word,
289303
RESERVED_PROPS_LIST
290304
));
291305

292306
if (reservedFirst.length === 0) {
293307
return function Report(decl) {
294-
report(context, messages.listIsEmpty, 'listIsEmpty', {
308+
report(context, messages.reservedListIsEmpty, 'reservedListIsEmpty', {
295309
node: decl,
296310
});
297311
};
@@ -310,6 +324,27 @@ function validateReservedFirstConfig(context, reservedFirst) {
310324
}
311325
}
312326

327+
/**
328+
* Checks if the `customPropsFirst` option is valid
329+
* @param {Object} context The context of the rule
330+
* @param {boolean | string[]} customPropsFirst The `customPropsFirst` option
331+
* @return {Function | undefined} If an error is detected, a function to generate the error message, otherwise, `undefined`
332+
*/
333+
// eslint-disable-next-line consistent-return
334+
function validateCustomPropsFirstConfig(context, customPropsFirst) {
335+
if (customPropsFirst) {
336+
if (Array.isArray(customPropsFirst)) {
337+
if (customPropsFirst.length === 0) {
338+
return function Report(decl) {
339+
report(context, messages.customPropsListIsEmpty, 'customPropsListIsEmpty', {
340+
node: decl,
341+
});
342+
};
343+
}
344+
}
345+
}
346+
}
347+
313348
const reportedNodeAttributes = new WeakMap();
314349
/**
315350
* Check if the current node attribute has already been reported with the same error type
@@ -320,8 +355,9 @@ const reportedNodeAttributes = new WeakMap();
320355
* @param {Object} node The parent node for the node attribute
321356
* @param {Object} context The context of the rule
322357
* @param {Array<String>} reservedList The list of reserved props
358+
* @param {Array<String>} customPropsList The list of custom props
323359
*/
324-
function reportNodeAttribute(nodeAttribute, errorType, node, context, reservedList) {
360+
function reportNodeAttribute(nodeAttribute, errorType, node, context, reservedList, customPropsList) {
325361
const errors = reportedNodeAttributes.get(nodeAttribute) || [];
326362

327363
if (includes(errors, errorType)) {
@@ -334,7 +370,7 @@ function reportNodeAttribute(nodeAttribute, errorType, node, context, reservedLi
334370

335371
report(context, messages[errorType], errorType, {
336372
node: nodeAttribute.name,
337-
fix: generateFixerFunction(node, context, reservedList),
373+
fix: generateFixerFunction(node, context, reservedList, customPropsList),
338374
});
339375
}
340376

@@ -382,6 +418,9 @@ module.exports = {
382418
reservedFirst: {
383419
type: ['array', 'boolean'],
384420
},
421+
customPropsFirst: {
422+
type: ['array', 'boolean'],
423+
},
385424
locale: {
386425
type: 'string',
387426
default: 'auto',
@@ -402,6 +441,9 @@ module.exports = {
402441
const reservedFirst = configuration.reservedFirst || false;
403442
const reservedFirstError = validateReservedFirstConfig(context, reservedFirst);
404443
const reservedList = Array.isArray(reservedFirst) ? reservedFirst : RESERVED_PROPS_LIST;
444+
const customPropsFirst = configuration.customPropsFirst || false;
445+
const customPropsFirstError = validateCustomPropsFirstConfig(context, customPropsFirst);
446+
const customPropsList = Array.isArray(customPropsFirst) ? customPropsFirst : [];
405447
const locale = configuration.locale || 'auto';
406448

407449
return {
@@ -436,14 +478,33 @@ module.exports = {
436478
return memo;
437479
}
438480

439-
const previousIsReserved = isReservedPropName(previousPropName, nodeReservedList);
440-
const currentIsReserved = isReservedPropName(currentPropName, nodeReservedList);
481+
const previousIsReserved = isPropNameInList(previousPropName, nodeReservedList);
482+
const currentIsReserved = isPropNameInList(currentPropName, nodeReservedList);
441483

442484
if (previousIsReserved && !currentIsReserved) {
443485
return decl;
444486
}
445487
if (!previousIsReserved && currentIsReserved) {
446-
reportNodeAttribute(decl, 'listReservedPropsFirst', node, context, nodeReservedList);
488+
reportNodeAttribute(decl, 'listReservedPropsFirst', node, context, nodeReservedList, customPropsList);
489+
490+
return memo;
491+
}
492+
}
493+
494+
if (customPropsFirst) {
495+
if (customPropsFirstError) {
496+
customPropsFirstError(decl);
497+
return memo;
498+
}
499+
500+
const previousIsCustom = isPropNameInList(propName(memo), customPropsList);
501+
const currentIsCustom = isPropNameInList(propName(decl), customPropsList);
502+
503+
if (previousIsCustom && !currentIsCustom) {
504+
return decl;
505+
}
506+
if (!previousIsCustom && currentIsCustom) {
507+
reportNodeAttribute(decl, 'listCustomPropsFirst', node, context, nodeReservedList, customPropsList);
447508

448509
return memo;
449510
}
@@ -456,7 +517,7 @@ module.exports = {
456517
}
457518
if (previousIsCallback && !currentIsCallback) {
458519
// Encountered a non-callback prop after a callback prop
459-
reportNodeAttribute(memo, 'listCallbacksLast', node, context, nodeReservedList);
520+
reportNodeAttribute(memo, 'listCallbacksLast', node, context, nodeReservedList, customPropsList);
460521

461522
return memo;
462523
}
@@ -467,7 +528,7 @@ module.exports = {
467528
return decl;
468529
}
469530
if (!currentValue && previousValue) {
470-
reportNodeAttribute(decl, 'listShorthandFirst', node, context, nodeReservedList);
531+
reportNodeAttribute(decl, 'listShorthandFirst', node, context, nodeReservedList, customPropsList);
471532

472533
return memo;
473534
}
@@ -478,7 +539,7 @@ module.exports = {
478539
return decl;
479540
}
480541
if (currentValue && !previousValue) {
481-
reportNodeAttribute(memo, 'listShorthandLast', node, context, nodeReservedList);
542+
reportNodeAttribute(memo, 'listShorthandLast', node, context, nodeReservedList, customPropsList);
482543

483544
return memo;
484545
}
@@ -493,7 +554,7 @@ module.exports = {
493554
}
494555
if (!previousIsMultiline && currentIsMultiline) {
495556
// Encountered a non-multiline prop before a multiline prop
496-
reportNodeAttribute(decl, 'listMultilineFirst', node, context, nodeReservedList);
557+
reportNodeAttribute(decl, 'listMultilineFirst', node, context, nodeReservedList, customPropsList);
497558

498559
return memo;
499560
}
@@ -504,7 +565,7 @@ module.exports = {
504565
}
505566
if (previousIsMultiline && !currentIsMultiline) {
506567
// Encountered a non-multiline prop after a multiline prop
507-
reportNodeAttribute(memo, 'listMultilineLast', node, context, nodeReservedList);
568+
reportNodeAttribute(memo, 'listMultilineLast', node, context, nodeReservedList, customPropsList);
508569

509570
return memo;
510571
}
@@ -518,7 +579,7 @@ module.exports = {
518579
: previousPropName > currentPropName
519580
)
520581
) {
521-
reportNodeAttribute(decl, 'sortPropsByAlpha', node, context, nodeReservedList);
582+
reportNodeAttribute(decl, 'sortPropsByAlpha', node, context, nodeReservedList, customPropsList);
522583

523584
return memo;
524585
}

0 commit comments

Comments
 (0)