Skip to content

Commit

Permalink
feat: add partition by comment and partition by new line in sort-inte…
Browse files Browse the repository at this point in the history
…rsection-types
  • Loading branch information
hugop95 authored Sep 22, 2024
1 parent a9aee0c commit 9fe8abd
Show file tree
Hide file tree
Showing 3 changed files with 399 additions and 111 deletions.
35 changes: 35 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,37 @@ 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”).

### partitionByComment

<sub>default: `false`</sub>

Allows you to use comments to separate the members of intersection types into logical groups. This can help in organizing and maintaining large enums by creating partitions within the enum based on comments.

- `true` — All comments will be treated as delimiters, creating partitions.
- `false` — Comments will not be used as delimiters.
- `string` — A glob pattern to specify which comments should act as delimiters.
- `string[]` — A list of glob patterns to specify which comments should act as delimiters.

### partitionByNewLine

<sub>default: `false`</sub>

When `true`, the rule will not sort the members of an intersection type if there is an empty line between them. This can be useful for keeping logically separated groups of members in their defined order.

```ts
type Employee =
// Group 1
FirstName &
LastName &

// Group 2
Age &

// Group 3
Address &
Country
```
### groups
<sub>default: `[]`</sub>
Expand Down Expand Up @@ -180,6 +211,8 @@ type Example =
type: 'alphabetical',
order: 'asc',
ignoreCase: true,
partitionByNewLine: false,
partitionByNewLine: false,
groups: [],
},
],
Expand All @@ -204,6 +237,8 @@ type Example =
type: 'alphabetical',
order: 'asc',
ignoreCase: true,
partitionByNewLine: false,
partitionByComment: false,
groups: [],
},
],
Expand Down
279 changes: 168 additions & 111 deletions rules/sort-intersection-types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { SortingNode } from '../typings'

import { validateGroupsConfiguration } from '../utils/validate-groups-configuration'
import { hasPartitionComment } from '../utils/is-partition-comment'
import { getCommentsBefore } from '../utils/get-comments-before'
import { createEslintRule } from '../utils/create-eslint-rule'
import { getLinesBetween } from '../utils/get-lines-between'
import { getGroupNumber } from '../utils/get-group-number'
import { getSourceCode } from '../utils/get-source-code'
import { toSingleLine } from '../utils/to-single-line'
Expand Down Expand Up @@ -37,7 +40,9 @@ type Group =
type Options = [
Partial<{
type: 'alphabetical' | 'line-length' | 'natural'
partitionByComment: string[] | boolean | string
groups: (Group[] | Group)[]
partitionByNewLine: boolean
order: 'desc' | 'asc'
ignoreCase: boolean
}>,
Expand Down Expand Up @@ -88,6 +93,29 @@ export default createEslintRule<Options, MESSAGE_ID>({
],
},
},
partitionByComment: {
description:
'Allows you to use comments to separate the intersection types members into logical groups.',
anyOf: [
{
type: 'array',
items: {
type: 'string',
},
},
{
type: 'boolean',
},
{
type: 'string',
},
],
},
partitionByNewLine: {
description:
'Allows to use spaces to separate the nodes into logical groups.',
type: 'boolean',
},
},
additionalProperties: false,
},
Expand All @@ -104,6 +132,8 @@ export default createEslintRule<Options, MESSAGE_ID>({
type: 'alphabetical',
order: 'asc',
ignoreCase: true,
partitionByNewLine: false,
partitionByComment: false,
groups: [],
},
],
Expand All @@ -115,6 +145,8 @@ export default createEslintRule<Options, MESSAGE_ID>({
type: 'alphabetical',
ignoreCase: true,
order: 'asc',
partitionByComment: false,
partitionByNewLine: false,
groups: [],
} as const)

Expand All @@ -139,128 +171,153 @@ export default createEslintRule<Options, MESSAGE_ID>({
)

let sourceCode = getSourceCode(context)
let partitionComment = options.partitionByComment

let nodes: SortingNode[] = node.types.map(type => {
let { getGroup, defineGroup } = useGroups(options.groups)
let formattedMembers: SortingNode[][] = node.types.reduce(
(accumulator: SortingNode[][], type) => {
let { getGroup, defineGroup } = useGroups(options.groups)

switch (type.type) {
case 'TSConditionalType':
defineGroup('conditional')
break
case 'TSConstructorType':
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 'TSSymbolKeyword':
case 'TSThisType':
case 'TSUnknownKeyword':
case 'TSIntrinsicKeyword':
defineGroup('keyword')
break
case 'TSLiteralType':
case 'TSTemplateLiteralType':
defineGroup('literal')
break
case 'TSArrayType':
case 'TSIndexedAccessType':
case 'TSInferType':
case 'TSTypeReference':
case 'TSQualifiedName':
defineGroup('named')
break
case 'TSMappedType':
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':
case 'TSVoidKeyword':
defineGroup('nullish')
break
}
switch (type.type) {
case 'TSConditionalType':
defineGroup('conditional')
break
case 'TSConstructorType':
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 'TSSymbolKeyword':
case 'TSThisType':
case 'TSUnknownKeyword':
case 'TSIntrinsicKeyword':
defineGroup('keyword')
break
case 'TSLiteralType':
case 'TSTemplateLiteralType':
defineGroup('literal')
break
case 'TSArrayType':
case 'TSIndexedAccessType':
case 'TSInferType':
case 'TSTypeReference':
case 'TSQualifiedName':
defineGroup('named')
break
case 'TSMappedType':
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':
case 'TSVoidKeyword':
defineGroup('nullish')
break
}

return {
name: sourceCode.text.slice(...type.range),
size: rangeToDiff(type.range),
group: getGroup(),
node: type,
}
})
let lastSortingNode = accumulator.at(-1)?.at(-1)
let sortingNode: SortingNode = {
name: sourceCode.text.slice(...type.range),
size: rangeToDiff(type.range),
group: getGroup(),
node: type,
}
if (
(partitionComment &&
hasPartitionComment(
partitionComment,
getCommentsBefore(type, sourceCode),
)) ||
(options.partitionByNewLine &&
lastSortingNode &&
getLinesBetween(sourceCode, lastSortingNode, sortingNode))
) {
accumulator.push([])
}

pairwise(nodes, (left, right) => {
let leftNum = getGroupNumber(options.groups, left)
let rightNum = getGroupNumber(options.groups, right)
accumulator.at(-1)?.push(sortingNode)

if (
leftNum > rightNum ||
(leftNum === rightNum && isPositive(compare(left, right, options)))
) {
context.report({
messageId:
leftNum !== rightNum
? 'unexpectedIntersectionTypesGroupOrder'
: 'unexpectedIntersectionTypesOrder',
data: {
left: toSingleLine(left.name),
leftGroup: left.group,
right: toSingleLine(right.name),
rightGroup: right.group,
},
node: right.node,
fix: fixer => {
let grouped: {
[key: string]: SortingNode[]
} = {}
return accumulator
},
[[]],
)

for (let nodes of formattedMembers) {
pairwise(nodes, (left, right) => {
let leftNum = getGroupNumber(options.groups, left)
let rightNum = getGroupNumber(options.groups, right)

for (let currentNode of nodes) {
let groupNum = getGroupNumber(options.groups, currentNode)
if (
leftNum > rightNum ||
(leftNum === rightNum && isPositive(compare(left, right, options)))
) {
context.report({
messageId:
leftNum !== rightNum
? 'unexpectedIntersectionTypesGroupOrder'
: 'unexpectedIntersectionTypesOrder',
data: {
left: toSingleLine(left.name),
leftGroup: left.group,
right: toSingleLine(right.name),
rightGroup: right.group,
},
node: right.node,
fix: fixer => {
let grouped: {
[key: string]: SortingNode[]
} = {}

if (!(groupNum in grouped)) {
grouped[groupNum] = [currentNode]
} else {
grouped[groupNum] = sortNodes(
[...grouped[groupNum], currentNode],
options,
)
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[] = []
let sortedNodes: SortingNode[] = []

for (let group of Object.keys(grouped).sort(
(a, b) => Number(a) - Number(b),
)) {
sortedNodes.push(...sortNodes(grouped[group], options))
}
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)
},
})
}
})
return makeFixes(fixer, nodes, sortedNodes, sourceCode, {
partitionComment,
})
},
})
}
})
}
},
}),
})
Loading

0 comments on commit 9fe8abd

Please sign in to comment.