Skip to content

Commit f581714

Browse files
authoredJan 31, 2025··
fix(sort-classes): fix new lines inside always for signature overloads
1 parent 266910b commit f581714

7 files changed

+237
-58
lines changed
 

‎rules/sort-classes.ts

+34-9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
Selector,
77
} from './sort-classes/types'
88
import type { SortingNodeWithDependencies } from '../utils/sort-nodes-by-dependencies'
9+
import type { NewlinesBetweenOption } from '../types/common-options'
910

1011
import {
1112
buildCustomGroupsArrayJsonSchema,
@@ -101,6 +102,11 @@ let defaultOptions: Required<SortClassesOptions[0]> = {
101102
order: 'asc',
102103
}
103104

105+
interface SortClassSortingNodes
106+
extends SortingNodeWithDependencies<TSESTree.ClassElement> {
107+
overloadSignaturesGroupId: number | null
108+
}
109+
104110
export default createEslintRule<SortClassesOptions, MESSAGE_ID>({
105111
create: context => ({
106112
ClassBody: node => {
@@ -287,8 +293,8 @@ export default createEslintRule<SortClassesOptions, MESSAGE_ID>({
287293
return dependencies
288294
}
289295
let overloadSignatureGroups = getOverloadSignatureGroups(node.body)
290-
let formattedNodes: SortingNodeWithDependencies[][] = node.body.reduce(
291-
(accumulator: SortingNodeWithDependencies[][], member) => {
296+
let formattedNodes: SortClassSortingNodes[][] = node.body.reduce(
297+
(accumulator: SortClassSortingNodes[][], member) => {
292298
let name: string
293299
let dependencies: string[] = []
294300
let { defineGroup, getGroup } = useGroups(options)
@@ -553,26 +559,32 @@ export default createEslintRule<SortClassesOptions, MESSAGE_ID>({
553559
* It is unclear what should be considered the size of an overload
554560
* signature group. Take the size of the implementation by default.
555561
*/
556-
let overloadSignatureGroupMember = overloadSignatureGroups
557-
.find(overloadSignatures => overloadSignatures.includes(member))
558-
?.at(-1)
562+
let overloadSignatureGroupMemberIndex =
563+
overloadSignatureGroups.findIndex(overloadSignatures =>
564+
overloadSignatures.includes(member),
565+
)
566+
let overloadSignatureGroupMember =
567+
overloadSignatureGroups[overloadSignatureGroupMemberIndex]?.at(-1)
559568

560-
let sortingNode: SortingNodeWithDependencies = {
569+
let sortingNode: SortClassSortingNodes = {
561570
dependencyName: getDependencyName({
562571
nodeNameWithoutStartingHash: name.startsWith('#')
563572
? name.slice(1)
564573
: name,
565574
isStatic: modifiers.includes('static'),
566575
isPrivateHash,
567576
}),
577+
overloadSignaturesGroupId:
578+
overloadSignatureGroupMemberIndex === -1
579+
? null
580+
: overloadSignatureGroupMemberIndex,
568581
size: overloadSignatureGroupMember
569582
? rangeToDiff(overloadSignatureGroupMember, sourceCode)
570583
: rangeToDiff(member, sourceCode),
571584
isEslintDisabled: isNodeEslintDisabled(member, eslintDisabledLines),
572585
addSafetySemicolonWhenInline,
573586
group: getGroup(),
574587
node: member,
575-
576588
dependencies,
577589
name,
578590
}
@@ -599,7 +611,7 @@ export default createEslintRule<SortClassesOptions, MESSAGE_ID>({
599611

600612
let sortNodesExcludingEslintDisabled = (
601613
ignoreEslintDisabledNodes: boolean,
602-
): SortingNodeWithDependencies[] =>
614+
): SortClassSortingNodes[] =>
603615
sortNodesByDependencies(
604616
formattedNodes.flatMap(nodes =>
605617
sortNodesByGroups(nodes, options, {
@@ -618,7 +630,20 @@ export default createEslintRule<SortClassesOptions, MESSAGE_ID>({
618630

619631
let nodes = formattedNodes.flat()
620632

621-
reportAllErrors<MESSAGE_ID>({
633+
reportAllErrors<MESSAGE_ID, SortClassSortingNodes>({
634+
newlinesBetweenValueGetter: ({
635+
computedNewlinesBetween,
636+
right,
637+
left,
638+
}): NewlinesBetweenOption => {
639+
if (
640+
left.overloadSignaturesGroupId !== null &&
641+
left.overloadSignaturesGroupId === right.overloadSignaturesGroupId
642+
) {
643+
return 'never'
644+
}
645+
return computedNewlinesBetween
646+
},
622647
availableMessageIds: {
623648
missedSpacingBetweenMembers: 'missedSpacingBetweenClassMembers',
624649
extraSpacingBetweenMembers: 'extraSpacingBetweenClassMembers',

‎test/rules/sort-classes.test.ts

+109-2
Original file line numberDiff line numberDiff line change
@@ -4434,6 +4434,57 @@ describe(ruleName, () => {
44344434
},
44354435
)
44364436

4437+
for (let newlinesBetween of ['always', 'ignore', 'never'] as const) {
4438+
ruleTester.run(
4439+
`${ruleName}: enforces no newline between overload signatures when newlinesBetween is "${newlinesBetween}"`,
4440+
rule,
4441+
{
4442+
invalid: [
4443+
{
4444+
errors: [
4445+
{
4446+
data: {
4447+
right: 'method',
4448+
left: 'method',
4449+
},
4450+
messageId: 'extraSpacingBetweenClassMembers',
4451+
},
4452+
{
4453+
data: {
4454+
right: 'method',
4455+
left: 'method',
4456+
},
4457+
messageId: 'extraSpacingBetweenClassMembers',
4458+
},
4459+
],
4460+
output: dedent`
4461+
class Class {
4462+
method(a: string): void {}
4463+
method(a: number): void {}
4464+
method(a: string | number): void {}
4465+
}
4466+
`,
4467+
code: dedent`
4468+
class Class {
4469+
method(a: string): void {}
4470+
4471+
method(a: number): void {}
4472+
4473+
method(a: string | number): void {}
4474+
}
4475+
`,
4476+
options: [
4477+
{
4478+
newlinesBetween,
4479+
},
4480+
],
4481+
},
4482+
],
4483+
valid: [],
4484+
},
4485+
)
4486+
}
4487+
44374488
describe(`${ruleName}(${type}): "newlinesBetween" inside groups`, () => {
44384489
ruleTester.run(
44394490
`${ruleName}(${type}): handles "newlinesBetween" between consecutive groups`,
@@ -6841,14 +6892,12 @@ describe(ruleName, () => {
68416892
static setBackground(r: number, g: number, b: number, a?: number): this
68426893
static setBackground(color: number, hexFlag: boolean): this
68436894
static setBackground(color: Color | string | CSSColor): this
6844-
68456895
static setBackground(color: ColorArgument, arg1?: boolean | number, arg2?: number, arg3?: number): this {
68466896
/* ... */
68476897
}
68486898
setBackground(r: number, g: number, b: number, a?: number): this
68496899
setBackground(color: number, hexFlag: boolean): this
68506900
setBackground(color: Color | string | CSSColor): this
6851-
68526901
setBackground(color: ColorArgument, arg1?: boolean | number, arg2?: number, arg3?: number): this {
68536902
/* ... */
68546903
}
@@ -8181,6 +8230,64 @@ describe(ruleName, () => {
81818230
],
81828231
valid: [],
81838232
})
8233+
8234+
for (let newlinesInside of ['always', 'never'] as const) {
8235+
ruleTester.run(
8236+
`${ruleName}: enforces no newline between overload signatures when newlinesBetween is "${newlinesInside}"`,
8237+
rule,
8238+
{
8239+
invalid: [
8240+
{
8241+
errors: [
8242+
{
8243+
data: {
8244+
right: 'method',
8245+
left: 'method',
8246+
},
8247+
messageId: 'extraSpacingBetweenClassMembers',
8248+
},
8249+
{
8250+
data: {
8251+
right: 'method',
8252+
left: 'method',
8253+
},
8254+
messageId: 'extraSpacingBetweenClassMembers',
8255+
},
8256+
],
8257+
options: [
8258+
{
8259+
customGroups: [
8260+
{
8261+
groupName: 'methods',
8262+
selector: 'method',
8263+
newlinesInside,
8264+
},
8265+
],
8266+
groups: ['methods'],
8267+
},
8268+
],
8269+
output: dedent`
8270+
class Class {
8271+
method(a: string): void {}
8272+
method(a: number): void {}
8273+
method(a: string | number): void {}
8274+
}
8275+
`,
8276+
code: dedent`
8277+
class Class {
8278+
method(a: string): void {}
8279+
8280+
method(a: number): void {}
8281+
8282+
method(a: string | number): void {}
8283+
}
8284+
`,
8285+
},
8286+
],
8287+
valid: [],
8288+
},
8289+
)
8290+
}
81848291
})
81858292
})
81868293

‎utils/get-newlines-errors.ts

+27-7
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,36 @@ import type { SortingNode } from '../types/sorting-node'
1111
import { getNewlinesBetweenOption } from './get-newlines-between-option'
1212
import { getLinesBetween } from './get-lines-between'
1313

14-
interface GetNewlinesErrorsParameters<T extends string> {
14+
export type NewlinesBetweenValueGetter<T extends SortingNode> = (props: {
15+
computedNewlinesBetween: NewlinesBetweenOption
16+
right: T
17+
left: T
18+
}) => NewlinesBetweenOption
19+
20+
interface GetNewlinesErrorsParameters<
21+
MessageIds extends string,
22+
T extends SortingNode,
23+
> {
1524
options: {
1625
customGroups?: DeprecatedCustomGroupsOption | CustomGroupsOption
1726
newlinesBetween: NewlinesBetweenOption
1827
groups: GroupsOptions<string>
1928
}
29+
newlinesBetweenValueGetter?: NewlinesBetweenValueGetter<T>
2030
sourceCode: TSESLint.SourceCode
21-
missedSpacingError: T
22-
extraSpacingError: T
23-
right: SortingNode
24-
left: SortingNode
31+
missedSpacingError: MessageIds
32+
extraSpacingError: MessageIds
2533
rightNum: number
2634
leftNum: number
35+
right: T
36+
left: T
2737
}
2838

29-
export let getNewlinesErrors = <T extends string>({
39+
export let getNewlinesErrors = <
40+
MessageIds extends string,
41+
T extends SortingNode,
42+
>({
43+
newlinesBetweenValueGetter,
3044
missedSpacingError,
3145
extraSpacingError,
3246
sourceCode,
@@ -35,12 +49,18 @@ export let getNewlinesErrors = <T extends string>({
3549
options,
3650
right,
3751
left,
38-
}: GetNewlinesErrorsParameters<T>): T[] => {
52+
}: GetNewlinesErrorsParameters<MessageIds, T>): MessageIds[] => {
3953
let newlinesBetween = getNewlinesBetweenOption({
4054
nextSortingNode: right,
4155
sortingNode: left,
4256
options,
4357
})
58+
newlinesBetween =
59+
newlinesBetweenValueGetter?.({
60+
computedNewlinesBetween: newlinesBetween,
61+
right,
62+
left,
63+
}) ?? newlinesBetween
4464
if (leftNum > rightNum) {
4565
return []
4666
}

‎utils/make-fixes.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,37 @@ import type {
77
CustomGroupsOption,
88
GroupsOptions,
99
} from '../types/common-options'
10+
import type { NewlinesBetweenValueGetter } from './get-newlines-errors'
1011
import type { SortingNode } from '../types/sorting-node'
1112

1213
import { makeCommentAfterFixes } from './make-comment-after-fixes'
1314
import { makeNewlinesFixes } from './make-newlines-fixes'
1415
import { makeOrderFixes } from './make-order-fixes'
1516

16-
export interface MakeFixesParameters {
17+
export interface MakeFixesParameters<T extends SortingNode> {
1718
options?: {
1819
customGroups?: DeprecatedCustomGroupsOption | CustomGroupsOption
1920
partitionByComment?: PartitionByCommentOption
2021
newlinesBetween?: NewlinesBetweenOption
2122
groups?: GroupsOptions<string>
2223
}
24+
newlinesBetweenValueGetter?: NewlinesBetweenValueGetter<T>
2325
ignoreFirstNodeHighestBlockComment?: boolean
2426
sourceCode: TSESLint.SourceCode
25-
sortedNodes: SortingNode[]
2627
fixer: TSESLint.RuleFixer
27-
nodes: SortingNode[]
28+
sortedNodes: T[]
29+
nodes: T[]
2830
}
2931

30-
export let makeFixes = ({
32+
export let makeFixes = <T extends SortingNode>({
3133
ignoreFirstNodeHighestBlockComment,
34+
newlinesBetweenValueGetter,
3235
sortedNodes,
3336
sourceCode,
3437
options,
3538
fixer,
3639
nodes,
37-
}: MakeFixesParameters): TSESLint.RuleFix[] => {
40+
}: MakeFixesParameters<T>): TSESLint.RuleFix[] => {
3841
let orderFixes = makeOrderFixes({
3942
ignoreFirstNodeHighestBlockComment,
4043
sortedNodes,
@@ -64,6 +67,7 @@ export let makeFixes = ({
6467
newlinesBetween: options.newlinesBetween,
6568
groups: options.groups,
6669
},
70+
newlinesBetweenValueGetter,
6771
sortedNodes,
6872
sourceCode,
6973
fixer,

‎utils/make-newlines-fixes.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,34 @@ import type {
66
CustomGroupsOption,
77
GroupsOptions,
88
} from '../types/common-options'
9+
import type { NewlinesBetweenValueGetter } from './get-newlines-errors'
910
import type { SortingNode } from '../types/sorting-node'
1011

1112
import { getNewlinesBetweenOption } from './get-newlines-between-option'
1213
import { getLinesBetween } from './get-lines-between'
1314
import { getNodeRange } from './get-node-range'
1415

15-
interface MakeNewlinesFixesParameters {
16+
interface MakeNewlinesFixesParameters<T extends SortingNode> {
1617
options: {
1718
customGroups?: DeprecatedCustomGroupsOption | CustomGroupsOption
1819
newlinesBetween: NewlinesBetweenOption
1920
groups: GroupsOptions<string>
2021
}
22+
newlinesBetweenValueGetter?: NewlinesBetweenValueGetter<T>
2123
sourceCode: TSESLint.SourceCode
22-
sortedNodes: SortingNode[]
2324
fixer: TSESLint.RuleFixer
24-
nodes: SortingNode[]
25+
sortedNodes: T[]
26+
nodes: T[]
2527
}
2628

27-
export let makeNewlinesFixes = ({
29+
export let makeNewlinesFixes = <T extends SortingNode>({
30+
newlinesBetweenValueGetter,
2831
sortedNodes,
2932
sourceCode,
3033
options,
3134
fixer,
3235
nodes,
33-
}: MakeNewlinesFixesParameters): TSESLint.RuleFix[] => {
36+
}: MakeNewlinesFixesParameters<T>): TSESLint.RuleFix[] => {
3437
let fixes: TSESLint.RuleFix[] = []
3538

3639
for (let i = 0; i < sortedNodes.length - 1; i++) {
@@ -44,6 +47,12 @@ export let makeNewlinesFixes = ({
4447
sortingNode: sortedSortingNode,
4548
options,
4649
})
50+
newlinesBetween =
51+
newlinesBetweenValueGetter?.({
52+
computedNewlinesBetween: newlinesBetween,
53+
right: nextSortedSortingNode,
54+
left: sortedSortingNode,
55+
}) ?? newlinesBetween
4756

4857
if (newlinesBetween === 'ignore') {
4958
continue

‎utils/report-all-errors.ts

+21-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { TSESLint } from '@typescript-eslint/utils'
22

33
import type { SortingNodeWithDependencies } from './sort-nodes-by-dependencies'
4+
import type { NewlinesBetweenValueGetter } from './get-newlines-errors'
45
import type { GroupsOptions } from '../types/common-options'
56
import type { SortingNode } from '../types/sorting-node'
67
import type { MakeFixesParameters } from './make-fixes'
@@ -12,35 +13,41 @@ import { getGroupNumber } from './get-group-number'
1213
import { reportErrors } from './report-errors'
1314
import { pairwise } from './pairwise'
1415

15-
interface ReportAllErrorsParameters<MessageIds extends string> {
16+
interface ReportAllErrorsParameters<
17+
MessageIds extends string,
18+
T extends SortingNode,
19+
> {
1620
availableMessageIds: {
1721
missedSpacingBetweenMembers?: MessageIds
1822
extraSpacingBetweenMembers?: MessageIds
1923
unexpectedDependencyOrder?: MessageIds
2024
unexpectedGroupOrder?: MessageIds
2125
unexpectedOrder: MessageIds
2226
}
23-
sortNodesExcludingEslintDisabled(
24-
ignoreEslintDisabledNodes: boolean,
25-
): SortingNode[]
2627
options: {
2728
groups?: GroupsOptions<string>
28-
} & MakeFixesParameters['options']
29+
} & MakeFixesParameters<T>['options']
30+
sortNodesExcludingEslintDisabled(ignoreEslintDisabledNodes: boolean): T[]
31+
newlinesBetweenValueGetter?: NewlinesBetweenValueGetter<T>
2932
context: TSESLint.RuleContext<MessageIds, unknown[]>
3033
ignoreFirstNodeHighestBlockComment?: boolean
3134
sourceCode: TSESLint.SourceCode
32-
nodes: SortingNode[]
35+
nodes: T[]
3336
}
3437

35-
export let reportAllErrors = <MessageIds extends string>({
38+
export let reportAllErrors = <
39+
MessageIds extends string,
40+
T extends SortingNode = SortingNode,
41+
>({
3642
ignoreFirstNodeHighestBlockComment,
3743
sortNodesExcludingEslintDisabled,
44+
newlinesBetweenValueGetter,
3845
availableMessageIds,
3946
sourceCode,
4047
context,
4148
options,
4249
nodes,
43-
}: ReportAllErrorsParameters<MessageIds>): void => {
50+
}: ReportAllErrorsParameters<MessageIds, T>): void => {
4451
let sortedNodes = sortNodesExcludingEslintDisabled(false)
4552
let sortedNodesExcludingEslintDisabled =
4653
sortNodesExcludingEslintDisabled(true)
@@ -58,14 +65,12 @@ export let reportAllErrors = <MessageIds extends string>({
5865

5966
let messageIds: MessageIds[] = []
6067

61-
let firstUnorderedNodeDependentOnRight:
62-
| SortingNodeWithDependencies
63-
| undefined
68+
let firstUnorderedNodeDependentOnRight: undefined | T
6469
if (availableMessageIds.unexpectedDependencyOrder) {
6570
firstUnorderedNodeDependentOnRight = getFirstUnorderedNodeDependentOn(
66-
right as SortingNodeWithDependencies,
67-
nodes as SortingNodeWithDependencies[],
68-
)
71+
right as unknown as SortingNodeWithDependencies,
72+
nodes as unknown as SortingNodeWithDependencies[],
73+
) as unknown as T
6974
}
7075

7176
if (
@@ -101,6 +106,7 @@ export let reportAllErrors = <MessageIds extends string>({
101106
},
102107
missedSpacingError: availableMessageIds.missedSpacingBetweenMembers,
103108
extraSpacingError: availableMessageIds.extraSpacingBetweenMembers,
109+
newlinesBetweenValueGetter,
104110
rightNum: rightNumber,
105111
leftNum: leftNumber,
106112
sourceCode,
@@ -114,6 +120,7 @@ export let reportAllErrors = <MessageIds extends string>({
114120
sortedNodes: sortedNodesExcludingEslintDisabled,
115121
ignoreFirstNodeHighestBlockComment,
116122
firstUnorderedNodeDependentOnRight,
123+
newlinesBetweenValueGetter,
117124
messageIds,
118125
sourceCode,
119126
options,

‎utils/report-errors.ts

+23-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { TSESLint } from '@typescript-eslint/utils'
22

3+
import type { NewlinesBetweenValueGetter } from './get-newlines-errors'
34
import type { SortingNode } from '../types/sorting-node'
45
import type { MakeFixesParameters } from './make-fixes'
56

@@ -23,22 +24,27 @@ export const EXTRA_SPACING_ERROR =
2324
export const MISSED_SPACING_ERROR =
2425
`Missed spacing between "{{${LEFT}}}" and "{{${RIGHT}}}".` as const
2526

26-
interface ReportErrorsParameters<MessageIds extends string> {
27+
interface ReportErrorsParameters<
28+
MessageIds extends string,
29+
T extends SortingNode,
30+
> {
31+
newlinesBetweenValueGetter?: NewlinesBetweenValueGetter<T>
2732
context: TSESLint.RuleContext<MessageIds, unknown[]>
28-
firstUnorderedNodeDependentOnRight?: SortingNode
2933
ignoreFirstNodeHighestBlockComment?: boolean
30-
options?: MakeFixesParameters['options']
34+
options?: MakeFixesParameters<T>['options']
35+
firstUnorderedNodeDependentOnRight?: T
3136
sourceCode: TSESLint.SourceCode
32-
sortedNodes: SortingNode[]
3337
messageIds: MessageIds[]
34-
nodes: SortingNode[]
35-
right: SortingNode
36-
left: SortingNode
38+
sortedNodes: T[]
39+
nodes: T[]
40+
right: T
41+
left: T
3742
}
3843

39-
export let reportErrors = <MessageIds extends string>({
44+
export let reportErrors = <MessageIds extends string, T extends SortingNode>({
4045
firstUnorderedNodeDependentOnRight,
4146
ignoreFirstNodeHighestBlockComment,
47+
newlinesBetweenValueGetter,
4248
sortedNodes,
4349
messageIds,
4450
sourceCode,
@@ -47,25 +53,26 @@ export let reportErrors = <MessageIds extends string>({
4753
nodes,
4854
right,
4955
left,
50-
}: ReportErrorsParameters<MessageIds>): void => {
56+
}: ReportErrorsParameters<MessageIds, T>): void => {
5157
for (let messageId of messageIds) {
5258
context.report({
53-
data: {
54-
[NODE_DEPENDENT_ON_RIGHT]: firstUnorderedNodeDependentOnRight?.name,
55-
[RIGHT]: toSingleLine(right.name),
56-
[LEFT]: toSingleLine(left.name),
57-
[RIGHT_GROUP]: right.group,
58-
[LEFT_GROUP]: left.group,
59-
},
6059
fix: (fixer: TSESLint.RuleFixer) =>
6160
makeFixes({
6261
ignoreFirstNodeHighestBlockComment,
62+
newlinesBetweenValueGetter,
6363
sortedNodes,
6464
sourceCode,
6565
options,
6666
fixer,
6767
nodes,
6868
}),
69+
data: {
70+
[NODE_DEPENDENT_ON_RIGHT]: firstUnorderedNodeDependentOnRight?.name,
71+
[RIGHT]: toSingleLine(right.name),
72+
[LEFT]: toSingleLine(left.name),
73+
[RIGHT_GROUP]: right.group,
74+
[LEFT_GROUP]: left.group,
75+
},
6976
node: right.node,
7077
messageId,
7178
})

0 commit comments

Comments
 (0)
Please sign in to comment.