Skip to content

Commit 684d3a9

Browse files
authored
Merge pull request swiftlang#179 from dylansturg/ugh_commas
Prevent trailing commas from overflowing max line length.
2 parents 634a112 + 18b1b9b commit 684d3a9

File tree

5 files changed

+188
-46
lines changed

5 files changed

+188
-46
lines changed

Sources/SwiftFormatPrettyPrint/PrettyPrint.swift

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -651,15 +651,12 @@ public class PrettyPrinter {
651651
lengths.append(0)
652652

653653
case .commaDelimitedRegionEnd:
654-
// The trailing comma needs to be included in the length of the preceding break, but is not
655-
// included in the length of the enclosing group. A trailing comma cannot cause the group
656-
// to break onto multiple lines, because the comma isn't printed for a single line group.
657-
if let index = delimIndexStack.last, case .break = tokens[index] {
658-
lengths[index] += 1
659-
}
660-
// If the closest delimiter token is an open, instead of a break, then adding the comma's
661-
// length isn't necessary. In that case, the comma is printed if the preceding break fires.
662-
654+
// The token's length is only necessary when a comma will be printed, but it's impossible to
655+
// know at this point whether the region-start token will be on the same line as this token.
656+
// Without adding this length to the total, it would be possible for this comma to be
657+
// printed in column `maxLineLength`. Unfortunately, this can cause breaks to fire
658+
// unnecessarily when the enclosed tokens comma would fit within `maxLineLength`.
659+
total += 1
663660
lengths.append(1)
664661
}
665662
}

Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -728,28 +728,33 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
728728
override func visit(_ node: ArrayElementListSyntax) -> SyntaxVisitorContinueKind {
729729
insertTokens(.break(.same), betweenElementsOf: node)
730730

731-
// The syntax library can't distinguish an array initializer (where the elements are types) from
732-
// an array literal (where the elements are the contents of the array). We never want to add a
733-
// trailing comma in the initializer case and there's always exactly 1 element, so we never add
734-
// a trailing comma to 1 element arrays.
735-
if let firstElement = node.first, let lastElement = node.last, firstElement != lastElement {
736-
before(firstElement.firstToken, tokens: .commaDelimitedRegionStart)
737-
let endToken =
738-
Token.commaDelimitedRegionEnd(hasTrailingComma: lastElement.trailingComma != nil)
739-
after(lastElement.lastToken, tokens: endToken)
740-
if let existingTrailingComma = lastElement.trailingComma {
741-
ignoredTokens.insert(existingTrailingComma)
731+
for element in node {
732+
before(element.firstToken, tokens: .open)
733+
after(element.lastToken, tokens: .close)
734+
if let trailingComma = element.trailingComma {
735+
closingDelimiterTokens.insert(trailingComma)
736+
}
737+
}
738+
739+
if let lastElement = node.last {
740+
if let trailingComma = lastElement.trailingComma {
741+
ignoredTokens.insert(trailingComma)
742+
}
743+
// The syntax library can't distinguish an array initializer (where the elements are types)
744+
// from an array literal (where the elements are the contents of the array). We never want to
745+
// add a trailing comma in the initializer case and there's always exactly 1 element, so we
746+
// never add a trailing comma to 1 element arrays.
747+
if let firstElement = node.first, firstElement != lastElement {
748+
before(firstElement.firstToken, tokens: .commaDelimitedRegionStart)
749+
let endToken =
750+
Token.commaDelimitedRegionEnd(hasTrailingComma: lastElement.trailingComma != nil)
751+
after(lastElement.expression.lastToken, tokens: [endToken])
742752
}
743753
}
744754
return .visitChildren
745755
}
746756

747757
override func visit(_ node: ArrayElementSyntax) -> SyntaxVisitorContinueKind {
748-
before(node.firstToken, tokens: .open)
749-
after(node.lastToken, tokens: .close)
750-
if let trailingComma = node.trailingComma {
751-
closingDelimiterTokens.insert(trailingComma)
752-
}
753758
return .visitChildren
754759
}
755760

@@ -762,17 +767,28 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
762767
override func visit(_ node: DictionaryElementListSyntax) -> SyntaxVisitorContinueKind {
763768
insertTokens(.break(.same), betweenElementsOf: node)
764769

765-
// The syntax library can't distinguish a dictionary initializer (where the elements are types)
766-
// from a dictionary literal (where the elements are the contents of the dictionary). We never
767-
// want to add a trailing comma in the initializer case and there's always exactly 1 element,
768-
// so we never add a trailing comma to 1 element dictionaries.
769-
if let firstElement = node.first, let lastElement = node.last, firstElement != lastElement {
770-
before(firstElement.firstToken, tokens: .commaDelimitedRegionStart)
771-
let endToken =
772-
Token.commaDelimitedRegionEnd(hasTrailingComma: lastElement.trailingComma != nil)
773-
after(lastElement.lastToken, tokens: endToken)
774-
if let existingTrailingComma = lastElement.trailingComma {
775-
ignoredTokens.insert(existingTrailingComma)
770+
for element in node {
771+
before(element.firstToken, tokens: .open)
772+
after(element.colon, tokens: .break)
773+
after(element.lastToken, tokens: .close)
774+
if let trailingComma = element.trailingComma {
775+
closingDelimiterTokens.insert(trailingComma)
776+
}
777+
}
778+
779+
if let lastElement = node.last {
780+
if let trailingComma = lastElement.trailingComma {
781+
ignoredTokens.insert(trailingComma)
782+
}
783+
// The syntax library can't distinguish a dictionary initializer (where the elements are
784+
// types) from a dictionary literal (where the elements are the contents of the dictionary).
785+
// We never want to add a trailing comma in the initializer case and there's always exactly 1
786+
// element, so we never add a trailing comma to 1 element dictionaries.
787+
if let firstElement = node.first, let lastElement = node.last, firstElement != lastElement {
788+
before(firstElement.firstToken, tokens: .commaDelimitedRegionStart)
789+
let endToken =
790+
Token.commaDelimitedRegionEnd(hasTrailingComma: lastElement.trailingComma != nil)
791+
after(lastElement.lastToken, tokens: endToken)
776792
}
777793
}
778794
return .visitChildren
@@ -784,12 +800,6 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
784800
}
785801

786802
override func visit(_ node: DictionaryElementSyntax) -> SyntaxVisitorContinueKind {
787-
before(node.firstToken, tokens: .open)
788-
after(node.colon, tokens: .break)
789-
after(node.lastToken, tokens: .close)
790-
if let trailingComma = node.trailingComma {
791-
closingDelimiterTokens.insert(trailingComma)
792-
}
793803
return .visitChildren
794804
}
795805

Tests/SwiftFormatPrettyPrintTests/ArrayDeclTests.swift

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ final class ArrayDeclTests: PrettyPrintTestCase {
77
"""
88
let a = [1, 2, 3,]
99
let a: [Bool] = [false, true, true, false]
10-
let a = [11111111, 2222222, 33333333, 444444]
1110
let a = [11111111, 2222222, 33333333, 4444444]
1211
let a: [String] = ["One", "Two", "Three", "Four"]
1312
let a: [String] = ["One", "Two", "Three", "Four", "Five", "Six", "Seven"]
@@ -16,13 +15,13 @@ final class ArrayDeclTests: PrettyPrintTestCase {
1615
"One", "Two", "Three", "Four", "Five",
1716
"Six", "Seven", "Eight",
1817
]
18+
let a = [11111111, 2222222, 33333333, 444444]
1919
"""
2020

2121
let expected =
2222
"""
2323
let a = [1, 2, 3]
2424
let a: [Bool] = [false, true, true, false]
25-
let a = [11111111, 2222222, 33333333, 444444]
2625
let a = [
2726
11111111, 2222222, 33333333, 4444444,
2827
]
@@ -43,6 +42,15 @@ final class ArrayDeclTests: PrettyPrintTestCase {
4342
]
4443
4544
"""
45+
// Ideally, this array would be left on 1 line without a trailing comma. We don't know if the
46+
// comma is required when calculating the length of array elements, so the comma's length is
47+
// always added to last element and that 1 character causes the newlines inside of the array.
48+
+ """
49+
let a = [
50+
11111111, 2222222, 33333333, 444444,
51+
]
52+
53+
"""
4654

4755
assertPrettyPrintEqual(input: input, expected: expected, linelength: 45)
4856
}
@@ -134,4 +142,59 @@ final class ArrayDeclTests: PrettyPrintTestCase {
134142

135143
assertPrettyPrintEqual(input: input, expected: expected, linelength: 32)
136144
}
145+
146+
func testInnerElementBreakingFromComma() {
147+
let input =
148+
"""
149+
let a = [("abc", "def", "xyz"),("this ", "string", "is long"),]
150+
let a = [("abc", "def", "xyz"),("this ", "string", "is long")]
151+
let a = [("this ", "string", "is long"),]
152+
let a = [("this ", "string", "is long")]
153+
let a = ["this ", "string", "is longer",]
154+
let a = [("this", "str"), ("is", "lng")]
155+
a = [("az", "by"), ("cf", "de")]
156+
"""
157+
158+
let expected =
159+
"""
160+
let a = [
161+
("abc", "def", "xyz"),
162+
(
163+
"this ", "string", "is long"
164+
),
165+
]
166+
let a = [
167+
("abc", "def", "xyz"),
168+
(
169+
"this ", "string", "is long"
170+
),
171+
]
172+
let a = [
173+
("this ", "string", "is long")
174+
]
175+
let a = [
176+
("this ", "string", "is long")
177+
]
178+
let a = [
179+
"this ", "string",
180+
"is longer",
181+
]
182+
let a = [
183+
("this", "str"),
184+
("is", "lng"),
185+
]
186+
187+
"""
188+
// Ideally, this array would be left on 1 line without a trailing comma. We don't know if the
189+
// comma is required when calculating the length of array elements, so the comma's length is
190+
// always added to last element and that 1 character causes the newlines inside of the array.
191+
+ """
192+
a = [
193+
("az", "by"), ("cf", "de"),
194+
]
195+
196+
"""
197+
198+
assertPrettyPrintEqual(input: input, expected: expected, linelength: 32)
199+
}
137200
}

Tests/SwiftFormatPrettyPrintTests/DictionaryDeclTests.swift

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ final class DictionaryDeclTests: PrettyPrintTestCase {
77
"""
88
let a = [1: "a", 2: "b", 3: "c",]
99
let a: [Int: String] = [1: "a", 2: "b", 3: "c"]
10-
let a = [10000: "abc", 20000: "def", 30000: "ghi"]
1110
let a = [10000: "abc", 20000: "def", 30000: "ghij"]
1211
let a: [Int: String] = [1: "a", 2: "b", 3: "c", 4: "d"]
1312
let a: [Int: String] = [1: "a", 2: "b", 3: "c", 4: "d", 5: "e", 6: "f", 7: "g"]
@@ -16,13 +15,13 @@ final class DictionaryDeclTests: PrettyPrintTestCase {
1615
1: "a", 2: "b", 3: "c", 4: "d", 5: "e", 6: "f",
1716
7: "g", 8: "i",
1817
]
18+
let a = [10000: "abc", 20000: "def", 30000: "ghi"]
1919
"""
2020

2121
let expected =
2222
"""
2323
let a = [1: "a", 2: "b", 3: "c"]
2424
let a: [Int: String] = [1: "a", 2: "b", 3: "c"]
25-
let a = [10000: "abc", 20000: "def", 30000: "ghi"]
2625
let a = [
2726
10000: "abc", 20000: "def", 30000: "ghij",
2827
]
@@ -43,6 +42,16 @@ final class DictionaryDeclTests: PrettyPrintTestCase {
4342
]
4443
4544
"""
45+
// Ideally, this dictionary would be left on 1 line without a trailing comma. We don't know if
46+
// the comma is required when calculating the length of elements, so the comma's length is
47+
// always added to last element and that 1 character causes the newlines inside of the
48+
// dictionary.
49+
+ """
50+
let a = [
51+
10000: "abc", 20000: "def", 30000: "ghi",
52+
]
53+
54+
"""
4655

4756
assertPrettyPrintEqual(input: input, expected: expected, linelength: 50)
4857
}
@@ -175,4 +184,65 @@ final class DictionaryDeclTests: PrettyPrintTestCase {
175184

176185
assertPrettyPrintEqual(input: input, expected: expected, linelength: 32)
177186
}
187+
188+
func testInnerElementBreakingFromComma() {
189+
let input =
190+
"""
191+
let a = [key1: ("abc", "def", "xyz"),key2: ("this ", "string", "is long"),]
192+
let a = [key1: ("abc", "def", "xyz"),key2: ("this ", "string", "is long")]
193+
let a = [key2: ("this ", "string", "is long")]
194+
let a = [key2: ("this ", "string", "is long"),]
195+
let a = [key2: ("this ", "string", "is long ")]
196+
let a = [key1: ("a", "z"), key2: ("b ", "y")]
197+
let a = [key1: ("ab", "z"), key2: ("b ", "y")]
198+
a = [k1: ("ab", "z"), k2: ("bc", "y")]
199+
"""
200+
201+
let expected =
202+
"""
203+
let a = [
204+
key1: ("abc", "def", "xyz"),
205+
key2: (
206+
"this ", "string", "is long"
207+
),
208+
]
209+
let a = [
210+
key1: ("abc", "def", "xyz"),
211+
key2: (
212+
"this ", "string", "is long"
213+
),
214+
]
215+
let a = [
216+
key2: ("this ", "string", "is long")
217+
]
218+
let a = [
219+
key2: ("this ", "string", "is long")
220+
]
221+
let a = [
222+
key2: (
223+
"this ", "string", "is long "
224+
)
225+
]
226+
let a = [
227+
key1: ("a", "z"), key2: ("b ", "y"),
228+
]
229+
let a = [
230+
key1: ("ab", "z"),
231+
key2: ("b ", "y"),
232+
]
233+
234+
"""
235+
// Ideally, this dictionary would be left on 1 line without a trailing comma. We don't know if
236+
// the comma is required when calculating the length of elements, so the comma's length is
237+
// always added to last element and that 1 character causes the newlines inside of the
238+
// dictionary.
239+
+ """
240+
a = [
241+
k1: ("ab", "z"), k2: ("bc", "y"),
242+
]
243+
244+
"""
245+
246+
assertPrettyPrintEqual(input: input, expected: expected, linelength: 38)
247+
}
178248
}

Tests/SwiftFormatPrettyPrintTests/XCTestManifests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ extension ArrayDeclTests {
2525
("testArrayOfFunctions", testArrayOfFunctions),
2626
("testBasicArrays", testBasicArrays),
2727
("testGroupsTrailingComma", testGroupsTrailingComma),
28+
("testInnerElementBreakingFromComma", testInnerElementBreakingFromComma),
2829
("testNoTrailingCommasInTypes", testNoTrailingCommasInTypes),
2930
("testTrailingCommaDiagnostics", testTrailingCommaDiagnostics),
3031
("testWhitespaceOnlyDoesNotChangeTrailingComma", testWhitespaceOnlyDoesNotChangeTrailingComma),
@@ -196,6 +197,7 @@ extension DictionaryDeclTests {
196197
("testBasicDictionaries", testBasicDictionaries),
197198
("testDiscretionaryNewlineAfterColon", testDiscretionaryNewlineAfterColon),
198199
("testGroupsTrailingComma", testGroupsTrailingComma),
200+
("testInnerElementBreakingFromComma", testInnerElementBreakingFromComma),
199201
("testNoTrailingCommasInTypes", testNoTrailingCommasInTypes),
200202
("testTrailingCommaDiagnostics", testTrailingCommaDiagnostics),
201203
("testWhitespaceOnlyDoesNotChangeTrailingComma", testWhitespaceOnlyDoesNotChangeTrailingComma),

0 commit comments

Comments
 (0)