diff --git a/docs/rules/sort-jsx-props.md b/docs/rules/sort-jsx-props.md
index e21f66bcb..50b16b402 100644
--- a/docs/rules/sort-jsx-props.md
+++ b/docs/rules/sort-jsx-props.md
@@ -87,14 +87,17 @@ If you use the [`jsx-sort-props`](https://github.com/jsx-eslint/eslint-plugin-re
This rule accepts an options object with the following properties:
```ts
+type Group =
+ | 'multiline'
+ | 'shorthand'
+ | 'unknown'
+
interface Options {
type?: 'alphabetical' | 'natural' | 'line-length'
order?: 'asc' | 'desc'
'ignore-case'?: boolean
- 'always-on-top'?: string[]
- callback?: 'first' | 'ignore' | 'last'
- multiline?: 'first' | 'ignore' | 'last'
- shorthand?: 'first' | 'ignore' | 'last'
+ groups?: (Group | Group[])[]
+ 'custom-groups': { [key in T[number]]: string[] | string }
}
```
@@ -119,35 +122,27 @@ interface Options {
Only affects alphabetical and natural sorting. When `true` the rule ignores the case-sensitivity of the order.
-### always-on-top
+### groups
(default: `[]`)
-You can set a list of property names that will always go at the beginning of the JSX element.
-
-### callback
-
-(default: `'ignore'`)
-
-- `first` - enforce callback JSX props to be at the top of the list
-- `ignore` - sort callback props in general order
-- `last` - enforce callback JSX props to be at the end of the list
-
-### multiline
+You can set up a list of JSX props groups for sorting. Groups can be combined. There are predefined groups: `'multiline'`, `'shorthand'`.
-(default: `'ignore'`)
+### custom-groups
-- `first` - enforce multiline JSX props to be at the top of the list
-- `ignore` - sort multiline props in general order
-- `last` - enforce multiline JSX props to be at the end of the list
+(default: `{}`)
-### shorthand
+You can define your own groups for JSX props. The [minimatch](https://github.com/isaacs/minimatch) library is used for pattern matching.
-(default: `'ignore'`)
+Example:
-- `first` - enforce shorthand JSX props to be at the top of the list
-- `ignore` - sort shorthand props in general order
-- `last` - enforce shorthand JSX props to be at the end of the list
+```
+{
+ "custom-groups": {
+ "callback": "on*"
+ }
+}
+```
## ⚙️ Usage
@@ -163,10 +158,11 @@ You can set a list of property names that will always go at the beginning of the
{
"type": "natural",
"order": "asc",
- "always-on-top": ["id", "name"],
- "shorthand": "last",
- "multiline": "first",
- "callback": "ignore"
+ "groups": [
+ "multiline",
+ "unknown",
+ "shorthand"
+ ]
}
]
}
@@ -188,10 +184,11 @@ export default [
{
type: 'natural',
order: 'asc',
- 'always-on-top': ['id', 'name'],
- shorthand: 'last',
- multiline: 'first',
- callback: 'ignore',
+ groups: [
+ 'multiline',
+ 'unknown',
+ 'shorthand',
+ ],
},
],
},
diff --git a/rules/sort-jsx-props.ts b/rules/sort-jsx-props.ts
index 0043790cd..42e1c8ece 100644
--- a/rules/sort-jsx-props.ts
+++ b/rules/sort-jsx-props.ts
@@ -1,6 +1,7 @@
import type { TSESTree } from '@typescript-eslint/types'
import { AST_NODE_TYPES } from '@typescript-eslint/types'
+import { minimatch } from 'minimatch'
import type { SortingNode } from '../typings'
@@ -11,27 +12,25 @@ import { makeFixes } from '../utils/make-fixes'
import { sortNodes } from '../utils/sort-nodes'
import { pairwise } from '../utils/pairwise'
import { complete } from '../utils/complete'
-import { groupBy } from '../utils/group-by'
import { compare } from '../utils/compare'
type MESSAGE_ID = 'unexpectedJSXPropsOrder'
-export enum Position {
- 'exception' = 'exception',
- 'ignore' = 'ignore',
- 'first' = 'first',
- 'last' = 'last',
-}
+type Group =
+ | 'multiline'
+ | 'shorthand'
+ | 'unknown'
+ | T[number]
-type SortingNodeWithPosition = SortingNode & { position: Position }
+type SortingNodeWithGroup = SortingNode & {
+ group: Group
+}
-type Options = [
+type Options = [
Partial<{
- 'always-on-top': string[]
+ 'custom-groups': { [key in T[number]]: string[] | string }
+ groups: (Group[] | Group)[]
'ignore-case': boolean
- multiline: Position
- shorthand: Position
- callback: Position
order: SortOrder
type: SortType
}>,
@@ -39,7 +38,7 @@ type Options = [
export const RULE_NAME = 'sort-jsx-props'
-export default createEslintRule({
+export default createEslintRule, MESSAGE_ID>({
name: RULE_NAME,
meta: {
type: 'suggestion',
@@ -52,6 +51,9 @@ export default createEslintRule({
{
type: 'object',
properties: {
+ 'custom-groups': {
+ type: 'object',
+ },
type: {
enum: [
SortType.alphabetical,
@@ -64,7 +66,7 @@ export default createEslintRule({
enum: [SortOrder.asc, SortOrder.desc],
default: SortOrder.asc,
},
- 'always-on-top': {
+ groups: {
type: 'array',
default: [],
},
@@ -72,15 +74,6 @@ export default createEslintRule({
type: 'boolean',
default: false,
},
- shorthand: {
- enum: [Position.first, Position.last, Position.ignore],
- },
- callback: {
- enum: [Position.first, Position.last, Position.ignore],
- },
- multiline: {
- enum: [Position.first, Position.last, Position.ignore],
- },
},
additionalProperties: false,
},
@@ -100,20 +93,18 @@ export default createEslintRule({
if (node.openingElement.attributes.length > 1) {
let options = complete(context.options.at(0), {
type: SortType.alphabetical,
- shorthand: Position.ignore,
- multiline: Position.ignore,
- callback: Position.ignore,
- 'always-on-top': [],
'ignore-case': false,
order: SortOrder.asc,
+ 'custom-groups': {},
+ groups: [],
})
let source = context.getSourceCode()
- let parts: SortingNodeWithPosition[][] =
+ let parts: SortingNodeWithGroup[][] =
node.openingElement.attributes.reduce(
(
- accumulator: SortingNodeWithPosition[][],
+ accumulator: SortingNodeWithGroup[][],
attribute: TSESTree.JSXSpreadAttribute | TSESTree.JSXAttribute,
) => {
if (attribute.type === AST_NODE_TYPES.JSXSpreadAttribute) {
@@ -121,44 +112,44 @@ export default createEslintRule({
return accumulator
}
- let position: Position = Position.ignore
+ let name = attribute.name.type === AST_NODE_TYPES.JSXNamespacedName
+ ? `${attribute.name.namespace.name}:${attribute.name.name.name}`
+ : attribute.name.name
- if (
- attribute.name.type === AST_NODE_TYPES.JSXIdentifier &&
- options['always-on-top'].includes(attribute.name.name)
- ) {
- position = Position.exception
- } else {
- if (
- options.shorthand !== Position.ignore &&
- attribute.value === null
- ) {
- position = options.shorthand
+ let group: Group | undefined
+
+ let defineGroup = (nodeGroup: Group) => {
+ if (!group && options.groups.flat().includes(nodeGroup)) {
+ group = nodeGroup
}
+ }
+ for (let [key, pattern] of Object.entries(options['custom-groups'])) {
if (
- options.callback !== Position.ignore &&
- attribute.name.type === AST_NODE_TYPES.JSXIdentifier &&
- attribute.name.name.indexOf('on') === 0 &&
- attribute.value !== null
+ Array.isArray(pattern) &&
+ pattern.some(patternValue => minimatch(name, patternValue))
) {
- position = options.callback
- } else if (
- options.multiline !== Position.ignore &&
- attribute.loc.start.line !== attribute.loc.end.line
- ) {
- position = options.multiline
+ defineGroup(key)
+ }
+
+ if (typeof pattern === 'string' && minimatch(name, pattern)) {
+ defineGroup(key)
}
}
+ if (attribute.value === null) {
+ defineGroup('shorthand')
+ }
+
+ if (attribute.loc.start.line !== attribute.loc.end.line) {
+ defineGroup('multiline')
+ }
+
let jsxNode = {
- name:
- attribute.name.type === AST_NODE_TYPES.JSXNamespacedName
- ? `${attribute.name.namespace.name}:${attribute.name.name.name}`
- : attribute.name.name,
size: rangeToDiff(attribute.range),
+ group: group ?? 'unknown',
node: attribute,
- position,
+ name,
}
accumulator.at(-1)!.push(jsxNode)
@@ -168,32 +159,27 @@ export default createEslintRule({
[[]],
)
- for (let nodes of parts) {
- pairwise(nodes, (left, right) => {
- let comparison: boolean
+ let getGroupNumber = (nodeWithGroup: SortingNodeWithGroup): number => {
+ for (let i = 0, max = options.groups.length; i < max; i++) {
+ let currentGroup = options.groups[i]
if (
- left.position === Position.exception &&
- right.position === Position.exception
+ nodeWithGroup.group === currentGroup ||
+ (Array.isArray(currentGroup) && currentGroup.includes(nodeWithGroup.group))
) {
- comparison =
- options['always-on-top'].indexOf(left.name) >
- options['always-on-top'].indexOf(right.name)
- } else if (left.position === right.position) {
- comparison = compare(left, right, options)
- } else {
- let positionPower = {
- [Position.exception]: 2,
- [Position.first]: 1,
- [Position.ignore]: 0,
- [Position.last]: -1,
- }
-
- comparison =
- positionPower[left.position] < positionPower[right.position]
+ return i
}
+ }
+ return options.groups.length
+ }
- if (comparison) {
+ for (let nodes of parts) {
+ pairwise(nodes, (left, right) => {
+ let leftNum = getGroupNumber(left)
+ let rightNum = getGroupNumber(right)
+
+ if ((leftNum > rightNum ||
+ (leftNum === rightNum && compare(left, right, options)))) {
context.report({
messageId: 'unexpectedJSXPropsOrder',
data: {
@@ -202,24 +188,31 @@ export default createEslintRule({
},
node: right.node,
fix: fixer => {
- let groups = groupBy(nodes, ({ position }) => position)
-
- let getGroup = (index: string) =>
- index in groups ? groups[index] : []
-
- let sortedNodes = [
- getGroup(Position.exception).sort(
- (aNode, bNode) =>
- options['always-on-top'].indexOf(aNode.name) -
- options['always-on-top'].indexOf(bNode.name),
- ),
- sortNodes(getGroup(Position.first), options),
- sortNodes(getGroup(Position.ignore), options),
- sortNodes(getGroup(Position.last), options),
- ].flat()
+ let grouped: {
+ [key: string]: SortingNodeWithGroup[]
+ } = {}
+
+ for (let currentNode of nodes) {
+ let groupNum = getGroupNumber(currentNode)
+
+ if (!(groupNum in grouped)) {
+ grouped[groupNum] = [currentNode]
+ } else {
+ grouped[groupNum] = sortNodes(
+ [...grouped[groupNum], currentNode],
+ options,
+ )
+ }
+ }
+
+ let sortedNodes: SortingNode[] = []
+
+ for(let group of Object.keys(grouped).sort()) {
+ sortedNodes.push(...sortNodes(grouped[group], options))
+ }
return makeFixes(fixer, nodes, sortedNodes, source)
- },
+ }
})
}
})
diff --git a/test/sort-jsx-props.test.ts b/test/sort-jsx-props.test.ts
index 9b04e7f49..77a025551 100644
--- a/test/sort-jsx-props.test.ts
+++ b/test/sort-jsx-props.test.ts
@@ -2,7 +2,7 @@ import { ESLintUtils } from '@typescript-eslint/utils'
import { describe, it } from 'vitest'
import { dedent } from 'ts-dedent'
-import rule, { RULE_NAME, Position } from '../rules/sort-jsx-props'
+import rule, { RULE_NAME } from '../rules/sort-jsx-props'
import { SortOrder, SortType } from '../typings'
describe(RULE_NAME, () => {
@@ -247,8 +247,8 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ groups: ['unknown', 'shorthand'],
type: SortType.alphabetical,
- shorthand: Position.last,
order: SortOrder.asc,
},
],
@@ -278,8 +278,8 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ groups: ['unknown', 'shorthand'],
type: SortType.alphabetical,
- shorthand: Position.last,
order: SortOrder.asc,
},
],
@@ -311,8 +311,9 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { callback: 'on*' },
+ groups: ['unknown', 'callback'],
type: SortType.alphabetical,
- callback: Position.last,
order: SortOrder.asc,
},
],
@@ -338,8 +339,9 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { callback: 'on*' },
+ groups: ['unknown', 'callback'],
type: SortType.alphabetical,
- callback: Position.last,
order: SortOrder.asc,
},
],
@@ -377,8 +379,8 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ groups: [['multiline'], 'unknown'],
type: SortType.alphabetical,
- multiline: Position.first,
order: SortOrder.asc,
},
],
@@ -416,8 +418,8 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ groups: ['multiline', 'unknown'],
type: SortType.alphabetical,
- multiline: Position.first,
order: SortOrder.asc,
},
],
@@ -458,9 +460,10 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { top: ['id', 'name'] },
type: SortType.alphabetical,
+ groups: ['top', 'unknown'],
order: SortOrder.asc,
- 'always-on-top': ['id', 'name'],
},
],
},
@@ -489,9 +492,10 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { top: ['id', 'name'] },
type: SortType.alphabetical,
+ groups: ['top', 'unknown'],
order: SortOrder.asc,
- 'always-on-top': ['id', 'name'],
},
],
errors: [
@@ -526,9 +530,10 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { top: ['id'] },
type: SortType.alphabetical,
+ groups: ['top', 'unknown'],
order: SortOrder.asc,
- 'always-on-top': ['id', 'name'],
},
],
errors: [
@@ -556,9 +561,10 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { top: ['id'] },
type: SortType.alphabetical,
+ groups: ['top', 'unknown'],
order: SortOrder.asc,
- 'always-on-top': ['id'],
},
],
errors: [
@@ -808,8 +814,8 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ groups: ['unknown', 'shorthand'],
type: SortType.natural,
- shorthand: Position.last,
order: SortOrder.asc,
},
],
@@ -839,8 +845,8 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ groups: ['unknown', 'shorthand'],
type: SortType.natural,
- shorthand: Position.last,
order: SortOrder.asc,
},
],
@@ -872,8 +878,9 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { callback: 'on*' },
+ groups: ['unknown', 'callback'],
type: SortType.natural,
- callback: Position.last,
order: SortOrder.asc,
},
],
@@ -899,8 +906,9 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { callback: 'on*' },
+ groups: ['unknown', 'callback'],
type: SortType.natural,
- callback: Position.last,
order: SortOrder.asc,
},
],
@@ -938,8 +946,8 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ groups: [['multiline'], 'unknown'],
type: SortType.natural,
- multiline: Position.first,
order: SortOrder.asc,
},
],
@@ -977,8 +985,8 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ groups: ['multiline', 'unknown'],
type: SortType.natural,
- multiline: Position.first,
order: SortOrder.asc,
},
],
@@ -1019,9 +1027,10 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { top: ['id', 'name'] },
+ groups: ['top', 'unknown'],
type: SortType.natural,
order: SortOrder.asc,
- 'always-on-top': ['id', 'name'],
},
],
},
@@ -1050,9 +1059,10 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { top: ['id', 'name'] },
+ groups: ['top', 'unknown'],
type: SortType.natural,
order: SortOrder.asc,
- 'always-on-top': ['id', 'name'],
},
],
errors: [
@@ -1087,9 +1097,10 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { top: ['id'] },
+ groups: ['top', 'unknown'],
type: SortType.natural,
order: SortOrder.asc,
- 'always-on-top': ['id', 'name'],
},
],
errors: [
@@ -1117,9 +1128,10 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { top: ['id'] },
+ groups: ['top', 'unknown'],
type: SortType.natural,
order: SortOrder.asc,
- 'always-on-top': ['id'],
},
],
errors: [
@@ -1181,8 +1193,8 @@ describe(RULE_NAME, () => {
let Odokawa = () => (
Pew-pew
@@ -1369,8 +1381,8 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ groups: ['unknown', 'shorthand'],
type: SortType['line-length'],
- shorthand: Position.last,
order: SortOrder.desc,
},
],
@@ -1391,8 +1403,8 @@ describe(RULE_NAME, () => {
output: dedent`
let Spike = () => (
@@ -1400,9 +1412,9 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ groups: ['unknown', 'shorthand'],
type: SortType['line-length'],
order: SortOrder.desc,
- shorthand: Position.last,
},
],
errors: [
@@ -1440,8 +1452,9 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { callback: 'on*' },
+ groups: ['unknown', 'callback'],
type: SortType['line-length'],
- callback: Position.last,
order: SortOrder.desc,
},
],
@@ -1467,8 +1480,9 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { callback: 'on*' },
+ groups: ['unknown', 'callback'],
type: SortType['line-length'],
- callback: Position.last,
order: SortOrder.desc,
},
],
@@ -1506,8 +1520,8 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ groups: [['multiline'], 'unknown'],
type: SortType['line-length'],
- multiline: Position.first,
order: SortOrder.desc,
},
],
@@ -1545,8 +1559,8 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ groups: ['multiline', 'unknown'],
type: SortType['line-length'],
- multiline: Position.first,
order: SortOrder.desc,
},
],
@@ -1587,9 +1601,10 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { top: ['id', 'name'] },
type: SortType['line-length'],
+ groups: ['top', 'unknown'],
order: SortOrder.desc,
- 'always-on-top': ['id', 'name'],
},
],
},
@@ -1618,9 +1633,10 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { top: ['id', 'name'] },
type: SortType['line-length'],
+ groups: ['top', 'unknown'],
order: SortOrder.desc,
- 'always-on-top': ['id', 'name'],
},
],
errors: [
@@ -1662,9 +1678,10 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { top: ['id', 'name'] },
type: SortType['line-length'],
+ groups: ['top', 'unknown'],
order: SortOrder.desc,
- 'always-on-top': ['id', 'name'],
},
],
errors: [
@@ -1692,9 +1709,10 @@ describe(RULE_NAME, () => {
`,
options: [
{
+ 'custom-groups': { top: ['id', 'name'] },
type: SortType['line-length'],
+ groups: ['top', 'unknown'],
order: SortOrder.desc,
- 'always-on-top': ['id'],
},
],
errors: [