Skip to content

Commit c5fb5d2

Browse files
committed
Introduce trailingCommasInMultilineLists configuration
1 parent cbe4886 commit c5fb5d2

File tree

7 files changed

+943
-19
lines changed

7 files changed

+943
-19
lines changed

Documentation/Configuration.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,22 @@ switch someValue {
194194

195195
---
196196

197+
### `trailingCommasInMultilineLists`
198+
**type:** `string`
199+
200+
**description:** Determines how trailing commas in comma-separated lists should be handled during formatting.
201+
202+
- If set to `"always"`, a trailing comma is always added in multi-line lists.
203+
- If set to `"never"`, trailing commas are removed even in multi-line contexts.
204+
- If set to `"ignore"` (the default), existing commas are preserved as-is, and for collections, the behavior falls back to the `multiElementCollectionTrailingCommas`.
205+
206+
This option takes precedence over `multiElementCollectionTrailingCommas`, unless it is set to `"ignore"`.
207+
208+
209+
**default:** `"ignore"`
210+
211+
---
212+
197213
### `multiElementCollectionTrailingCommas`
198214
**type:** boolean
199215

Sources/SwiftFormat/API/Configuration+Default.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ extension Configuration {
3939
self.indentSwitchCaseLabels = false
4040
self.spacesAroundRangeFormationOperators = false
4141
self.noAssignmentInExpressions = NoAssignmentInExpressionsConfiguration()
42+
self.trailingCommasInMultilineLists = .ignore
4243
self.multiElementCollectionTrailingCommas = true
4344
self.reflowMultilineStringLiterals = .never
4445
self.indentBlankLines = false

Sources/SwiftFormat/API/Configuration.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public struct Configuration: Codable, Equatable {
4444
case rules
4545
case spacesAroundRangeFormationOperators
4646
case noAssignmentInExpressions
47+
case trailingCommasInMultilineLists
4748
case multiElementCollectionTrailingCommas
4849
case reflowMultilineStringLiterals
4950
case indentBlankLines
@@ -173,6 +174,22 @@ public struct Configuration: Codable, Equatable {
173174
/// Contains exceptions for the `NoAssignmentInExpressions` rule.
174175
public var noAssignmentInExpressions: NoAssignmentInExpressionsConfiguration
175176

177+
/// Determines how trailing commas in comma-separated lists should be handled during formatting.
178+
public enum TrailingCommasInMultilineLists: String, Codable {
179+
case always
180+
case never
181+
case ignore
182+
}
183+
184+
/// Determines how trailing commas in comma-separated lists are handled during formatting.
185+
///
186+
/// This setting takes precedence over `multiElementCollectionTrailingCommas`.
187+
/// If set to `.ignore` (the default), the formatter defers to `multiElementCollectionTrailingCommas`
188+
/// for collections only. In all other cases, existing trailing commas are preserved as-is and not modified.
189+
/// If set to `.always` or `.never`, that behavior is applied uniformly across all list types,
190+
/// regardless of `multiElementCollectionTrailingCommas`.
191+
public var trailingCommasInMultilineLists: TrailingCommasInMultilineLists
192+
176193
/// Determines if multi-element collection literals should have trailing commas.
177194
///
178195
/// When `true` (default), the correct form is:
@@ -384,6 +401,9 @@ public struct Configuration: Codable, Equatable {
384401
forKey: .noAssignmentInExpressions
385402
)
386403
?? defaults.noAssignmentInExpressions
404+
self.trailingCommasInMultilineLists =
405+
try container.decodeIfPresent(TrailingCommasInMultilineLists.self, forKey: .trailingCommasInMultilineLists)
406+
?? defaults.trailingCommasInMultilineLists
387407
self.multiElementCollectionTrailingCommas =
388408
try container.decodeIfPresent(
389409
Bool.self,

Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ public class PrettyPrinter {
501501
case .commaDelimitedRegionStart:
502502
commaDelimitedRegionStack.append(openCloseBreakCompensatingLineNumber)
503503

504-
case .commaDelimitedRegionEnd(let hasTrailingComma, let isSingleElement):
504+
case .commaDelimitedRegionEnd(let isCollection, let hasTrailingComma, let isSingleElement):
505505
guard let startLineNumber = commaDelimitedRegionStack.popLast() else {
506506
fatalError("Found trailing comma end with no corresponding start.")
507507
}
@@ -511,17 +511,30 @@ public class PrettyPrinter {
511511
// types) from a literal (where the elements are the contents of a collection instance).
512512
// We never want to add a trailing comma in an initializer so we disable trailing commas on
513513
// single element collections.
514-
let shouldHaveTrailingComma =
515-
startLineNumber != openCloseBreakCompensatingLineNumber && !isSingleElement
516-
&& configuration.multiElementCollectionTrailingCommas
517-
if shouldHaveTrailingComma && !hasTrailingComma {
518-
diagnose(.addTrailingComma, category: .trailingComma)
519-
} else if !shouldHaveTrailingComma && hasTrailingComma {
520-
diagnose(.removeTrailingComma, category: .trailingComma)
521-
}
514+
let shouldHandleCommaDelimitedRegion: Bool? =
515+
switch configuration.trailingCommasInMultilineLists {
516+
case .always:
517+
true
518+
case .never:
519+
false
520+
case .ignore:
521+
isCollection ? configuration.multiElementCollectionTrailingCommas : nil
522+
}
523+
if let shouldHandleCommaDelimitedRegion {
524+
let shouldHaveTrailingComma =
525+
startLineNumber != openCloseBreakCompensatingLineNumber && !isSingleElement
526+
&& shouldHandleCommaDelimitedRegion
527+
if shouldHaveTrailingComma && !hasTrailingComma {
528+
diagnose(.addTrailingComma, category: .trailingComma)
529+
} else if !shouldHaveTrailingComma && hasTrailingComma {
530+
diagnose(.removeTrailingComma, category: .trailingComma)
531+
}
522532

523-
let shouldWriteComma = whitespaceOnly ? hasTrailingComma : shouldHaveTrailingComma
524-
if shouldWriteComma {
533+
let shouldWriteComma = whitespaceOnly ? hasTrailingComma : shouldHaveTrailingComma
534+
if shouldWriteComma {
535+
outputBuffer.write(",")
536+
}
537+
} else if hasTrailingComma {
525538
outputBuffer.write(",")
526539
}
527540

@@ -686,15 +699,28 @@ public class PrettyPrinter {
686699
case .commaDelimitedRegionStart:
687700
lengths.append(0)
688701

689-
case .commaDelimitedRegionEnd(_, let isSingleElement):
702+
case .commaDelimitedRegionEnd(let isCollection, _, let isSingleElement):
690703
// The token's length is only necessary when a comma will be printed, but it's impossible to
691704
// know at this point whether the region-start token will be on the same line as this token.
692705
// Without adding this length to the total, it would be possible for this comma to be
693706
// printed in column `maxLineLength`. Unfortunately, this can cause breaks to fire
694707
// unnecessarily when the enclosed tokens comma would fit within `maxLineLength`.
695-
let length = isSingleElement ? 0 : 1
696-
total += length
697-
lengths.append(length)
708+
let shouldHandleCommaDelimitedRegion: Bool? =
709+
switch configuration.trailingCommasInMultilineLists {
710+
case .always:
711+
true
712+
case .never:
713+
false
714+
case .ignore:
715+
isCollection ? configuration.multiElementCollectionTrailingCommas : nil
716+
}
717+
if let shouldHandleCommaDelimitedRegion, shouldHandleCommaDelimitedRegion {
718+
let length = isSingleElement ? 0 : 1
719+
total += length
720+
lengths.append(length)
721+
} else {
722+
lengths.append(0)
723+
}
698724

699725
case .enableFormatting, .disableFormatting:
700726
// no effect on length calculations

Sources/SwiftFormat/PrettyPrint/Token.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ enum Token {
194194

195195
/// Marks the end of a comma delimited collection, where a trailing comma should be inserted
196196
/// if and only if the collection spans multiple lines and has multiple elements.
197-
case commaDelimitedRegionEnd(hasTrailingComma: Bool, isSingleElement: Bool)
197+
case commaDelimitedRegionEnd(isCollection: Bool, hasTrailingComma: Bool, isSingleElement: Bool)
198198

199199
/// Starts a scope where `contextual` breaks have consistent behavior.
200200
case contextualBreakingStart

Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -922,9 +922,19 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
922922
}
923923

924924
override func visit(_ node: LabeledExprListSyntax) -> SyntaxVisitorContinueKind {
925-
// Intentionally do nothing here. Since `TupleExprElement`s are used both in tuple expressions
926-
// and function argument lists, which need to be formatted, differently, those nodes manually
927-
// loop over the nodes and arrange them in those contexts.
925+
if let lastElement = node.last {
926+
if let trailingComma = lastElement.trailingComma {
927+
ignoredTokens.insert(trailingComma)
928+
}
929+
before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart)
930+
let endToken =
931+
Token.commaDelimitedRegionEnd(
932+
isCollection: false,
933+
hasTrailingComma: lastElement.trailingComma != nil,
934+
isSingleElement: node.first == lastElement
935+
)
936+
after(lastElement.expression.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
937+
}
928938
return .visitChildren
929939
}
930940

@@ -974,6 +984,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
974984
before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart)
975985
let endToken =
976986
Token.commaDelimitedRegionEnd(
987+
isCollection: true,
977988
hasTrailingComma: lastElement.trailingComma != nil,
978989
isSingleElement: node.first == lastElement
979990
)
@@ -1018,6 +1029,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
10181029
before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart)
10191030
let endToken =
10201031
Token.commaDelimitedRegionEnd(
1032+
isCollection: true,
10211033
hasTrailingComma: lastElement.trailingComma != nil,
10221034
isSingleElement: node.first == node.last
10231035
)
@@ -1291,6 +1303,27 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
12911303
return .visitChildren
12921304
}
12931305

1306+
override func visit(_ node: ClosureCaptureListSyntax) -> SyntaxVisitorContinueKind {
1307+
if let lastElement = node.last {
1308+
if let trailingComma = lastElement.trailingComma {
1309+
ignoredTokens.insert(trailingComma)
1310+
}
1311+
before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart)
1312+
let endToken =
1313+
Token.commaDelimitedRegionEnd(
1314+
isCollection: false,
1315+
hasTrailingComma: lastElement.trailingComma != nil,
1316+
isSingleElement: node.first == lastElement
1317+
)
1318+
if lastElement.initializer != nil {
1319+
after(lastElement.initializer?.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
1320+
} else {
1321+
after(lastElement.name.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
1322+
}
1323+
}
1324+
return .visitChildren
1325+
}
1326+
12941327
override func visit(_ node: ClosureCaptureSyntax) -> SyntaxVisitorContinueKind {
12951328
before(node.firstToken(viewMode: .sourceAccurate), tokens: .open)
12961329
after(node.specifier?.lastToken(viewMode: .sourceAccurate), tokens: .break)
@@ -1405,6 +1438,27 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
14051438
return .visitChildren
14061439
}
14071440

1441+
override func visit(_ node: EnumCaseParameterListSyntax) -> SyntaxVisitorContinueKind {
1442+
if let lastElement = node.last {
1443+
if let trailingComma = lastElement.trailingComma {
1444+
ignoredTokens.insert(trailingComma)
1445+
}
1446+
before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart)
1447+
let endToken =
1448+
Token.commaDelimitedRegionEnd(
1449+
isCollection: false,
1450+
hasTrailingComma: lastElement.trailingComma != nil,
1451+
isSingleElement: node.first == lastElement
1452+
)
1453+
if lastElement.defaultValue != nil {
1454+
after(lastElement.defaultValue?.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
1455+
} else {
1456+
after(lastElement.type.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
1457+
}
1458+
}
1459+
return .visitChildren
1460+
}
1461+
14081462
override func visit(_ node: FunctionParameterClauseSyntax) -> SyntaxVisitorContinueKind {
14091463
// Prioritize keeping ") throws -> <return_type>" together. We can only do this if the function
14101464
// has arguments.
@@ -1417,6 +1471,29 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
14171471
return .visitChildren
14181472
}
14191473

1474+
override func visit(_ node: FunctionParameterListSyntax) -> SyntaxVisitorContinueKind {
1475+
if let lastElement = node.last {
1476+
if let trailingComma = lastElement.trailingComma {
1477+
ignoredTokens.insert(trailingComma)
1478+
}
1479+
before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart)
1480+
let endToken =
1481+
Token.commaDelimitedRegionEnd(
1482+
isCollection: false,
1483+
hasTrailingComma: lastElement.trailingComma != nil,
1484+
isSingleElement: node.first == lastElement
1485+
)
1486+
if lastElement.defaultValue != nil {
1487+
after(lastElement.defaultValue?.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
1488+
} else if lastElement.ellipsis != nil {
1489+
after(lastElement.ellipsis?.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
1490+
} else {
1491+
after(lastElement.type.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
1492+
}
1493+
}
1494+
return .visitChildren
1495+
}
1496+
14201497
override func visit(_ node: ClosureParameterSyntax) -> SyntaxVisitorContinueKind {
14211498
before(node.firstToken(viewMode: .sourceAccurate), tokens: .open)
14221499
arrangeAttributeList(node.attributes)
@@ -1722,6 +1799,28 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
17221799
return .visitChildren
17231800
}
17241801

1802+
override func visit(_ node: GenericParameterListSyntax) -> SyntaxVisitorContinueKind {
1803+
if let lastElement = node.last {
1804+
if let trailingComma = lastElement.trailingComma {
1805+
ignoredTokens.insert(trailingComma)
1806+
}
1807+
before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart)
1808+
let endToken =
1809+
Token.commaDelimitedRegionEnd(
1810+
isCollection: false,
1811+
hasTrailingComma: lastElement.trailingComma != nil,
1812+
isSingleElement: node.first == lastElement
1813+
)
1814+
1815+
if lastElement.inheritedType != nil {
1816+
after(lastElement.inheritedType?.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
1817+
} else {
1818+
after(lastElement.name.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
1819+
}
1820+
}
1821+
return .visitChildren
1822+
}
1823+
17251824
override func visit(_ node: PrimaryAssociatedTypeClauseSyntax) -> SyntaxVisitorContinueKind {
17261825
after(node.leftAngle, tokens: .break(.open, size: 0), .open(argumentListConsistency()))
17271826
before(node.rightAngle, tokens: .break(.close, size: 0), .close)
@@ -1772,6 +1871,24 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
17721871
return .visitChildren
17731872
}
17741873

1874+
override func visit(_ node: TuplePatternElementListSyntax) -> SyntaxVisitorContinueKind {
1875+
if let lastElement = node.last {
1876+
if let trailingComma = lastElement.trailingComma {
1877+
ignoredTokens.insert(trailingComma)
1878+
}
1879+
before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart)
1880+
let endToken =
1881+
Token.commaDelimitedRegionEnd(
1882+
isCollection: false,
1883+
hasTrailingComma: lastElement.trailingComma != nil,
1884+
isSingleElement: node.first == lastElement
1885+
)
1886+
1887+
after(lastElement.pattern.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
1888+
}
1889+
return .visitChildren
1890+
}
1891+
17751892
override func visit(_ node: TryExprSyntax) -> SyntaxVisitorContinueKind {
17761893
before(
17771894
node.expression.firstToken(viewMode: .sourceAccurate),

0 commit comments

Comments
 (0)