Skip to content

Commit

Permalink
feat: add groups option in sort-union-types rule
Browse files Browse the repository at this point in the history
  • Loading branch information
azat-io committed Jul 22, 2024
1 parent c4977df commit c69f277
Show file tree
Hide file tree
Showing 3 changed files with 591 additions and 8 deletions.
58 changes: 58 additions & 0 deletions docs/content/rules/sort-union-types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,64 @@ Controls whether sorting should be case-sensitive or not.
- `true` — Ignore case when sorting alphabetically or naturally (e.g., “A” and “a” are the same).
- `false` — Consider case when sorting (e.g., “A” comes before “a”).

### groups

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

Allows you to specify a list of union 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.

There are a lot of predefined groups.

Predefined Groups:

- `'conditional`' — Conditional types.
- `'function`' — Function types.
- `'import`' — Imported types.
- `'intersection`' — Intersection types.
- `'keyword`' — Keyword types.
- `'literal`' — Literal types.
- `'named`' — Named types.
- `'object`' — Object types.
- `'operator`' — Operator types.
- `'tuple`' — Tuple types.
- `'union`' — Union types.
- `'nullish`' — Nullish types (`null` or `undefined`).
- `'unknown`' — Types that don’t fit into any other group.

Example:

```ts
type Example =
// 'conditional' — Conditional types.
(A extends B ? C : D) |
// 'function' — Function types.
((arg: T) => U) |
// 'import' — Imported types.
import('module').Type |
// 'intersection' — Intersection types.
(A & B) |
// 'keyword' — Keyword types.
any |
// 'literal' — Literal types.
'literal' |
42 |
// 'named' — Named types.
SomeType |
AnotherType |
// 'object' — Object types.
{ a: string; b: number; } |
// 'operator' — Operator types.
keyof T |
// 'tuple' — Tuple types.
[string, number] |
// 'union' — Union types.
(A | B) |
// 'nullish' — Nullish types.
null |
undefined;
```


## Usage

<CodeTabs
Expand Down
121 changes: 113 additions & 8 deletions rules/sort-union-types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { SortingNode } from '../typings'

import { createEslintRule } from '../utils/create-eslint-rule'
import { getGroupNumber } from '../utils/get-group-number'
import { getSourceCode } from '../utils/get-source-code'
import { toSingleLine } from '../utils/to-single-line'
import { rangeToDiff } from '../utils/range-to-diff'
import { isPositive } from '../utils/is-positive'
import { useGroups } from '../utils/use-groups'
import { sortNodes } from '../utils/sort-nodes'
import { makeFixes } from '../utils/make-fixes'
import { complete } from '../utils/complete'
Expand All @@ -13,9 +15,25 @@ import { compare } from '../utils/compare'

type MESSAGE_ID = 'unexpectedUnionTypesOrder'

type Group =
| 'intersection'
| 'conditional'
| 'function'
| 'operator'
| 'keyword'
| 'literal'
| 'nullish'
| 'unknown'
| 'import'
| 'object'
| 'named'
| 'tuple'
| 'union'

type Options = [
Partial<{
type: 'alphabetical' | 'line-length' | 'natural'
groups: (Group[] | Group)[]
order: 'desc' | 'asc'
ignoreCase: boolean
}>,
Expand Down Expand Up @@ -49,6 +67,9 @@ export default createEslintRule<Options, MESSAGE_ID>({
type: 'boolean',
default: true,
},
groups: {
type: 'array',
},
},
additionalProperties: false,
},
Expand All @@ -70,20 +91,80 @@ export default createEslintRule<Options, MESSAGE_ID>({
type: 'alphabetical',
ignoreCase: true,
order: 'asc',
groups: [],
} as const)

let sourceCode = getSourceCode(context)

let nodes: SortingNode[] = node.types.map(type => ({
name: sourceCode.text.slice(...type.range),
size: rangeToDiff(type.range),
node: type,
}))
let nodes: SortingNode[] = node.types.map(type => {
let { getGroup, defineGroup } = useGroups(options.groups)

switch (type.type) {
case 'TSConditionalType':
defineGroup('conditional')
break
case 'TSFunctionType':
defineGroup('function')
break
case 'TSImportType':
defineGroup('import')
break
case 'TSIntersectionType':
defineGroup('intersection')
break
case 'TSAnyKeyword':
case 'TSBigIntKeyword':
case 'TSBooleanKeyword':
case 'TSNeverKeyword':
case 'TSNumberKeyword':
case 'TSObjectKeyword':
case 'TSStringKeyword':
case 'TSUnknownKeyword':
case 'TSVoidKeyword':
defineGroup('keyword')
break
case 'TSLiteralType':
defineGroup('literal')
break
case 'TSTypeReference':
case 'TSIndexedAccessType':
defineGroup('named')
break
case 'TSTypeLiteral':
defineGroup('object')
break
case 'TSTypeQuery':
case 'TSTypeOperator':
defineGroup('operator')
break
case 'TSTupleType':
defineGroup('tuple')
break
case 'TSUnionType':
defineGroup('union')
break
case 'TSNullKeyword':
case 'TSUndefinedKeyword':
defineGroup('nullish')
break
}

return {
name: sourceCode.text.slice(...type.range),
size: rangeToDiff(type.range),
group: getGroup(),
node: type,
}
})

pairwise(nodes, (left, right) => {
let compareValue = isPositive(compare(left, right, options))
let leftNum = getGroupNumber(options.groups, left)
let rightNum = getGroupNumber(options.groups, right)

if (compareValue) {
if (
leftNum > rightNum ||
(leftNum === rightNum && isPositive(compare(left, right, options)))
) {
context.report({
messageId: 'unexpectedUnionTypesOrder',
data: {
Expand All @@ -92,7 +173,31 @@ export default createEslintRule<Options, MESSAGE_ID>({
},
node: right.node,
fix: fixer => {
let sortedNodes: SortingNode[] = sortNodes(nodes, options)
let grouped: {
[key: string]: SortingNode[]
} = {}

for (let currentNode of nodes) {
let groupNum = getGroupNumber(options.groups, 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(
(a, b) => Number(a) - Number(b),
)) {
sortedNodes.push(...sortNodes(grouped[group], options))
}

return makeFixes(fixer, nodes, sortedNodes, sourceCode)
},
})
Expand Down
Loading

0 comments on commit c69f277

Please sign in to comment.