Skip to content

Commit 208a9db

Browse files
committed
feat: add groups option in sort-intersection-types rule
1 parent a6e67e9 commit 208a9db

File tree

3 files changed

+588
-12
lines changed

3 files changed

+588
-12
lines changed

docs/content/rules/sort-intersection-types.mdx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,62 @@ Controls whether sorting should be case-sensitive or not.
102102
- `true` — Ignore case when sorting alphabetically or naturally (e.g., “A” and “a” are the same).
103103
- `false` — Consider case when sorting (e.g., “A” comes before “a”).
104104

105+
### groups
106+
107+
<sub>(default: `[]`)</sub>
108+
109+
Allows you to specify a list of intersection type groups for sorting. Groups help organize types into categories, making your type definitions more readable and maintainable. Multiple groups can be combined to achieve the desired sorting order.
110+
111+
There are a lot of predefined groups.
112+
113+
Predefined Groups:
114+
115+
- `'conditional`' — Conditional types.
116+
- `'function`' — Function types.
117+
- `'import`' — Imported types.
118+
- `'intersection`' — Intersection types.
119+
- `'keyword`' — Keyword types.
120+
- `'literal`' — Literal types.
121+
- `'named`' — Named types.
122+
- `'object`' — Object types.
123+
- `'operator`' — Operator types.
124+
- `'tuple`' — Tuple types.
125+
- `'union`' — Union types.
126+
- `'nullish`' — Nullish types (`null` or `undefined`).
127+
- `'unknown`' — Types that don’t fit into any other group.
128+
129+
Example:
130+
131+
```ts
132+
type Example =
133+
// 'conditional' — Conditional types.
134+
(A extends B ? C : D) &
135+
// 'function' — Function types.
136+
((arg: T) => U) &
137+
// 'import' — Imported types.
138+
import('module').Type &
139+
// 'intersection' — Intersection types.
140+
(A & B) &
141+
// 'keyword' — Keyword types.
142+
any &
143+
// 'literal' — Literal types.
144+
'literal' &
145+
42 &
146+
// 'named' — Named types.
147+
SomeType &
148+
AnotherType &
149+
// 'object' — Object types.
150+
{ a: string; b: number; } &
151+
// 'operator' — Operator types.
152+
keyof T &
153+
// 'tuple' — Tuple types.
154+
[string, number] &
155+
// 'union' — Union types.
156+
(A | B) &
157+
// 'nullish' — Nullish types.
158+
null &
159+
undefined;
160+
```
105161

106162
## Usage
107163

rules/sort-intersection-types.ts

Lines changed: 112 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,39 @@
11
import type { SortingNode } from '../typings'
22

33
import { createEslintRule } from '../utils/create-eslint-rule'
4+
import { getGroupNumber } from '../utils/get-group-number'
45
import { getSourceCode } from '../utils/get-source-code'
56
import { toSingleLine } from '../utils/to-single-line'
67
import { rangeToDiff } from '../utils/range-to-diff'
78
import { isPositive } from '../utils/is-positive'
89
import { sortNodes } from '../utils/sort-nodes'
10+
import { useGroups } from '../utils/use-groups'
911
import { makeFixes } from '../utils/make-fixes'
1012
import { complete } from '../utils/complete'
1113
import { pairwise } from '../utils/pairwise'
1214
import { compare } from '../utils/compare'
1315

1416
type MESSAGE_ID = 'unexpectedIntersectionTypesOrder'
1517

18+
type Group =
19+
| 'intersection'
20+
| 'conditional'
21+
| 'function'
22+
| 'operator'
23+
| 'keyword'
24+
| 'literal'
25+
| 'nullish'
26+
| 'unknown'
27+
| 'import'
28+
| 'object'
29+
| 'named'
30+
| 'tuple'
31+
| 'union'
32+
1633
type Options = [
1734
Partial<{
1835
type: 'alphabetical' | 'line-length' | 'natural'
36+
groups: (Group[] | Group)[]
1937
order: 'desc' | 'asc'
2038
ignoreCase: boolean
2139
}>,
@@ -49,6 +67,9 @@ export default createEslintRule<Options, MESSAGE_ID>({
4967
type: 'boolean',
5068
default: true,
5169
},
70+
groups: {
71+
type: 'array',
72+
},
5273
},
5374
additionalProperties: false,
5475
},
@@ -70,24 +91,80 @@ export default createEslintRule<Options, MESSAGE_ID>({
7091
type: 'alphabetical',
7192
ignoreCase: true,
7293
order: 'asc',
94+
groups: [],
7395
} as const)
7496

7597
let sourceCode = getSourceCode(context)
7698

77-
let nodes: SortingNode[] = node.types.map(type => ({
78-
group:
79-
type.type === 'TSNullKeyword' || type.type === 'TSUndefinedKeyword'
80-
? 'nullable'
81-
: 'unknown',
82-
name: sourceCode.text.slice(...type.range),
83-
size: rangeToDiff(type.range),
84-
node: type,
85-
}))
99+
let nodes: SortingNode[] = node.types.map(type => {
100+
let { getGroup, defineGroup } = useGroups(options.groups)
101+
102+
switch (type.type) {
103+
case 'TSConditionalType':
104+
defineGroup('conditional')
105+
break
106+
case 'TSFunctionType':
107+
defineGroup('function')
108+
break
109+
case 'TSImportType':
110+
defineGroup('import')
111+
break
112+
case 'TSIntersectionType':
113+
defineGroup('intersection')
114+
break
115+
case 'TSAnyKeyword':
116+
case 'TSBigIntKeyword':
117+
case 'TSBooleanKeyword':
118+
case 'TSNeverKeyword':
119+
case 'TSNumberKeyword':
120+
case 'TSObjectKeyword':
121+
case 'TSStringKeyword':
122+
case 'TSUnknownKeyword':
123+
case 'TSVoidKeyword':
124+
defineGroup('keyword')
125+
break
126+
case 'TSLiteralType':
127+
defineGroup('literal')
128+
break
129+
case 'TSTypeReference':
130+
case 'TSIndexedAccessType':
131+
defineGroup('named')
132+
break
133+
case 'TSTypeLiteral':
134+
defineGroup('object')
135+
break
136+
case 'TSTypeQuery':
137+
case 'TSTypeOperator':
138+
defineGroup('operator')
139+
break
140+
case 'TSTupleType':
141+
defineGroup('tuple')
142+
break
143+
case 'TSUnionType':
144+
defineGroup('union')
145+
break
146+
case 'TSNullKeyword':
147+
case 'TSUndefinedKeyword':
148+
defineGroup('nullish')
149+
break
150+
}
151+
152+
return {
153+
name: sourceCode.text.slice(...type.range),
154+
size: rangeToDiff(type.range),
155+
group: getGroup(),
156+
node: type,
157+
}
158+
})
86159

87160
pairwise(nodes, (left, right) => {
88-
let compareValue = isPositive(compare(left, right, options))
161+
let leftNum = getGroupNumber(options.groups, left)
162+
let rightNum = getGroupNumber(options.groups, right)
89163

90-
if (compareValue) {
164+
if (
165+
leftNum > rightNum ||
166+
(leftNum === rightNum && isPositive(compare(left, right, options)))
167+
) {
91168
context.report({
92169
messageId: 'unexpectedIntersectionTypesOrder',
93170
data: {
@@ -96,7 +173,30 @@ export default createEslintRule<Options, MESSAGE_ID>({
96173
},
97174
node: right.node,
98175
fix: fixer => {
99-
let sortedNodes = sortNodes(nodes, options)
176+
let grouped: {
177+
[key: string]: SortingNode[]
178+
} = {}
179+
180+
for (let currentNode of nodes) {
181+
let groupNum = getGroupNumber(options.groups, currentNode)
182+
183+
if (!(groupNum in grouped)) {
184+
grouped[groupNum] = [currentNode]
185+
} else {
186+
grouped[groupNum] = sortNodes(
187+
[...grouped[groupNum], currentNode],
188+
options,
189+
)
190+
}
191+
}
192+
193+
let sortedNodes: SortingNode[] = []
194+
195+
for (let group of Object.keys(grouped).sort(
196+
(a, b) => Number(a) - Number(b),
197+
)) {
198+
sortedNodes.push(...sortNodes(grouped[group], options))
199+
}
100200

101201
return makeFixes(fixer, nodes, sortedNodes, sourceCode)
102202
},

0 commit comments

Comments
 (0)