Skip to content

Commit

Permalink
feat: add partition by new line option to sort-object-typea rule
Browse files Browse the repository at this point in the history
  • Loading branch information
azat-io committed Nov 11, 2023
1 parent 48532ef commit 563c815
Show file tree
Hide file tree
Showing 3 changed files with 337 additions and 81 deletions.
9 changes: 8 additions & 1 deletion docs/rules/sort-object-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ interface Options {
order?: 'asc' | 'desc'
'ignore-case'?: boolean
groups?: (Group | Group[])[]
'custom-groups': { [key: Group]: string[] | string }
'custom-groups'?: { [key: Group]: string[] | string }
'partition-by-new-line'?: boolean
}
```

Expand Down Expand Up @@ -128,6 +129,12 @@ Example:
}
```

### partition-by-new-line

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

When `true`, does not sort the object type's members if there is an empty string between them.

## ⚙️ Usage

::: code-group
Expand Down
188 changes: 108 additions & 80 deletions rules/sort-object-types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { SortingNode } from '../typings'

import { createEslintRule } from '../utils/create-eslint-rule'
import { getLinesBetween } from '../utils/get-lines-between'
import { getGroupNumber } from '../utils/get-group-number'
import { toSingleLine } from '../utils/to-single-line'
import { rangeToDiff } from '../utils/range-to-diff'
Expand All @@ -20,6 +21,7 @@ type Group<T extends string[]> = 'multiline' | 'unknown' | T[number]
type Options<T extends string[]> = [
Partial<{
groups: (Group<T>[] | Group<T>)[]
'partition-by-new-line': boolean
'ignore-case': boolean
'custom-groups': {}
order: SortOrder
Expand Down Expand Up @@ -66,6 +68,10 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
type: 'array',
default: [],
},
'partition-by-new-line': {
type: 'boolean',
default: false,
},
},
additionalProperties: false,
},
Expand All @@ -85,6 +91,7 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
TSTypeLiteral: node => {
if (node.members.length > 1) {
let options = complete(context.options.at(0), {
'partition-by-new-line': false,
type: SortType.alphabetical,
'ignore-case': false,
order: SortOrder.asc,
Expand All @@ -94,100 +101,121 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({

let source = context.getSourceCode()

let nodes: SortingNode[] = node.members.map(member => {
let name: string
let raw = source.text.slice(member.range.at(0), member.range.at(1))
let formattedMembers: SortingNode[][] = node.members.reduce(
(accumulator: SortingNode[][], member) => {
let name: string
let raw = source.text.slice(member.range.at(0), member.range.at(1))
let lastMember = accumulator.at(-1)?.at(-1)

let { getGroup, defineGroup, setCustomGroups } = useGroups(
options.groups,
)

let formatName = (value: string): string =>
value.replace(/(,|;)$/, '')
let { getGroup, defineGroup, setCustomGroups } = useGroups(
options.groups,
)

if (member.type === 'TSPropertySignature') {
if (member.key.type === 'Identifier') {
;({ name } = member.key)
} else if (member.key.type === 'Literal') {
name = `${member.key.value}`
let formatName = (value: string): string =>
value.replace(/(,|;)$/, '')

if (member.type === 'TSPropertySignature') {
if (member.key.type === 'Identifier') {
;({ name } = member.key)
} else if (member.key.type === 'Literal') {
name = `${member.key.value}`
} else {
name = source.text.slice(
member.range.at(0),
member.typeAnnotation?.range.at(0),
)
}
} else if (member.type === 'TSIndexSignature') {
let endIndex: number =
member.typeAnnotation?.range.at(0) ?? member.range.at(1)!

name = formatName(source.text.slice(member.range.at(0), endIndex))
} else {
name = source.text.slice(
member.range.at(0),
member.typeAnnotation?.range.at(0),
name = formatName(
source.text.slice(member.range.at(0), member.range.at(1)),
)
}
} else if (member.type === 'TSIndexSignature') {
let endIndex: number =
member.typeAnnotation?.range.at(0) ?? member.range.at(1)!

name = formatName(source.text.slice(member.range.at(0), endIndex))
} else {
name = formatName(
source.text.slice(member.range.at(0), member.range.at(1)),
)
}

setCustomGroups(options['custom-groups'], name)
setCustomGroups(options['custom-groups'], name)

if (member.loc.start.line !== member.loc.end.line) {
defineGroup('multiline')
}
if (member.loc.start.line !== member.loc.end.line) {
defineGroup('multiline')
}

let endsWithComma = raw.endsWith(';') || raw.endsWith(',')
let endSize = endsWithComma ? 1 : 0
let endsWithComma = raw.endsWith(';') || raw.endsWith(',')
let endSize = endsWithComma ? 1 : 0

return {
size: rangeToDiff(member.range) - endSize,
group: getGroup(),
node: member,
name,
}
})
let memberSortingNode = {
size: rangeToDiff(member.range) - endSize,
node: member,
name,
}

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

if (
leftNum > rightNum ||
(leftNum === rightNum && isPositive(compare(left, right, options)))
) {
context.report({
messageId: 'unexpectedObjectTypesOrder',
data: {
left: toSingleLine(left.name),
right: toSingleLine(right.name),
},
node: right.node,
fix: fixer => {
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,
)
if (
options['partition-by-new-line'] &&
lastMember &&
getLinesBetween(source, lastMember, memberSortingNode)
) {
accumulator.push([])
}

accumulator.at(-1)?.push({
...memberSortingNode,
group: getGroup(),
})

return accumulator
},
[[]],
)

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

if (
leftNum > rightNum ||
(leftNum === rightNum &&
isPositive(compare(left, right, options)))
) {
context.report({
messageId: 'unexpectedObjectTypesOrder',
data: {
left: toSingleLine(left.name),
right: toSingleLine(right.name),
},
node: right.node,
fix: fixer => {
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[] = []
let sortedNodes: SortingNode[] = []

for (let group of Object.keys(grouped).sort()) {
sortedNodes.push(...sortNodes(grouped[group], options))
}
for (let group of Object.keys(grouped).sort()) {
sortedNodes.push(...sortNodes(grouped[group], options))
}

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

0 comments on commit 563c815

Please sign in to comment.