Skip to content

Commit 61e4cf5

Browse files
authored
Merge pull request #545 from allevato/keypath-wrapping
Wrap keypath literals appropriately.
2 parents 8793d96 + a4de415 commit 61e4cf5

File tree

2 files changed

+92
-28
lines changed

2 files changed

+92
-28
lines changed

Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,6 +1760,30 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
17601760
}
17611761

17621762
override func visit(_ node: KeyPathExprSyntax) -> SyntaxVisitorContinueKind {
1763+
before(node.backslash, tokens: .open)
1764+
after(node.lastToken(viewMode: .sourceAccurate), tokens: .close)
1765+
return .visitChildren
1766+
}
1767+
1768+
override func visit(_ node: KeyPathComponentSyntax) -> SyntaxVisitorContinueKind {
1769+
// If this is the first component (immediately after the backslash), allow a break after the
1770+
// slash only if a typename follows it. Do not break in the middle of `\.`.
1771+
var breakBeforePeriod = true
1772+
if let keyPathComponents = node.parent?.as(KeyPathComponentListSyntax.self),
1773+
let keyPathExpr = keyPathComponents.parent?.as(KeyPathExprSyntax.self),
1774+
node == keyPathExpr.components.first, keyPathExpr.root == nil
1775+
{
1776+
breakBeforePeriod = false
1777+
}
1778+
if breakBeforePeriod {
1779+
before(node.period, tokens: .break(.continue, size: 0))
1780+
}
1781+
return .visitChildren
1782+
}
1783+
1784+
override func visit(_ node: KeyPathSubscriptComponentSyntax) -> SyntaxVisitorContinueKind {
1785+
after(node.leftBracket, tokens: .break(.open, size: 0), .open)
1786+
before(node.rightBracket, tokens: .break(.close, size: 0), .close)
17631787
return .visitChildren
17641788
}
17651789

@@ -1847,24 +1871,6 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
18471871
}
18481872

18491873
override func visit(_ node: InfixOperatorExprSyntax) -> SyntaxVisitorContinueKind {
1850-
// FIXME: This is a workaround/hack for https://github.com/apple/swift-syntax/issues/928. For
1851-
// keypaths like `\.?.foo`, they get represented (after folding) as an infix operator expression
1852-
// with an empty keypath, followed by the "binary operator" `.?.`, followed by other
1853-
// expressions. We can detect this and treat the whole thing as a verbatim node, which mimics
1854-
// what we do today for keypaths (i.e., nothing).
1855-
if let keyPathExpr = node.leftOperand.as(KeyPathExprSyntax.self),
1856-
keyPathExpr.components.isEmpty
1857-
{
1858-
// If there were spaces in the trailing trivia of the previous token, they would have been
1859-
// ignored (since this expression would be expected to insert its own preceding breaks).
1860-
// Preserve that whitespace verbatim for now.
1861-
if let previousToken = node.firstToken(viewMode: .sourceAccurate)?.previousToken(viewMode: .sourceAccurate) {
1862-
appendTrailingTrivia(previousToken, forced: true)
1863-
}
1864-
verbatimToken(Syntax(node), indentingBehavior: .none)
1865-
return .skipChildren
1866-
}
1867-
18681874
let binOp = node.operatorOperand
18691875
if binOp.is(ArrowExprSyntax.self) {
18701876
// `ArrowExprSyntax` nodes occur when a function type is written in an expression context;

Tests/SwiftFormatPrettyPrintTests/KeyPathExprTests.swift

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// TODO: Add more tests and figure out how we want to wrap keypaths. Right now, they just get
2-
// printed without breaks.
31
final class KeyPathExprTests: PrettyPrintTestCase {
42
func testSimple() {
53
let input =
@@ -47,15 +45,31 @@ final class KeyPathExprTests: PrettyPrintTestCase {
4745
let z = a.map(\.foo!.bar)
4846
"""#
4947

50-
let expected =
48+
let expected80 =
5149
#"""
5250
let x = \.foo?
5351
let y = \.foo!.bar
5452
let z = a.map(\.foo!.bar)
5553
5654
"""#
5755

58-
assertPrettyPrintEqual(input: input, expected: expected, linelength: 80)
56+
assertPrettyPrintEqual(input: input, expected: expected80, linelength: 80)
57+
58+
let expected11 =
59+
#"""
60+
let x =
61+
\.foo?
62+
let y =
63+
\.foo!
64+
.bar
65+
let z =
66+
a.map(
67+
\.foo!
68+
.bar)
69+
70+
"""#
71+
72+
assertPrettyPrintEqual(input: input, expected: expected11, linelength: 11)
5973
}
6074

6175
func testSubscript() {
@@ -80,19 +94,63 @@ final class KeyPathExprTests: PrettyPrintTestCase {
8094
func testImplicitSelfUnwrap() {
8195
let input =
8296
#"""
83-
//let x = \.?.foo
84-
//let y = \.?.foo.bar
97+
let x = \.?.foo
98+
let y = \.?.foo.bar
8599
let z = a.map(\.?.foo.bar)
86100
"""#
87101

88-
let expected =
102+
let expected80 =
89103
#"""
90-
//let x = \.?.foo
91-
//let y = \.?.foo.bar
104+
let x = \.?.foo
105+
let y = \.?.foo.bar
92106
let z = a.map(\.?.foo.bar)
93107
94108
"""#
95109

96-
assertPrettyPrintEqual(input: input, expected: expected, linelength: 80)
110+
assertPrettyPrintEqual(input: input, expected: expected80, linelength: 80)
111+
112+
let expected11 =
113+
#"""
114+
let x =
115+
\.?.foo
116+
let y =
117+
\.?.foo
118+
.bar
119+
let z =
120+
a.map(
121+
\.?.foo
122+
.bar)
123+
124+
"""#
125+
126+
assertPrettyPrintEqual(input: input, expected: expected11, linelength: 11)
127+
}
128+
129+
func testWrapping() {
130+
let input =
131+
#"""
132+
let x = \ReallyLongType.reallyLongProperty.anotherLongProperty
133+
let x = \.reeeeallyLongProperty.anotherLongProperty
134+
let x = \.longProperty.a.b.c[really + long + expression].anotherLongProperty
135+
"""#
136+
137+
let expected =
138+
#"""
139+
let x =
140+
\ReallyLongType
141+
.reallyLongProperty
142+
.anotherLongProperty
143+
let x =
144+
\.reeeeallyLongProperty
145+
.anotherLongProperty
146+
let x =
147+
\.longProperty.a.b.c[
148+
really + long
149+
+ expression
150+
].anotherLongProperty
151+
152+
"""#
153+
154+
assertPrettyPrintEqual(input: input, expected: expected, linelength: 23)
97155
}
98156
}

0 commit comments

Comments
 (0)