Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: move from position opts to groups in sort-jsx-props rule #40

Merged
merged 1 commit into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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