Skip to content

Commit c405d72

Browse files
committed
Handle existing commas in single element collections.
The `whitespaceOnly` mode in the PrettyPrinter handle existing commas in single element collections incorrectly by always removing the comma. This happened because an earlier fix stopped placing `commaDelimitedRegion[Start|End]` tokens around single element collections, while still ignoring any existing trailing comma token. Rather than move the `whitespaceOnly` decision deep into the TokenStreamCreator, I've revised that previous fix so that all logic to decide when to keep and remove commas is localized inside the PrettyPrinter. This was causing crashes when linting code that had a trailing comma in a single element collection, because `whitespaceOnly` mode was incorrectly causing non-whitespace changes to occur. Fixes SR-12782
1 parent 5986f65 commit c405d72

File tree

5 files changed

+32
-27
lines changed

5 files changed

+32
-27
lines changed

Sources/SwiftFormatPrettyPrint/PrettyPrint.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -541,12 +541,18 @@ public class PrettyPrinter {
541541
case .commaDelimitedRegionStart:
542542
commaDelimitedRegionStack.append(openCloseBreakCompensatingLineNumber)
543543

544-
case .commaDelimitedRegionEnd(let hasTrailingComma):
544+
case .commaDelimitedRegionEnd(let hasTrailingComma, let isSingleElement):
545545
guard let startLineNumber = commaDelimitedRegionStack.popLast() else {
546546
fatalError("Found trailing comma end with no corresponding start.")
547547
}
548548

549-
let shouldHaveTrailingComma = startLineNumber != openCloseBreakCompensatingLineNumber
549+
// We need to specifically disable trailing commas on elements of single item collections.
550+
// The syntax library can't distinguish a collection's initializer (where the elements are
551+
// types) from a literal (where the elements are the contents of a collection instance).
552+
// We never want to add a trailing comma in an initializer so we disable trailing commas on
553+
// single element collections.
554+
let shouldHaveTrailingComma =
555+
startLineNumber != openCloseBreakCompensatingLineNumber && !isSingleElement
550556
if shouldHaveTrailingComma && !hasTrailingComma {
551557
diagnose(.addTrailingComma)
552558
} else if !shouldHaveTrailingComma && hasTrailingComma {
@@ -653,14 +659,15 @@ public class PrettyPrinter {
653659
case .commaDelimitedRegionStart:
654660
lengths.append(0)
655661

656-
case .commaDelimitedRegionEnd:
662+
case .commaDelimitedRegionEnd(_, let isSingleElement):
657663
// The token's length is only necessary when a comma will be printed, but it's impossible to
658664
// know at this point whether the region-start token will be on the same line as this token.
659665
// Without adding this length to the total, it would be possible for this comma to be
660666
// printed in column `maxLineLength`. Unfortunately, this can cause breaks to fire
661667
// unnecessarily when the enclosed tokens comma would fit within `maxLineLength`.
662-
total += 1
663-
lengths.append(1)
668+
let length = isSingleElement ? 0 : 1
669+
total += length
670+
lengths.append(length)
664671
}
665672
}
666673

Sources/SwiftFormatPrettyPrint/Token.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,8 @@ enum Token {
185185
case commaDelimitedRegionStart
186186

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

191191
/// Starts a scope where `contextual` breaks have consistent behavior.
192192
case contextualBreakingStart

Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -795,16 +795,12 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
795795
if let trailingComma = lastElement.trailingComma {
796796
ignoredTokens.insert(trailingComma)
797797
}
798-
// The syntax library can't distinguish an array initializer (where the elements are types)
799-
// from an array literal (where the elements are the contents of the array). We never want to
800-
// add a trailing comma in the initializer case and there's always exactly 1 element, so we
801-
// never add a trailing comma to 1 element arrays.
802-
if let firstElement = node.first, firstElement != lastElement {
803-
before(firstElement.firstToken, tokens: .commaDelimitedRegionStart)
804-
let endToken =
805-
Token.commaDelimitedRegionEnd(hasTrailingComma: lastElement.trailingComma != nil)
806-
after(lastElement.expression.lastToken, tokens: [endToken])
807-
}
798+
before(node.first?.firstToken, tokens: .commaDelimitedRegionStart)
799+
let endToken =
800+
Token.commaDelimitedRegionEnd(
801+
hasTrailingComma: lastElement.trailingComma != nil,
802+
isSingleElement: node.first == lastElement)
803+
after(lastElement.expression.lastToken, tokens: [endToken])
808804
}
809805
return .visitChildren
810806
}
@@ -842,16 +838,12 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
842838
if let trailingComma = lastElement.trailingComma {
843839
ignoredTokens.insert(trailingComma)
844840
}
845-
// The syntax library can't distinguish a dictionary initializer (where the elements are
846-
// types) from a dictionary literal (where the elements are the contents of the dictionary).
847-
// We never want to add a trailing comma in the initializer case and there's always exactly 1
848-
// element, so we never add a trailing comma to 1 element dictionaries.
849-
if let firstElement = node.first, let lastElement = node.last, firstElement != lastElement {
850-
before(firstElement.firstToken, tokens: .commaDelimitedRegionStart)
851-
let endToken =
852-
Token.commaDelimitedRegionEnd(hasTrailingComma: lastElement.trailingComma != nil)
853-
after(lastElement.lastToken, tokens: endToken)
854-
}
841+
before(node.first?.firstToken, tokens: .commaDelimitedRegionStart)
842+
let endToken =
843+
Token.commaDelimitedRegionEnd(
844+
hasTrailingComma: lastElement.trailingComma != nil,
845+
isSingleElement: node.first == node.last)
846+
after(lastElement.lastToken, tokens: endToken)
855847
}
856848
return .visitChildren
857849
}

Tests/SwiftFormatPrettyPrintTests/ArrayDeclTests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ final class ArrayDeclTests: PrettyPrintTestCase {
103103
func testWhitespaceOnlyDoesNotChangeTrailingComma() {
104104
let input =
105105
"""
106+
let a = [
107+
"String",
108+
]
106109
let a = [1, 2, 3,]
107110
let a: [String] = [
108111
"One", "Two", "Three", "Four", "Five",

Tests/SwiftFormatPrettyPrintTests/DictionaryDeclTests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ final class DictionaryDeclTests: PrettyPrintTestCase {
9292
func testWhitespaceOnlyDoesNotChangeTrailingComma() {
9393
let input =
9494
"""
95+
let a = [
96+
1: "a",
97+
]
9598
let a = [1: "a", 2: "b", 3: "c",]
9699
let a: [Int: String] = [
97100
1: "a", 2: "b", 3: "c", 4: "d", 5: "e", 6: "f",

0 commit comments

Comments
 (0)