Skip to content

Commit

Permalink
feat: add groups option in sort-intersection-types rule
Browse files Browse the repository at this point in the history
  • Loading branch information
azat-io committed Jul 22, 2024
1 parent a6e67e9 commit 208a9db
Show file tree
Hide file tree
Showing 3 changed files with 588 additions and 12 deletions.
56 changes: 56 additions & 0 deletions docs/content/rules/sort-intersection-types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,62 @@ 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 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.

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

Expand Down
124 changes: 112 additions & 12 deletions rules/sort-intersection-types.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
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 { sortNodes } from '../utils/sort-nodes'
import { useGroups } from '../utils/use-groups'
import { makeFixes } from '../utils/make-fixes'
import { complete } from '../utils/complete'
import { pairwise } from '../utils/pairwise'
import { compare } from '../utils/compare'

type MESSAGE_ID = 'unexpectedIntersectionTypesOrder'

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,24 +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 => ({
group:
type.type === 'TSNullKeyword' || type.type === 'TSUndefinedKeyword'
? 'nullable'
: 'unknown',
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: 'unexpectedIntersectionTypesOrder',
data: {
Expand All @@ -96,7 +173,30 @@ export default createEslintRule<Options, MESSAGE_ID>({
},
node: right.node,
fix: fixer => {
let sortedNodes = 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 208a9db

Please sign in to comment.