Skip to content

Introduce trailingCommasInMultilineLists configuration #1044

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Documentation/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,22 @@ switch someValue {

---

### `trailingCommasInMultilineLists`
**type:** `string`

**description:** Determines how trailing commas in comma-separated lists should be handled during formatting.

- If set to `"always"`, a trailing comma is always added in multi-line lists.
- If set to `"never"`, trailing commas are removed even in multi-line contexts.
- If set to `"ignore"` (the default), existing commas are preserved as-is, and for collections, the behavior falls back to the `multiElementCollectionTrailingCommas`.

This option takes precedence over `multiElementCollectionTrailingCommas`, unless it is set to `"ignore"`.


**default:** `"ignore"`

---

### `multiElementCollectionTrailingCommas`
**type:** boolean

Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftFormat/API/Configuration+Default.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ extension Configuration {
self.indentSwitchCaseLabels = false
self.spacesAroundRangeFormationOperators = false
self.noAssignmentInExpressions = NoAssignmentInExpressionsConfiguration()
self.trailingCommasInMultilineLists = .ignore
self.multiElementCollectionTrailingCommas = true
self.reflowMultilineStringLiterals = .never
self.indentBlankLines = false
Expand Down
20 changes: 20 additions & 0 deletions Sources/SwiftFormat/API/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public struct Configuration: Codable, Equatable {
case rules
case spacesAroundRangeFormationOperators
case noAssignmentInExpressions
case trailingCommasInMultilineLists
case multiElementCollectionTrailingCommas
case reflowMultilineStringLiterals
case indentBlankLines
Expand Down Expand Up @@ -173,6 +174,22 @@ public struct Configuration: Codable, Equatable {
/// Contains exceptions for the `NoAssignmentInExpressions` rule.
public var noAssignmentInExpressions: NoAssignmentInExpressionsConfiguration

/// Determines how trailing commas in comma-separated lists should be handled during formatting.
public enum TrailingCommasInMultilineLists: String, Codable {
case always
case never
case ignore
}

/// Determines how trailing commas in comma-separated lists are handled during formatting.
///
/// This setting takes precedence over `multiElementCollectionTrailingCommas`.
/// If set to `.ignore` (the default), the formatter defers to `multiElementCollectionTrailingCommas`
/// for collections only. In all other cases, existing trailing commas are preserved as-is and not modified.
/// If set to `.always` or `.never`, that behavior is applied uniformly across all list types,
/// regardless of `multiElementCollectionTrailingCommas`.
public var trailingCommasInMultilineLists: TrailingCommasInMultilineLists

/// Determines if multi-element collection literals should have trailing commas.
///
/// When `true` (default), the correct form is:
Expand Down Expand Up @@ -384,6 +401,9 @@ public struct Configuration: Codable, Equatable {
forKey: .noAssignmentInExpressions
)
?? defaults.noAssignmentInExpressions
self.trailingCommasInMultilineLists =
try container.decodeIfPresent(TrailingCommasInMultilineLists.self, forKey: .trailingCommasInMultilineLists)
?? defaults.trailingCommasInMultilineLists
self.multiElementCollectionTrailingCommas =
try container.decodeIfPresent(
Bool.self,
Expand Down
56 changes: 41 additions & 15 deletions Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ public class PrettyPrinter {
case .commaDelimitedRegionStart:
commaDelimitedRegionStack.append(openCloseBreakCompensatingLineNumber)

case .commaDelimitedRegionEnd(let hasTrailingComma, let isSingleElement):
case .commaDelimitedRegionEnd(let isCollection, let hasTrailingComma, let isSingleElement):
guard let startLineNumber = commaDelimitedRegionStack.popLast() else {
fatalError("Found trailing comma end with no corresponding start.")
}
Expand All @@ -511,17 +511,30 @@ public class PrettyPrinter {
// types) from a literal (where the elements are the contents of a collection instance).
// We never want to add a trailing comma in an initializer so we disable trailing commas on
// single element collections.
let shouldHaveTrailingComma =
startLineNumber != openCloseBreakCompensatingLineNumber && !isSingleElement
&& configuration.multiElementCollectionTrailingCommas
if shouldHaveTrailingComma && !hasTrailingComma {
diagnose(.addTrailingComma, category: .trailingComma)
} else if !shouldHaveTrailingComma && hasTrailingComma {
diagnose(.removeTrailingComma, category: .trailingComma)
}
let shouldHandleCommaDelimitedRegion: Bool? =
switch configuration.trailingCommasInMultilineLists {
case .always:
true
case .never:
false
case .ignore:
isCollection ? configuration.multiElementCollectionTrailingCommas : nil
}
if let shouldHandleCommaDelimitedRegion {
let shouldHaveTrailingComma =
startLineNumber != openCloseBreakCompensatingLineNumber && !isSingleElement
&& shouldHandleCommaDelimitedRegion
if shouldHaveTrailingComma && !hasTrailingComma {
diagnose(.addTrailingComma, category: .trailingComma)
} else if !shouldHaveTrailingComma && hasTrailingComma {
diagnose(.removeTrailingComma, category: .trailingComma)
}

let shouldWriteComma = whitespaceOnly ? hasTrailingComma : shouldHaveTrailingComma
if shouldWriteComma {
let shouldWriteComma = whitespaceOnly ? hasTrailingComma : shouldHaveTrailingComma
if shouldWriteComma {
outputBuffer.write(",")
}
} else if hasTrailingComma {
outputBuffer.write(",")
}

Expand Down Expand Up @@ -686,15 +699,28 @@ public class PrettyPrinter {
case .commaDelimitedRegionStart:
lengths.append(0)

case .commaDelimitedRegionEnd(_, let isSingleElement):
case .commaDelimitedRegionEnd(let isCollection, _, let isSingleElement):
// The token's length is only necessary when a comma will be printed, but it's impossible to
// know at this point whether the region-start token will be on the same line as this token.
// Without adding this length to the total, it would be possible for this comma to be
// printed in column `maxLineLength`. Unfortunately, this can cause breaks to fire
// unnecessarily when the enclosed tokens comma would fit within `maxLineLength`.
let length = isSingleElement ? 0 : 1
total += length
lengths.append(length)
let shouldHandleCommaDelimitedRegion: Bool? =
switch configuration.trailingCommasInMultilineLists {
case .always:
true
case .never:
false
case .ignore:
isCollection ? configuration.multiElementCollectionTrailingCommas : nil
}
if let shouldHandleCommaDelimitedRegion, shouldHandleCommaDelimitedRegion {
let length = isSingleElement ? 0 : 1
total += length
lengths.append(length)
} else {
lengths.append(0)
}

case .enableFormatting, .disableFormatting:
// no effect on length calculations
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftFormat/PrettyPrint/Token.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ enum Token {

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

/// Starts a scope where `contextual` breaks have consistent behavior.
case contextualBreakingStart
Expand Down
123 changes: 120 additions & 3 deletions Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -922,9 +922,19 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
}

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

Expand Down Expand Up @@ -974,6 +984,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart)
let endToken =
Token.commaDelimitedRegionEnd(
isCollection: true,
hasTrailingComma: lastElement.trailingComma != nil,
isSingleElement: node.first == lastElement
)
Expand Down Expand Up @@ -1018,6 +1029,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart)
let endToken =
Token.commaDelimitedRegionEnd(
isCollection: true,
hasTrailingComma: lastElement.trailingComma != nil,
isSingleElement: node.first == node.last
)
Expand Down Expand Up @@ -1291,6 +1303,27 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
return .visitChildren
}

override func visit(_ node: ClosureCaptureListSyntax) -> SyntaxVisitorContinueKind {
if let lastElement = node.last {
if let trailingComma = lastElement.trailingComma {
ignoredTokens.insert(trailingComma)
}
before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart)
let endToken =
Token.commaDelimitedRegionEnd(
isCollection: false,
hasTrailingComma: lastElement.trailingComma != nil,
isSingleElement: node.first == lastElement
)
if lastElement.initializer != nil {
after(lastElement.initializer?.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
} else {
after(lastElement.name.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
}
}
return .visitChildren
}

override func visit(_ node: ClosureCaptureSyntax) -> SyntaxVisitorContinueKind {
before(node.firstToken(viewMode: .sourceAccurate), tokens: .open)
after(node.specifier?.lastToken(viewMode: .sourceAccurate), tokens: .break)
Expand Down Expand Up @@ -1405,6 +1438,27 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
return .visitChildren
}

override func visit(_ node: EnumCaseParameterListSyntax) -> SyntaxVisitorContinueKind {
if let lastElement = node.last {
if let trailingComma = lastElement.trailingComma {
ignoredTokens.insert(trailingComma)
}
before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart)
let endToken =
Token.commaDelimitedRegionEnd(
isCollection: false,
hasTrailingComma: lastElement.trailingComma != nil,
isSingleElement: node.first == lastElement
)
if lastElement.defaultValue != nil {
after(lastElement.defaultValue?.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
} else {
after(lastElement.type.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
}
}
return .visitChildren
}

override func visit(_ node: FunctionParameterClauseSyntax) -> SyntaxVisitorContinueKind {
// Prioritize keeping ") throws -> <return_type>" together. We can only do this if the function
// has arguments.
Expand All @@ -1417,6 +1471,29 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
return .visitChildren
}

override func visit(_ node: FunctionParameterListSyntax) -> SyntaxVisitorContinueKind {
if let lastElement = node.last {
if let trailingComma = lastElement.trailingComma {
ignoredTokens.insert(trailingComma)
}
before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart)
let endToken =
Token.commaDelimitedRegionEnd(
isCollection: false,
hasTrailingComma: lastElement.trailingComma != nil,
isSingleElement: node.first == lastElement
)
if lastElement.defaultValue != nil {
after(lastElement.defaultValue?.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
} else if lastElement.ellipsis != nil {
after(lastElement.ellipsis?.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
} else {
after(lastElement.type.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
}
}
return .visitChildren
}

override func visit(_ node: ClosureParameterSyntax) -> SyntaxVisitorContinueKind {
before(node.firstToken(viewMode: .sourceAccurate), tokens: .open)
arrangeAttributeList(node.attributes)
Expand Down Expand Up @@ -1722,6 +1799,28 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
return .visitChildren
}

override func visit(_ node: GenericParameterListSyntax) -> SyntaxVisitorContinueKind {
if let lastElement = node.last {
if let trailingComma = lastElement.trailingComma {
ignoredTokens.insert(trailingComma)
}
before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart)
let endToken =
Token.commaDelimitedRegionEnd(
isCollection: false,
hasTrailingComma: lastElement.trailingComma != nil,
isSingleElement: node.first == lastElement
)

if lastElement.inheritedType != nil {
after(lastElement.inheritedType?.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
} else {
after(lastElement.name.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
}
}
return .visitChildren
}

override func visit(_ node: PrimaryAssociatedTypeClauseSyntax) -> SyntaxVisitorContinueKind {
after(node.leftAngle, tokens: .break(.open, size: 0), .open(argumentListConsistency()))
before(node.rightAngle, tokens: .break(.close, size: 0), .close)
Expand Down Expand Up @@ -1772,6 +1871,24 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
return .visitChildren
}

override func visit(_ node: TuplePatternElementListSyntax) -> SyntaxVisitorContinueKind {
if let lastElement = node.last {
if let trailingComma = lastElement.trailingComma {
ignoredTokens.insert(trailingComma)
}
before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart)
let endToken =
Token.commaDelimitedRegionEnd(
isCollection: false,
hasTrailingComma: lastElement.trailingComma != nil,
isSingleElement: node.first == lastElement
)

after(lastElement.pattern.lastToken(viewMode: .sourceAccurate), tokens: [endToken])
}
return .visitChildren
}

override func visit(_ node: TryExprSyntax) -> SyntaxVisitorContinueKind {
before(
node.expression.firstToken(viewMode: .sourceAccurate),
Expand Down
Loading