Skip to content

Commit

Permalink
feat!: move callback, multiline and shorthand opts to groups in sort-…
Browse files Browse the repository at this point in the history
…jsx-props rule
  • Loading branch information
azat-io committed Jul 21, 2023
1 parent 3d65cca commit 6dee53c
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 160 deletions.
63 changes: 30 additions & 33 deletions docs/rules/sort-jsx-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
```

Expand All @@ -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

<sub>(default: `[]`)</sub>

You can set a list of property names that will always go at the beginning of the JSX element.

### callback

<sub>(default: `'ignore'`)</sub>

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

<sub>(default: `'ignore'`)</sub>
### 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
<sub>(default: `{}`)</sub>

### shorthand
You can define your own groups for JSX props. The [minimatch](https://github.com/isaacs/minimatch) library is used for pattern matching.

<sub>(default: `'ignore'`)</sub>
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

Expand All @@ -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"
]
}
]
}
Expand All @@ -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',
],
},
],
},
Expand Down
181 changes: 87 additions & 94 deletions rules/sort-jsx-props.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -11,35 +12,33 @@ 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<T extends string[]> =
| 'multiline'
| 'shorthand'
| 'unknown'
| T[number]

type SortingNodeWithPosition = SortingNode & { position: Position }
type SortingNodeWithGroup<T extends string[]> = SortingNode & {
group: Group<T>
}

type Options = [
type Options<T extends string[]> = [
Partial<{
'always-on-top': string[]
'custom-groups': { [key in T[number]]: string[] | string }
groups: (Group<T>[] | Group<T>)[]
'ignore-case': boolean
multiline: Position
shorthand: Position
callback: Position
order: SortOrder
type: SortType
}>,
]

export const RULE_NAME = 'sort-jsx-props'

export default createEslintRule<Options, MESSAGE_ID>({
export default createEslintRule<Options<string[]>, MESSAGE_ID>({
name: RULE_NAME,
meta: {
type: 'suggestion',
Expand All @@ -52,6 +51,9 @@ export default createEslintRule<Options, MESSAGE_ID>({
{
type: 'object',
properties: {
'custom-groups': {
type: 'object',
},
type: {
enum: [
SortType.alphabetical,
Expand All @@ -64,23 +66,14 @@ export default createEslintRule<Options, MESSAGE_ID>({
enum: [SortOrder.asc, SortOrder.desc],
default: SortOrder.asc,
},
'always-on-top': {
groups: {
type: 'array',
default: [],
},
'ignore-case': {
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,
},
Expand All @@ -100,65 +93,63 @@ export default createEslintRule<Options, MESSAGE_ID>({
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<string[]>[][] =
node.openingElement.attributes.reduce(
(
accumulator: SortingNodeWithPosition[][],
accumulator: SortingNodeWithGroup<string[]>[][],
attribute: TSESTree.JSXSpreadAttribute | TSESTree.JSXAttribute,
) => {
if (attribute.type === AST_NODE_TYPES.JSXSpreadAttribute) {
accumulator.push([])
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<string[]> | undefined

let defineGroup = (nodeGroup: Group<string[]>) => {
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)
Expand All @@ -168,32 +159,27 @@ export default createEslintRule<Options, MESSAGE_ID>({
[[]],
)

for (let nodes of parts) {
pairwise(nodes, (left, right) => {
let comparison: boolean
let getGroupNumber = (nodeWithGroup: SortingNodeWithGroup<string[]>): 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: {
Expand All @@ -202,24 +188,31 @@ export default createEslintRule<Options, MESSAGE_ID>({
},
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<string[]>[]
} = {}

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)
},
}
})
}
})
Expand Down

0 comments on commit 6dee53c

Please sign in to comment.