Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions docs/rules/jsx-sort-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This rule checks all JSX components and verifies that all props are sorted alpha
Examples of **incorrect** code for this rule:

```jsx
<Hello lastName="Smith" firstName="John" />;
<Hello lastName="Smith" firstName="John" />
```

Examples of **correct** code for this rule:
Expand All @@ -35,6 +35,7 @@ Examples of **correct** code for this rule:
"ignoreCase": <boolean>,
"noSortAlphabetically": <boolean>,
"reservedFirst": <boolean>|<array<string>>,
"sortFirst": <array<string>>,
"locale": "auto" | "any valid locale"
}]
...
Expand All @@ -47,7 +48,7 @@ When `true` the rule ignores the case-sensitivity of the props order.
Examples of **correct** code for this rule

```jsx
<Hello name="John" Number="2" />;
<Hello name="John" Number="2" />
```

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

### `sortFirst`

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`).

The prop names in the array are matched case-sensitively by default, but respect the `ignoreCase` option when enabled.

Examples of **incorrect** code for this rule:

```jsx
// 'jsx-sort-props': [1, { sortFirst: ['className'] }]
<Hello name="John" className="test" />
```

Examples of **correct** code for this rule:

```jsx
// 'jsx-sort-props': [1, { sortFirst: ['className'] }]
<Hello className="test" name="John" />

// 'jsx-sort-props': [1, { sortFirst: ['className', 'id'] }]
<Hello className="test" id="test" name="John" />

// 'jsx-sort-props': [1, { sortFirst: ['className'], ignoreCase: true }]
<Hello classname="test" name="John" />
```

### `locale`

Defaults to `"auto"`, meaning, the locale of the current environment.
Expand Down
64 changes: 64 additions & 0 deletions lib/rules/jsx-sort-props.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const messages = {
listShorthandLast: 'Shorthand props must be listed after all other props',
listMultilineFirst: 'Multiline props must be listed before all other props',
listMultilineLast: 'Multiline props must be listed after all other props',
listSortFirstPropsFirst: 'Props in sortFirst must be listed before all other props',
sortPropsByAlpha: 'Props should be sorted alphabetically',
};

Expand All @@ -49,6 +50,17 @@ function isReservedPropName(name, list) {
return list.indexOf(name) >= 0;
}

function getSortFirstIndex(name, sortFirstList, ignoreCase) {
const normalizedPropName = ignoreCase ? name.toLowerCase() : name;
for (let i = 0; i < sortFirstList.length; i++) {
const normalizedListName = ignoreCase ? sortFirstList[i].toLowerCase() : sortFirstList[i];
if (normalizedPropName === normalizedListName) {
return i;
}
}
return -1;
}

let attributeMap;
// attributeMap = { end: endrange, hasComment: true||false if comment in between nodes exists, it needs to be sorted to end }

Expand All @@ -70,6 +82,24 @@ function contextCompare(a, b, options) {
return -1;
}

if (options.sortFirst && options.sortFirst.length > 0) {
const aSortFirstIndex = getSortFirstIndex(aProp, options.sortFirst, options.ignoreCase);
const bSortFirstIndex = getSortFirstIndex(bProp, options.sortFirst, options.ignoreCase);
if (aSortFirstIndex >= 0 && bSortFirstIndex >= 0) {
// Both are in sortFirst, maintain their exact order
if (aSortFirstIndex !== bSortFirstIndex) {
return aSortFirstIndex - bSortFirstIndex;
}
return 0;
}
if (aSortFirstIndex >= 0 && bSortFirstIndex < 0) {
return -1;
}
if (aSortFirstIndex < 0 && bSortFirstIndex >= 0) {
return 1;
}
}

if (options.reservedFirst) {
const aIsReserved = isReservedPropName(aProp, options.reservedList);
const bIsReserved = isReservedPropName(bProp, options.reservedList);
Expand Down Expand Up @@ -222,6 +252,7 @@ function generateFixerFunction(node, context, reservedList) {
const multiline = configuration.multiline || 'ignore';
const noSortAlphabetically = configuration.noSortAlphabetically || false;
const reservedFirst = configuration.reservedFirst || false;
const sortFirst = configuration.sortFirst || [];
const locale = configuration.locale || 'auto';

// Sort props according to the context. Only supports ignoreCase.
Expand All @@ -236,6 +267,7 @@ function generateFixerFunction(node, context, reservedList) {
noSortAlphabetically,
reservedFirst,
reservedList,
sortFirst,
locale,
};
const sortableAttributeGroups = getGroupsOfSortableAttributes(attributes, context);
Expand Down Expand Up @@ -382,6 +414,12 @@ module.exports = {
reservedFirst: {
type: ['array', 'boolean'],
},
sortFirst: {
type: 'array',
items: {
type: 'string',
},
},
locale: {
type: 'string',
default: 'auto',
Expand All @@ -402,6 +440,7 @@ module.exports = {
const reservedFirst = configuration.reservedFirst || false;
const reservedFirstError = validateReservedFirstConfig(context, reservedFirst);
const reservedList = Array.isArray(reservedFirst) ? reservedFirst : RESERVED_PROPS_LIST;
const sortFirst = configuration.sortFirst || [];
const locale = configuration.locale || 'auto';

return {
Expand All @@ -425,6 +464,31 @@ module.exports = {
const previousIsCallback = propTypesSortUtil.isCallbackPropName(previousPropName);
const currentIsCallback = propTypesSortUtil.isCallbackPropName(currentPropName);

if (sortFirst && sortFirst.length > 0) {
const previousSortFirstIndex = getSortFirstIndex(previousPropName, sortFirst, ignoreCase);
const currentSortFirstIndex = getSortFirstIndex(currentPropName, sortFirst, ignoreCase);

if (previousSortFirstIndex >= 0 && currentSortFirstIndex >= 0) {
// Both are in sortFirst, check their order
if (previousSortFirstIndex > currentSortFirstIndex) {
reportNodeAttribute(decl, 'listSortFirstPropsFirst', node, context, nodeReservedList);
return memo;
}
return decl;
}

if (previousSortFirstIndex >= 0 && currentSortFirstIndex < 0) {
// Previous is in sortFirst, current is not - this is correct, continue to next prop
return decl;
}

if (previousSortFirstIndex < 0 && currentSortFirstIndex >= 0) {
// Current is in sortFirst but previous is not - error
reportNodeAttribute(decl, 'listSortFirstPropsFirst', node, context, nodeReservedList);
return memo;
}
}

if (ignoreCase) {
previousPropName = previousPropName.toLowerCase();
currentPropName = currentPropName.toLowerCase();
Expand Down
2 changes: 1 addition & 1 deletion test-published-types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"private": true,
"version": "0.0.0",
"dependencies": {
"eslint": "^9.11.1"
"eslint": "~9.38.0"
}
}
132 changes: 131 additions & 1 deletion tests/lib/rules/jsx-sort-props.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ const expectedReservedFirstError = {
messageId: 'listReservedPropsFirst',
type: 'JSXIdentifier',
};
const expectedSortFirstError = {
messageId: 'listSortFirstPropsFirst',
type: 'JSXIdentifier',
};
const expectedEmptyReservedFirstError = {
messageId: 'listIsEmpty',
};
Expand Down Expand Up @@ -120,6 +124,33 @@ const multilineAndShorthandAndCallbackLastArgs = [
callbacksLast: true,
},
];
const sortFirstArgs = [{ sortFirst: ['className'] }];
const sortFirstMultipleArgs = [{ sortFirst: ['className', 'id'] }];
const sortFirstWithIgnoreCaseArgs = [{ sortFirst: ['className'], ignoreCase: true }];
const sortFirstWithReservedFirstArgs = [
{
sortFirst: ['className'],
reservedFirst: true,
},
];
const sortFirstWithShorthandFirstArgs = [
{
sortFirst: ['className'],
shorthandFirst: true,
},
];
const sortFirstWithCallbacksLastArgs = [
{
sortFirst: ['className'],
callbacksLast: true,
},
];
const sortFirstWithMultilineFirstArgs = [
{
sortFirst: ['className'],
multiline: 'first',
},
];

ruleTester.run('jsx-sort-props', rule, {
valid: parsers.all([].concat(
Expand Down Expand Up @@ -296,7 +327,29 @@ ruleTester.run('jsx-sort-props', rule, {
/>
`,
options: [{ locale: 'sk-SK' }],
} : []
} : [],
// sortFirst
{ code: '<App className="test" name="John" />;', options: sortFirstArgs },
{ code: '<App className="test" id="test" name="John" />;', options: sortFirstMultipleArgs },
{ code: '<App className="test" id="test" />;', options: sortFirstMultipleArgs },
{ code: '<App className="test" a b c />;', options: sortFirstArgs },
{ code: '<App className="test" id="test" a b c />;', options: sortFirstMultipleArgs },
{ code: '<App className="test" key={0} name="John" />;', options: sortFirstWithReservedFirstArgs },
{ code: '<App className="test" a name="John" />;', options: sortFirstWithShorthandFirstArgs },
{ code: '<App className="test" name="John" onClick={handleClick} />;', options: sortFirstWithCallbacksLastArgs },
{
code: `
<App
className="test"
data={{
test: 1,
}}
name="John"
/>
`,
options: sortFirstWithMultilineFirstArgs,
},
{ code: '<App classname="test" a="test2" />;', options: sortFirstWithIgnoreCaseArgs }
)),
invalid: parsers.all([].concat(
{
Expand Down Expand Up @@ -1101,6 +1154,83 @@ ruleTester.run('jsx-sort-props', rule, {
line: 11,
},
],
},
// sortFirst
{
code: '<App name="John" className="test" />;',
options: sortFirstArgs,
errors: [expectedSortFirstError],
output: '<App className="test" name="John" />;',
},
{
code: '<App id="test" className="test" name="John" />;',
options: sortFirstMultipleArgs,
errors: [expectedSortFirstError],
output: '<App className="test" id="test" name="John" />;',
},
{
code: '<App a className="test" b />;',
options: sortFirstArgs,
errors: [expectedSortFirstError],
output: '<App className="test" a b />;',
},
{
code: '<App key={0} className="test" name="John" />;',
options: sortFirstWithReservedFirstArgs,
errors: [expectedSortFirstError],
output: '<App className="test" key={0} name="John" />;',
},
{
code: '<App a className="test" name="John" />;',
options: sortFirstWithShorthandFirstArgs,
errors: [expectedSortFirstError],
output: '<App className="test" a name="John" />;',
},
{
code: '<App name="John" onClick={handleClick} className="test" />;',
options: sortFirstWithCallbacksLastArgs,
errors: [expectedSortFirstError],
output: '<App className="test" name="John" onClick={handleClick} />;',
},
{
code: `
<App
name="John"
className="test"
data={{
test: 1,
}}
/>
`,
options: sortFirstWithMultilineFirstArgs,
errors: [expectedSortFirstError, expectedMultilineFirstError],
output: `
<App
className="test"
data={{
test: 1,
}}
name="John"
/>
`,
},
{
code: '<App name="John" classname="test" />;',
options: sortFirstWithIgnoreCaseArgs,
errors: [expectedSortFirstError],
output: '<App classname="test" name="John" />;',
},
{
code: '<App className="test" id="test" tel={5555555} name="John" />;',
options: sortFirstMultipleArgs,
errors: [expectedError],
output: '<App className="test" id="test" name="John" tel={5555555} />;',
},
{
code: '<App id="test" className="test" id="test2" />;',
options: sortFirstMultipleArgs,
errors: [expectedSortFirstError],
output: '<App className="test" id="test" id="test2" />;',
}
)),
});
Loading