Skip to content

Commit 463c1d5

Browse files
committed
feat(sort-maps): adds groups, customGroups and newlinesBetween
1 parent a2b8d79 commit 463c1d5

File tree

4 files changed

+744
-26
lines changed

4 files changed

+744
-26
lines changed

rules/sort-maps.ts

Lines changed: 113 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,61 @@ import type { SortingNode } from '../types/sorting-node'
44
import type { Options } from './sort-maps/types'
55

66
import {
7+
buildCustomGroupsArrayJsonSchema,
78
partitionByCommentJsonSchema,
89
partitionByNewLineJsonSchema,
910
specialCharactersJsonSchema,
11+
newlinesBetweenJsonSchema,
1012
ignoreCaseJsonSchema,
1113
buildTypeJsonSchema,
1214
alphabetJsonSchema,
1315
localesJsonSchema,
16+
groupsJsonSchema,
1417
orderJsonSchema,
1518
} from '../utils/common-json-schemas'
19+
import { validateGeneratedGroupsConfiguration } from '../utils/validate-generated-groups-configuration'
1620
import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration'
21+
import { getCustomGroupsCompareOptions } from '../utils/get-custom-groups-compare-options'
1722
import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines'
23+
import { doesCustomGroupMatch } from './sort-maps/does-custom-group-match'
1824
import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled'
1925
import { hasPartitionComment } from '../utils/has-partition-comment'
2026
import { createNodeIndexMap } from '../utils/create-node-index-map'
27+
import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
2128
import { getCommentsBefore } from '../utils/get-comments-before'
29+
import { getNewlinesErrors } from '../utils/get-newlines-errors'
30+
import { singleCustomGroupJsonSchema } from './sort-maps/types'
2231
import { createEslintRule } from '../utils/create-eslint-rule'
2332
import { getLinesBetween } from '../utils/get-lines-between'
33+
import { getGroupNumber } from '../utils/get-group-number'
2434
import { getSourceCode } from '../utils/get-source-code'
2535
import { toSingleLine } from '../utils/to-single-line'
2636
import { rangeToDiff } from '../utils/range-to-diff'
2737
import { getSettings } from '../utils/get-settings'
2838
import { isSortable } from '../utils/is-sortable'
2939
import { makeFixes } from '../utils/make-fixes'
30-
import { sortNodes } from '../utils/sort-nodes'
40+
import { useGroups } from '../utils/use-groups'
3141
import { complete } from '../utils/complete'
3242
import { pairwise } from '../utils/pairwise'
3343

34-
type MESSAGE_ID = 'unexpectedMapElementsOrder'
44+
type MESSAGE_ID =
45+
| 'missedSpacingBetweenMapElementsMembers'
46+
| 'extraSpacingBetweenMapElementsMembers'
47+
| 'unexpectedMapElementsGroupOrder'
48+
| 'unexpectedMapElementsOrder'
3549

3650
let defaultOptions: Required<Options[0]> = {
3751
specialCharacters: 'keep',
3852
partitionByComment: false,
3953
partitionByNewLine: false,
54+
newlinesBetween: 'ignore',
4055
type: 'alphabetical',
4156
ignoreCase: true,
57+
customGroups: [],
4258
locales: 'en-US',
4359
alphabet: '',
4460
order: 'asc',
61+
groups: [],
4562
}
4663

4764
export default createEslintRule<Options, MESSAGE_ID>({
@@ -63,6 +80,12 @@ export default createEslintRule<Options, MESSAGE_ID>({
6380
let settings = getSettings(context.settings)
6481
let options = complete(context.options.at(0), settings, defaultOptions)
6582
validateCustomSortConfiguration(options)
83+
validateGeneratedGroupsConfiguration({
84+
customGroups: options.customGroups,
85+
groups: options.groups,
86+
selectors: [],
87+
modifiers: [],
88+
})
6689

6790
let sourceCode = getSourceCode(context)
6891
let eslintDisabledLines = getEslintDisabledLines({
@@ -104,12 +127,33 @@ export default createEslintRule<Options, MESSAGE_ID>({
104127
}
105128

106129
let lastSortingNode = formattedMembers.at(-1)?.at(-1)
130+
131+
let { defineGroup, getGroup } = useGroups(options)
132+
for (let customGroup of options.customGroups) {
133+
if (
134+
doesCustomGroupMatch({
135+
elementName: name,
136+
customGroup,
137+
})
138+
) {
139+
defineGroup(customGroup.groupName, true)
140+
/**
141+
* If the custom group is not referenced in the `groups` option, it
142+
* will be ignored
143+
*/
144+
if (getGroup() === customGroup.groupName) {
145+
break
146+
}
147+
}
148+
}
149+
107150
let sortingNode: SortingNode = {
108151
isEslintDisabled: isNodeEslintDisabled(
109152
element,
110153
eslintDisabledLines,
111154
),
112155
size: rangeToDiff(element, sourceCode),
156+
group: getGroup(),
113157
node: element,
114158
name,
115159
}
@@ -136,7 +180,11 @@ export default createEslintRule<Options, MESSAGE_ID>({
136180
let sortNodesExcludingEslintDisabled = (
137181
ignoreEslintDisabledNodes: boolean,
138182
): SortingNode[] =>
139-
sortNodes(nodes, options, { ignoreEslintDisabledNodes })
183+
sortNodesByGroups(nodes, options, {
184+
getGroupCompareOptions: groupNumber =>
185+
getCustomGroupsCompareOptions(options, groupNumber),
186+
ignoreEslintDisabledNodes,
187+
})
140188
let sortedNodes = sortNodesExcludingEslintDisabled(false)
141189
let sortedNodesExcludingEslintDisabled =
142190
sortNodesExcludingEslintDisabled(true)
@@ -147,31 +195,59 @@ export default createEslintRule<Options, MESSAGE_ID>({
147195
let leftIndex = nodeIndexMap.get(left)!
148196
let rightIndex = nodeIndexMap.get(right)!
149197

198+
let leftNumber = getGroupNumber(options.groups, left)
199+
let rightNumber = getGroupNumber(options.groups, right)
200+
150201
let indexOfRightExcludingEslintDisabled =
151202
sortedNodesExcludingEslintDisabled.indexOf(right)
203+
204+
let messageIds: MESSAGE_ID[] = []
205+
152206
if (
153-
leftIndex < rightIndex &&
154-
leftIndex < indexOfRightExcludingEslintDisabled
207+
leftIndex > rightIndex ||
208+
leftIndex >= indexOfRightExcludingEslintDisabled
155209
) {
156-
return
210+
messageIds.push(
211+
leftNumber === rightNumber
212+
? 'unexpectedMapElementsOrder'
213+
: 'unexpectedMapElementsGroupOrder',
214+
)
157215
}
158216

159-
context.report({
160-
fix: fixer =>
161-
makeFixes({
162-
sortedNodes: sortedNodesExcludingEslintDisabled,
163-
sourceCode,
164-
options,
165-
fixer,
166-
nodes,
167-
}),
168-
data: {
169-
right: toSingleLine(right.name),
170-
left: toSingleLine(left.name),
171-
},
172-
messageId: 'unexpectedMapElementsOrder',
173-
node: right.node,
174-
})
217+
messageIds = [
218+
...messageIds,
219+
...getNewlinesErrors({
220+
missedSpacingError: 'missedSpacingBetweenMapElementsMembers',
221+
extraSpacingError: 'extraSpacingBetweenMapElementsMembers',
222+
rightNum: rightNumber,
223+
leftNum: leftNumber,
224+
sourceCode,
225+
options,
226+
right,
227+
left,
228+
}),
229+
]
230+
231+
for (let messageId of messageIds) {
232+
context.report({
233+
fix: fixer =>
234+
makeFixes({
235+
sortedNodes: sortedNodesExcludingEslintDisabled,
236+
sourceCode,
237+
options,
238+
fixer,
239+
nodes,
240+
}),
241+
data: {
242+
right: toSingleLine(right.name),
243+
left: toSingleLine(left.name),
244+
rightGroup: right.group,
245+
leftGroup: left.group,
246+
},
247+
node: right.node,
248+
messageId,
249+
})
250+
}
175251
})
176252
}
177253
}
@@ -186,27 +262,38 @@ export default createEslintRule<Options, MESSAGE_ID>({
186262
description:
187263
'Allows you to use comments to separate the maps members into logical groups.',
188264
},
265+
customGroups: buildCustomGroupsArrayJsonSchema({
266+
singleCustomGroupJsonSchema,
267+
}),
189268
partitionByNewLine: partitionByNewLineJsonSchema,
190269
specialCharacters: specialCharactersJsonSchema,
270+
newlinesBetween: newlinesBetweenJsonSchema,
191271
ignoreCase: ignoreCaseJsonSchema,
192272
alphabet: alphabetJsonSchema,
193273
type: buildTypeJsonSchema(),
194274
locales: localesJsonSchema,
275+
groups: groupsJsonSchema,
195276
order: orderJsonSchema,
196277
},
197278
additionalProperties: false,
198279
type: 'object',
199280
},
200281
],
282+
messages: {
283+
unexpectedMapElementsGroupOrder:
284+
'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).',
285+
missedSpacingBetweenMapElementsMembers:
286+
'Missed spacing between "{{left}}" and "{{right}}" members.',
287+
extraSpacingBetweenMapElementsMembers:
288+
'Extra spacing between "{{left}}" and "{{right}}" members.',
289+
unexpectedMapElementsOrder:
290+
'Expected "{{right}}" to come before "{{left}}".',
291+
},
201292
docs: {
202293
url: 'https://perfectionist.dev/rules/sort-maps',
203294
description: 'Enforce sorted Map elements.',
204295
recommended: true,
205296
},
206-
messages: {
207-
unexpectedMapElementsOrder:
208-
'Expected "{{right}}" to come before "{{left}}".',
209-
},
210297
type: 'suggestion',
211298
fixable: 'code',
212299
},
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { SingleCustomGroup, AnyOfCustomGroup } from './types'
2+
3+
import { matches } from '../../utils/matches'
4+
5+
interface DoesCustomGroupMatchProps {
6+
customGroup: SingleCustomGroup | AnyOfCustomGroup
7+
elementName: string
8+
}
9+
10+
export let doesCustomGroupMatch = (
11+
props: DoesCustomGroupMatchProps,
12+
): boolean => {
13+
if ('anyOf' in props.customGroup) {
14+
// At least one subgroup must match
15+
return props.customGroup.anyOf.some(subgroup =>
16+
doesCustomGroupMatch({ ...props, customGroup: subgroup }),
17+
)
18+
}
19+
20+
if (
21+
'elementNamePattern' in props.customGroup &&
22+
props.customGroup.elementNamePattern
23+
) {
24+
let matchesElementNamePattern: boolean = matches(
25+
props.elementName,
26+
props.customGroup.elementNamePattern,
27+
)
28+
if (!matchesElementNamePattern) {
29+
return false
30+
}
31+
}
32+
33+
return true
34+
}

rules/sort-maps/types.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'
2+
3+
import { elementNamePatternJsonSchema } from '../../utils/common-json-schemas'
4+
15
export type Options = Partial<{
26
partitionByComment:
37
| {
@@ -7,11 +11,45 @@ export type Options = Partial<{
711
| string[]
812
| boolean
913
| string
14+
groups: (
15+
| { newlinesBetween: 'ignore' | 'always' | 'never' }
16+
| Group[]
17+
| Group
18+
)[]
1019
type: 'alphabetical' | 'line-length' | 'natural' | 'custom'
20+
newlinesBetween: 'ignore' | 'always' | 'never'
1121
specialCharacters: 'remove' | 'trim' | 'keep'
1222
locales: NonNullable<Intl.LocalesArgument>
1323
partitionByNewLine: boolean
24+
customGroups: CustomGroup[]
1425
order: 'desc' | 'asc'
1526
ignoreCase: boolean
1627
alphabet: string
1728
}>[]
29+
30+
export interface SingleCustomGroup {
31+
elementNamePattern?: string
32+
}
33+
34+
export interface AnyOfCustomGroup {
35+
anyOf: SingleCustomGroup[]
36+
}
37+
38+
type CustomGroup = (
39+
| {
40+
order?: Options[0]['order']
41+
type?: Options[0]['type']
42+
}
43+
| {
44+
type?: 'unsorted'
45+
}
46+
) &
47+
(SingleCustomGroup | AnyOfCustomGroup) & {
48+
groupName: string
49+
}
50+
51+
type Group = 'unknown' | string
52+
53+
export let singleCustomGroupJsonSchema: Record<string, JSONSchema4> = {
54+
elementNamePattern: elementNamePatternJsonSchema,
55+
}

0 commit comments

Comments
 (0)