Skip to content

Commit 96cfcd8

Browse files
committed
feat: add group kind option in sort-object-types
1 parent 721e1ee commit 96cfcd8

File tree

3 files changed

+568
-80
lines changed

3 files changed

+568
-80
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,15 @@ type User = {
150150

151151
In this example, the `partitionByNewLine` option will cause the rule to treat each group of members (separated by empty lines) independently, preserving their order within each group.
152152

153+
### groupKind
154+
155+
<sub>default: `'mixed'`</sub>
156+
157+
Allows you to group type object keys by their kind, determining whether required values should come before or after optional values.
158+
159+
- `mixed` — Do not group object keys by their kind; required values are sorted together optional values.
160+
- `required-first` — Group all required values before optional.
161+
- `optional-first` — Group all optional values before required.
153162

154163
### groups
155164

rules/sort-object-types.ts

Lines changed: 160 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { TSESTree } from '@typescript-eslint/types'
2+
13
import type { SortingNode } from '../typings'
24

35
import { createEslintRule } from '../utils/create-eslint-rule'
@@ -20,6 +22,7 @@ type Group<T extends string[]> = 'multiline' | 'unknown' | T[number]
2022

2123
type Options<T extends string[]> = [
2224
Partial<{
25+
groupKind: 'required-first' | 'optional-first' | 'mixed'
2326
type: 'alphabetical' | 'line-length' | 'natural'
2427
groups: (Group<T>[] | Group<T>)[]
2528
partitionByNewLine: boolean
@@ -62,6 +65,11 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
6265
'Allows to use spaces to separate the nodes into logical groups.',
6366
type: 'boolean',
6467
},
68+
groupKind: {
69+
description: 'Specifies top-level groups.',
70+
type: 'string',
71+
enum: ['mixed', 'required-first', 'optional-first'],
72+
},
6573
groups: {
6674
description: 'Specifies the order of the groups.',
6775
type: 'array',
@@ -111,6 +119,7 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
111119
order: 'asc',
112120
ignoreCase: true,
113121
partitionByNewLine: false,
122+
groupKind: 'mixed',
114123
groups: [],
115124
customGroups: {},
116125
},
@@ -121,6 +130,7 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
121130
let options = complete(context.options.at(0), {
122131
partitionByNewLine: false,
123132
type: 'alphabetical',
133+
groupKind: 'mixed',
124134
ignoreCase: true,
125135
customGroups: {},
126136
order: 'asc',
@@ -129,89 +139,133 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
129139

130140
let sourceCode = getSourceCode(context)
131141

132-
let formattedMembers: SortingNode[][] = node.members.reduce(
133-
(accumulator: SortingNode[][], member) => {
134-
let name: string
135-
let raw = sourceCode.text.slice(
136-
member.range.at(0),
137-
member.range.at(1),
138-
)
139-
let lastMember = accumulator.at(-1)?.at(-1)
142+
let formattedMembers: SortingNode<TSESTree.TypeElement>[][] =
143+
node.members.reduce(
144+
(accumulator: SortingNode<TSESTree.TypeElement>[][], member) => {
145+
let name: string
146+
let raw = sourceCode.text.slice(
147+
member.range.at(0),
148+
member.range.at(1),
149+
)
150+
let lastMember = accumulator.at(-1)?.at(-1)
151+
152+
let { getGroup, defineGroup, setCustomGroups } = useGroups(
153+
options.groups,
154+
)
140155

141-
let { getGroup, defineGroup, setCustomGroups } = useGroups(
142-
options.groups,
143-
)
156+
let formatName = (value: string): string =>
157+
value.replace(/(,|;)$/, '')
144158

145-
let formatName = (value: string): string =>
146-
value.replace(/(,|;)$/, '')
159+
if (member.type === 'TSPropertySignature') {
160+
if (member.key.type === 'Identifier') {
161+
;({ name } = member.key)
162+
} else if (member.key.type === 'Literal') {
163+
name = `${member.key.value}`
164+
} else {
165+
name = sourceCode.text.slice(
166+
member.range.at(0),
167+
member.typeAnnotation?.range.at(0),
168+
)
169+
}
170+
} else if (member.type === 'TSIndexSignature') {
171+
let endIndex: number =
172+
member.typeAnnotation?.range.at(0) ?? member.range.at(1)!
147173

148-
if (member.type === 'TSPropertySignature') {
149-
if (member.key.type === 'Identifier') {
150-
;({ name } = member.key)
151-
} else if (member.key.type === 'Literal') {
152-
name = `${member.key.value}`
174+
name = formatName(
175+
sourceCode.text.slice(member.range.at(0), endIndex),
176+
)
153177
} else {
154-
name = sourceCode.text.slice(
155-
member.range.at(0),
156-
member.typeAnnotation?.range.at(0),
178+
name = formatName(
179+
sourceCode.text.slice(member.range.at(0), member.range.at(1)),
157180
)
158181
}
159-
} else if (member.type === 'TSIndexSignature') {
160-
let endIndex: number =
161-
member.typeAnnotation?.range.at(0) ?? member.range.at(1)!
162-
163-
name = formatName(
164-
sourceCode.text.slice(member.range.at(0), endIndex),
165-
)
166-
} else {
167-
name = formatName(
168-
sourceCode.text.slice(member.range.at(0), member.range.at(1)),
169-
)
170-
}
171182

172-
setCustomGroups(options.customGroups, name)
183+
setCustomGroups(options.customGroups, name)
173184

174-
if (member.loc.start.line !== member.loc.end.line) {
175-
defineGroup('multiline')
176-
}
185+
if (member.loc.start.line !== member.loc.end.line) {
186+
defineGroup('multiline')
187+
}
177188

178-
let endsWithComma = raw.endsWith(';') || raw.endsWith(',')
179-
let endSize = endsWithComma ? 1 : 0
189+
let endsWithComma = raw.endsWith(';') || raw.endsWith(',')
190+
let endSize = endsWithComma ? 1 : 0
180191

181-
let memberSortingNode = {
182-
size: rangeToDiff(member.range) - endSize,
183-
node: member,
184-
name,
185-
}
192+
let memberSortingNode = {
193+
size: rangeToDiff(member.range) - endSize,
194+
node: member,
195+
name,
196+
}
186197

187-
if (
188-
options.partitionByNewLine &&
189-
lastMember &&
190-
getLinesBetween(sourceCode, lastMember, memberSortingNode)
191-
) {
192-
accumulator.push([])
193-
}
198+
if (
199+
options.partitionByNewLine &&
200+
lastMember &&
201+
getLinesBetween(sourceCode, lastMember, memberSortingNode)
202+
) {
203+
accumulator.push([])
204+
}
194205

195-
accumulator.at(-1)?.push({
196-
...memberSortingNode,
197-
group: getGroup(),
198-
})
206+
accumulator.at(-1)?.push({
207+
...memberSortingNode,
208+
group: getGroup(),
209+
})
199210

200-
return accumulator
201-
},
202-
[[]],
203-
)
211+
return accumulator
212+
},
213+
[[]],
214+
)
204215

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

221+
let getIsOptionalValue = (nodeValue: TSESTree.TypeElement) => {
222+
if (
223+
nodeValue.type === 'TSCallSignatureDeclaration' ||
224+
nodeValue.type === 'TSConstructSignatureDeclaration' ||
225+
nodeValue.type === 'TSIndexSignature'
226+
) {
227+
return false
228+
}
229+
return nodeValue.optional
230+
}
231+
232+
let isLeftOptional = getIsOptionalValue(left.node)
233+
let isRightOptional = getIsOptionalValue(right.node)
234+
235+
let compareValue
210236
if (
211-
leftNum > rightNum ||
212-
(leftNum === rightNum &&
213-
isPositive(compare(left, right, options)))
237+
options.groupKind === 'optional-first' &&
238+
isLeftOptional &&
239+
!isRightOptional
240+
) {
241+
compareValue = false
242+
} else if (
243+
options.groupKind === 'optional-first' &&
244+
!isLeftOptional &&
245+
isRightOptional
246+
) {
247+
compareValue = true
248+
} else if (
249+
options.groupKind === 'required-first' &&
250+
!isLeftOptional &&
251+
isRightOptional
252+
) {
253+
compareValue = false
254+
} else if (
255+
options.groupKind === 'required-first' &&
256+
isLeftOptional &&
257+
!isRightOptional
214258
) {
259+
compareValue = true
260+
} else if (leftNum > rightNum) {
261+
compareValue = true
262+
} else if (leftNum === rightNum) {
263+
compareValue = isPositive(compare(left, right, options))
264+
} else {
265+
compareValue = false
266+
}
267+
268+
if (compareValue) {
215269
context.report({
216270
messageId: 'unexpectedObjectTypesOrder',
217271
data: {
@@ -220,29 +274,55 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
220274
},
221275
node: right.node,
222276
fix: fixer => {
223-
let grouped: {
224-
[key: string]: SortingNode[]
225-
} = {}
277+
let groupedByKind
278+
if (options.groupKind !== 'mixed') {
279+
groupedByKind = nodes.reduce<
280+
SortingNode<TSESTree.TypeElement>[][]
281+
>(
282+
(accumulator, currentNode) => {
283+
let requiredIndex =
284+
options.groupKind === 'required-first' ? 0 : 1
285+
let optionalIndex =
286+
options.groupKind === 'required-first' ? 1 : 0
226287

227-
for (let currentNode of nodes) {
228-
let groupNum = getGroupNumber(options.groups, currentNode)
229-
230-
if (!(groupNum in grouped)) {
231-
grouped[groupNum] = [currentNode]
232-
} else {
233-
grouped[groupNum] = sortNodes(
234-
[...grouped[groupNum], currentNode],
235-
options,
236-
)
237-
}
288+
if (getIsOptionalValue(currentNode.node)) {
289+
accumulator[optionalIndex].push(currentNode)
290+
} else {
291+
accumulator[requiredIndex].push(currentNode)
292+
}
293+
return accumulator
294+
},
295+
[[], []],
296+
)
297+
} else {
298+
groupedByKind = [nodes]
238299
}
239300

240301
let sortedNodes: SortingNode[] = []
241302

242-
for (let group of Object.keys(grouped).sort(
243-
(a, b) => Number(a) - Number(b),
244-
)) {
245-
sortedNodes.push(...sortNodes(grouped[group], options))
303+
for (let nodesByKind of groupedByKind) {
304+
let grouped: {
305+
[key: string]: SortingNode[]
306+
} = {}
307+
308+
for (let currentNode of nodesByKind) {
309+
let groupNum = getGroupNumber(options.groups, currentNode)
310+
311+
if (!(groupNum in grouped)) {
312+
grouped[groupNum] = [currentNode]
313+
} else {
314+
grouped[groupNum] = sortNodes(
315+
[...grouped[groupNum], currentNode],
316+
options,
317+
)
318+
}
319+
}
320+
321+
for (let group of Object.keys(grouped).sort(
322+
(a, b) => Number(a) - Number(b),
323+
)) {
324+
sortedNodes.push(...sortNodes(grouped[group], options))
325+
}
246326
}
247327

248328
return makeFixes(fixer, nodes, sortedNodes, sourceCode)

0 commit comments

Comments
 (0)