diff --git a/docs/rules/jsx-sort-props.md b/docs/rules/jsx-sort-props.md index b51209f32e..d1b93009c7 100644 --- a/docs/rules/jsx-sort-props.md +++ b/docs/rules/jsx-sort-props.md @@ -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 -; + ``` Examples of **correct** code for this rule: @@ -35,6 +35,7 @@ Examples of **correct** code for this rule: "ignoreCase": , "noSortAlphabetically": , "reservedFirst": |>, + "sortFirst": >, "locale": "auto" | "any valid locale" }] ... @@ -47,7 +48,7 @@ When `true` the rule ignores the case-sensitivity of the props order. Examples of **correct** code for this rule ```jsx -; + ``` ### `callbacksLast` @@ -138,6 +139,32 @@ With `reservedFirst: ["key"]`, the following will **not** warn: ``` +### `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'] }] + +``` + +Examples of **correct** code for this rule: + +```jsx +// 'jsx-sort-props': [1, { sortFirst: ['className'] }] + + +// 'jsx-sort-props': [1, { sortFirst: ['className', 'id'] }] + + +// 'jsx-sort-props': [1, { sortFirst: ['className'], ignoreCase: true }] + +``` + ### `locale` Defaults to `"auto"`, meaning, the locale of the current environment. diff --git a/lib/rules/jsx-sort-props.js b/lib/rules/jsx-sort-props.js index ec9d869b74..4ca0906fc5 100644 --- a/lib/rules/jsx-sort-props.js +++ b/lib/rules/jsx-sort-props.js @@ -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', }; @@ -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 } @@ -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); @@ -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. @@ -236,6 +267,7 @@ function generateFixerFunction(node, context, reservedList) { noSortAlphabetically, reservedFirst, reservedList, + sortFirst, locale, }; const sortableAttributeGroups = getGroupsOfSortableAttributes(attributes, context); @@ -382,6 +414,12 @@ module.exports = { reservedFirst: { type: ['array', 'boolean'], }, + sortFirst: { + type: 'array', + items: { + type: 'string', + }, + }, locale: { type: 'string', default: 'auto', @@ -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 { @@ -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(); diff --git a/test-published-types/package.json b/test-published-types/package.json index 80953c53ab..412a588ea4 100644 --- a/test-published-types/package.json +++ b/test-published-types/package.json @@ -3,6 +3,6 @@ "private": true, "version": "0.0.0", "dependencies": { - "eslint": "^9.11.1" + "eslint": "~9.38.0" } } diff --git a/tests/lib/rules/jsx-sort-props.js b/tests/lib/rules/jsx-sort-props.js index 797dc26650..38d598a10f 100644 --- a/tests/lib/rules/jsx-sort-props.js +++ b/tests/lib/rules/jsx-sort-props.js @@ -58,6 +58,10 @@ const expectedReservedFirstError = { messageId: 'listReservedPropsFirst', type: 'JSXIdentifier', }; +const expectedSortFirstError = { + messageId: 'listSortFirstPropsFirst', + type: 'JSXIdentifier', +}; const expectedEmptyReservedFirstError = { messageId: 'listIsEmpty', }; @@ -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( @@ -296,7 +327,29 @@ ruleTester.run('jsx-sort-props', rule, { /> `, options: [{ locale: 'sk-SK' }], - } : [] + } : [], + // sortFirst + { code: ';', options: sortFirstArgs }, + { code: ';', options: sortFirstMultipleArgs }, + { code: ';', options: sortFirstMultipleArgs }, + { code: ';', options: sortFirstArgs }, + { code: ';', options: sortFirstMultipleArgs }, + { code: ';', options: sortFirstWithReservedFirstArgs }, + { code: ';', options: sortFirstWithShorthandFirstArgs }, + { code: ';', options: sortFirstWithCallbacksLastArgs }, + { + code: ` + + `, + options: sortFirstWithMultilineFirstArgs, + }, + { code: ';', options: sortFirstWithIgnoreCaseArgs } )), invalid: parsers.all([].concat( { @@ -1101,6 +1154,83 @@ ruleTester.run('jsx-sort-props', rule, { line: 11, }, ], + }, + // sortFirst + { + code: ';', + options: sortFirstArgs, + errors: [expectedSortFirstError], + output: ';', + }, + { + code: ';', + options: sortFirstMultipleArgs, + errors: [expectedSortFirstError], + output: ';', + }, + { + code: ';', + options: sortFirstArgs, + errors: [expectedSortFirstError], + output: ';', + }, + { + code: ';', + options: sortFirstWithReservedFirstArgs, + errors: [expectedSortFirstError], + output: ';', + }, + { + code: ';', + options: sortFirstWithShorthandFirstArgs, + errors: [expectedSortFirstError], + output: ';', + }, + { + code: ';', + options: sortFirstWithCallbacksLastArgs, + errors: [expectedSortFirstError], + output: ';', + }, + { + code: ` + + `, + options: sortFirstWithMultilineFirstArgs, + errors: [expectedSortFirstError, expectedMultilineFirstError], + output: ` + + `, + }, + { + code: ';', + options: sortFirstWithIgnoreCaseArgs, + errors: [expectedSortFirstError], + output: ';', + }, + { + code: ';', + options: sortFirstMultipleArgs, + errors: [expectedError], + output: ';', + }, + { + code: ';', + options: sortFirstMultipleArgs, + errors: [expectedSortFirstError], + output: ';', } )), });