Skip to content

Commit 8a9a07d

Browse files
committed
Add sortFirst option to jsx-sort-props rule
1 parent f2869fd commit 8a9a07d

File tree

3 files changed

+227
-3
lines changed

3 files changed

+227
-3
lines changed

docs/rules/jsx-sort-props.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ This rule checks all JSX components and verifies that all props are sorted alpha
1313
Examples of **incorrect** code for this rule:
1414

1515
```jsx
16-
<Hello lastName="Smith" firstName="John" />;
16+
<Hello lastName="Smith" firstName="John" />
1717
```
1818

1919
Examples of **correct** code for this rule:
@@ -35,6 +35,7 @@ Examples of **correct** code for this rule:
3535
"ignoreCase": <boolean>,
3636
"noSortAlphabetically": <boolean>,
3737
"reservedFirst": <boolean>|<array<string>>,
38+
"sortFirst": <array<string>>,
3839
"locale": "auto" | "any valid locale"
3940
}]
4041
...
@@ -47,7 +48,7 @@ When `true` the rule ignores the case-sensitivity of the props order.
4748
Examples of **correct** code for this rule
4849

4950
```jsx
50-
<Hello name="John" Number="2" />;
51+
<Hello name="John" Number="2" />
5152
```
5253

5354
### `callbacksLast`
@@ -138,6 +139,32 @@ With `reservedFirst: ["key"]`, the following will **not** warn:
138139
<Hello key={'uuid'} name="John" ref={johnRef} />
139140
```
140141

142+
### `sortFirst`
143+
144+
When `sortFirst` is defined as an array of prop names, those props must be listed before all other props, maintaining the exact order specified in the array. This option has the highest priority and takes precedence over all other sorting options (including `reservedFirst`, `shorthandFirst`, `callbacksLast`, and `multiline`).
145+
146+
The prop names in the array are matched case-sensitively by default, but respect the `ignoreCase` option when enabled.
147+
148+
Examples of **incorrect** code for this rule:
149+
150+
```jsx
151+
// 'jsx-sort-props': [1, { sortFirst: ['className'] }]
152+
<Hello name="John" className="test" />
153+
```
154+
155+
Examples of **correct** code for this rule:
156+
157+
```jsx
158+
// 'jsx-sort-props': [1, { sortFirst: ['className'] }]
159+
<Hello className="test" name="John" />
160+
161+
// 'jsx-sort-props': [1, { sortFirst: ['className', 'id'] }]
162+
<Hello className="test" id="test" name="John" />
163+
164+
// 'jsx-sort-props': [1, { sortFirst: ['className'], ignoreCase: true }]
165+
<Hello classname="test" name="John" />
166+
```
167+
141168
### `locale`
142169

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

lib/rules/jsx-sort-props.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const messages = {
3535
listShorthandLast: 'Shorthand props must be listed after all other props',
3636
listMultilineFirst: 'Multiline props must be listed before all other props',
3737
listMultilineLast: 'Multiline props must be listed after all other props',
38+
listSortFirstPropsFirst: 'Props in sortFirst must be listed before all other props',
3839
sortPropsByAlpha: 'Props should be sorted alphabetically',
3940
};
4041

@@ -49,6 +50,20 @@ function isReservedPropName(name, list) {
4950
return list.indexOf(name) >= 0;
5051
}
5152

53+
function getSortFirstIndex(name, sortFirstList, ignoreCase) {
54+
if (!sortFirstList || sortFirstList.length === 0) {
55+
return -1;
56+
}
57+
const normalizedPropName = ignoreCase ? name.toLowerCase() : name;
58+
for (let i = 0; i < sortFirstList.length; i++) {
59+
const normalizedListName = ignoreCase ? sortFirstList[i].toLowerCase() : sortFirstList[i];
60+
if (normalizedPropName === normalizedListName) {
61+
return i;
62+
}
63+
}
64+
return -1;
65+
}
66+
5267
let attributeMap;
5368
// attributeMap = { end: endrange, hasComment: true||false if comment in between nodes exists, it needs to be sorted to end }
5469

@@ -70,6 +85,24 @@ function contextCompare(a, b, options) {
7085
return -1;
7186
}
7287

88+
if (options.sortFirst && options.sortFirst.length > 0) {
89+
const aSortFirstIndex = getSortFirstIndex(aProp, options.sortFirst, options.ignoreCase);
90+
const bSortFirstIndex = getSortFirstIndex(bProp, options.sortFirst, options.ignoreCase);
91+
if (aSortFirstIndex >= 0 && bSortFirstIndex >= 0) {
92+
// Both are in sortFirst, maintain their exact order
93+
if (aSortFirstIndex !== bSortFirstIndex) {
94+
return aSortFirstIndex - bSortFirstIndex;
95+
}
96+
return 0;
97+
}
98+
if (aSortFirstIndex >= 0 && bSortFirstIndex < 0) {
99+
return -1;
100+
}
101+
if (aSortFirstIndex < 0 && bSortFirstIndex >= 0) {
102+
return 1;
103+
}
104+
}
105+
73106
if (options.reservedFirst) {
74107
const aIsReserved = isReservedPropName(aProp, options.reservedList);
75108
const bIsReserved = isReservedPropName(bProp, options.reservedList);
@@ -222,6 +255,7 @@ function generateFixerFunction(node, context, reservedList) {
222255
const multiline = configuration.multiline || 'ignore';
223256
const noSortAlphabetically = configuration.noSortAlphabetically || false;
224257
const reservedFirst = configuration.reservedFirst || false;
258+
const sortFirst = configuration.sortFirst || [];
225259
const locale = configuration.locale || 'auto';
226260

227261
// Sort props according to the context. Only supports ignoreCase.
@@ -236,6 +270,7 @@ function generateFixerFunction(node, context, reservedList) {
236270
noSortAlphabetically,
237271
reservedFirst,
238272
reservedList,
273+
sortFirst,
239274
locale,
240275
};
241276
const sortableAttributeGroups = getGroupsOfSortableAttributes(attributes, context);
@@ -382,6 +417,12 @@ module.exports = {
382417
reservedFirst: {
383418
type: ['array', 'boolean'],
384419
},
420+
sortFirst: {
421+
type: 'array',
422+
items: {
423+
type: 'string',
424+
},
425+
},
385426
locale: {
386427
type: 'string',
387428
default: 'auto',
@@ -402,6 +443,7 @@ module.exports = {
402443
const reservedFirst = configuration.reservedFirst || false;
403444
const reservedFirstError = validateReservedFirstConfig(context, reservedFirst);
404445
const reservedList = Array.isArray(reservedFirst) ? reservedFirst : RESERVED_PROPS_LIST;
446+
const sortFirst = configuration.sortFirst || [];
405447
const locale = configuration.locale || 'auto';
406448

407449
return {
@@ -425,6 +467,31 @@ module.exports = {
425467
const previousIsCallback = propTypesSortUtil.isCallbackPropName(previousPropName);
426468
const currentIsCallback = propTypesSortUtil.isCallbackPropName(currentPropName);
427469

470+
if (sortFirst && sortFirst.length > 0) {
471+
const previousSortFirstIndex = getSortFirstIndex(previousPropName, sortFirst, ignoreCase);
472+
const currentSortFirstIndex = getSortFirstIndex(currentPropName, sortFirst, ignoreCase);
473+
474+
if (previousSortFirstIndex >= 0 && currentSortFirstIndex >= 0) {
475+
// Both are in sortFirst, check their order
476+
if (previousSortFirstIndex > currentSortFirstIndex) {
477+
reportNodeAttribute(decl, 'listSortFirstPropsFirst', node, context, nodeReservedList);
478+
return memo;
479+
}
480+
return decl;
481+
}
482+
483+
if (previousSortFirstIndex >= 0 && currentSortFirstIndex < 0) {
484+
// Previous is in sortFirst, current is not - this is correct, continue to next prop
485+
return decl;
486+
}
487+
488+
if (previousSortFirstIndex < 0 && currentSortFirstIndex >= 0) {
489+
// Current is in sortFirst but previous is not - error
490+
reportNodeAttribute(decl, 'listSortFirstPropsFirst', node, context, nodeReservedList);
491+
return memo;
492+
}
493+
}
494+
428495
if (ignoreCase) {
429496
previousPropName = previousPropName.toLowerCase();
430497
currentPropName = currentPropName.toLowerCase();

tests/lib/rules/jsx-sort-props.js

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ const expectedReservedFirstError = {
5858
messageId: 'listReservedPropsFirst',
5959
type: 'JSXIdentifier',
6060
};
61+
const expectedSortFirstError = {
62+
messageId: 'listSortFirstPropsFirst',
63+
type: 'JSXIdentifier',
64+
};
6165
const expectedEmptyReservedFirstError = {
6266
messageId: 'listIsEmpty',
6367
};
@@ -120,6 +124,33 @@ const multilineAndShorthandAndCallbackLastArgs = [
120124
callbacksLast: true,
121125
},
122126
];
127+
const sortFirstArgs = [{ sortFirst: ['className'] }];
128+
const sortFirstMultipleArgs = [{ sortFirst: ['className', 'id'] }];
129+
const sortFirstWithIgnoreCaseArgs = [{ sortFirst: ['className'], ignoreCase: true }];
130+
const sortFirstWithReservedFirstArgs = [
131+
{
132+
sortFirst: ['className'],
133+
reservedFirst: true,
134+
},
135+
];
136+
const sortFirstWithShorthandFirstArgs = [
137+
{
138+
sortFirst: ['className'],
139+
shorthandFirst: true,
140+
},
141+
];
142+
const sortFirstWithCallbacksLastArgs = [
143+
{
144+
sortFirst: ['className'],
145+
callbacksLast: true,
146+
},
147+
];
148+
const sortFirstWithMultilineFirstArgs = [
149+
{
150+
sortFirst: ['className'],
151+
multiline: 'first',
152+
},
153+
];
123154

124155
ruleTester.run('jsx-sort-props', rule, {
125156
valid: parsers.all([].concat(
@@ -296,7 +327,29 @@ ruleTester.run('jsx-sort-props', rule, {
296327
/>
297328
`,
298329
options: [{ locale: 'sk-SK' }],
299-
} : []
330+
} : [],
331+
// sortFirst
332+
{ code: '<App className="test" name="John" />;', options: sortFirstArgs },
333+
{ code: '<App className="test" id="test" name="John" />;', options: sortFirstMultipleArgs },
334+
{ code: '<App className="test" id="test" />;', options: sortFirstMultipleArgs },
335+
{ code: '<App className="test" a b c />;', options: sortFirstArgs },
336+
{ code: '<App className="test" id="test" a b c />;', options: sortFirstMultipleArgs },
337+
{ code: '<App className="test" key={0} name="John" />;', options: sortFirstWithReservedFirstArgs },
338+
{ code: '<App className="test" a name="John" />;', options: sortFirstWithShorthandFirstArgs },
339+
{ code: '<App className="test" name="John" onClick={handleClick} />;', options: sortFirstWithCallbacksLastArgs },
340+
{
341+
code: `
342+
<App
343+
className="test"
344+
data={{
345+
test: 1,
346+
}}
347+
name="John"
348+
/>
349+
`,
350+
options: sortFirstWithMultilineFirstArgs,
351+
},
352+
{ code: '<App classname="test" a="test2" />;', options: sortFirstWithIgnoreCaseArgs }
300353
)),
301354
invalid: parsers.all([].concat(
302355
{
@@ -1101,6 +1154,83 @@ ruleTester.run('jsx-sort-props', rule, {
11011154
line: 11,
11021155
},
11031156
],
1157+
},
1158+
// sortFirst
1159+
{
1160+
code: '<App name="John" className="test" />;',
1161+
options: sortFirstArgs,
1162+
errors: [expectedSortFirstError],
1163+
output: '<App className="test" name="John" />;',
1164+
},
1165+
{
1166+
code: '<App id="test" className="test" name="John" />;',
1167+
options: sortFirstMultipleArgs,
1168+
errors: [expectedSortFirstError],
1169+
output: '<App className="test" id="test" name="John" />;',
1170+
},
1171+
{
1172+
code: '<App a className="test" b />;',
1173+
options: sortFirstArgs,
1174+
errors: [expectedSortFirstError],
1175+
output: '<App className="test" a b />;',
1176+
},
1177+
{
1178+
code: '<App key={0} className="test" name="John" />;',
1179+
options: sortFirstWithReservedFirstArgs,
1180+
errors: [expectedSortFirstError],
1181+
output: '<App className="test" key={0} name="John" />;',
1182+
},
1183+
{
1184+
code: '<App a className="test" name="John" />;',
1185+
options: sortFirstWithShorthandFirstArgs,
1186+
errors: [expectedSortFirstError],
1187+
output: '<App className="test" a name="John" />;',
1188+
},
1189+
{
1190+
code: '<App name="John" onClick={handleClick} className="test" />;',
1191+
options: sortFirstWithCallbacksLastArgs,
1192+
errors: [expectedSortFirstError],
1193+
output: '<App className="test" name="John" onClick={handleClick} />;',
1194+
},
1195+
{
1196+
code: `
1197+
<App
1198+
name="John"
1199+
className="test"
1200+
data={{
1201+
test: 1,
1202+
}}
1203+
/>
1204+
`,
1205+
options: sortFirstWithMultilineFirstArgs,
1206+
errors: [expectedSortFirstError, expectedMultilineFirstError],
1207+
output: `
1208+
<App
1209+
className="test"
1210+
data={{
1211+
test: 1,
1212+
}}
1213+
name="John"
1214+
/>
1215+
`,
1216+
},
1217+
{
1218+
code: '<App name="John" classname="test" />;',
1219+
options: sortFirstWithIgnoreCaseArgs,
1220+
errors: [expectedSortFirstError],
1221+
output: '<App classname="test" name="John" />;',
1222+
},
1223+
{
1224+
code: '<App className="test" id="test" tel={5555555} name="John" />;',
1225+
options: sortFirstMultipleArgs,
1226+
errors: [expectedError],
1227+
output: '<App className="test" id="test" name="John" tel={5555555} />;',
1228+
},
1229+
{
1230+
code: '<App id="test" className="test" id="test2" />;',
1231+
options: sortFirstMultipleArgs,
1232+
errors: [expectedSortFirstError],
1233+
output: '<App className="test" id="test" id="test2" />;',
11041234
}
11051235
)),
11061236
});

0 commit comments

Comments
 (0)