Skip to content

Commit a8e6e6f

Browse files
authored
Merge pull request #420 from allevato/keypath-workaround
Add a quick workaround for keypaths starting with `\.?.`.
2 parents 61344e8 + 17635f1 commit a8e6e6f

File tree

2 files changed

+130
-6
lines changed

2 files changed

+130
-6
lines changed

Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,12 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
135135
}
136136
}
137137

138-
private func verbatimToken(_ node: Syntax) {
138+
private func verbatimToken(_ node: Syntax, indentingBehavior: IndentingBehavior = .allLines) {
139139
if let firstToken = node.firstToken {
140140
appendBeforeTokens(firstToken)
141141
}
142142

143-
appendToken(.verbatim(Verbatim(text: node.description, indentingBehavior: .allLines)))
143+
appendToken(.verbatim(Verbatim(text: node.description, indentingBehavior: indentingBehavior)))
144144

145145
if let lastToken = node.lastToken {
146146
// Extract any comments that trail the verbatim block since they belong to the next syntax
@@ -1769,6 +1769,24 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
17691769
}
17701770

17711771
override func visit(_ node: InfixOperatorExprSyntax) -> SyntaxVisitorContinueKind {
1772+
// FIXME: This is a workaround/hack for https://github.com/apple/swift-syntax/issues/928. For
1773+
// keypaths like `\.?.foo`, they get represented (after folding) as an infix operator expression
1774+
// with an empty keypath, followed by the "binary operator" `.?.`, followed by other
1775+
// expressions. We can detect this and treat the whole thing as a verbatim node, which mimics
1776+
// what we do today for keypaths (i.e., nothing).
1777+
if let keyPathExpr = node.leftOperand.as(KeyPathExprSyntax.self),
1778+
keyPathExpr.components.isEmpty
1779+
{
1780+
// If there were spaces in the trailing trivia of the previous token, they would have been
1781+
// ignored (since this expression would be expected to insert its own preceding breaks).
1782+
// Preserve that whitespace verbatim for now.
1783+
if let previousToken = node.firstToken?.previousToken {
1784+
appendTrailingTrivia(previousToken, forced: true)
1785+
}
1786+
verbatimToken(Syntax(node), indentingBehavior: .none)
1787+
return .skipChildren
1788+
}
1789+
17721790
let binOp = node.operatorOperand
17731791
let rhs = node.rightOperand
17741792
maybeGroupAroundSubexpression(rhs, combiningOperator: binOp)
@@ -2514,14 +2532,22 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
25142532
/// trivia represents a malformed input (as opposed to garbage text in leading trivia, which has
25152533
/// some legitimate uses), this is a reasonable compromise to keep the garbage text roughly in the
25162534
/// same place but still let surrounding formatting occur somewhat as expected.
2517-
private func appendTrailingTrivia(_ token: TokenSyntax) {
2535+
private func appendTrailingTrivia(_ token: TokenSyntax, forced: Bool = false) {
25182536
let trailingTrivia = Array(token.trailingTrivia)
2519-
guard let lastUnexpectedIndex = trailingTrivia.lastIndex(where: { $0.isUnexpectedText }) else {
2520-
return
2537+
let lastIndex: Array<TriviaPiece>.Index
2538+
if forced {
2539+
lastIndex = trailingTrivia.index(before: trailingTrivia.endIndex)
2540+
} else {
2541+
guard
2542+
let lastUnexpectedIndex = trailingTrivia.lastIndex(where: { $0.isUnexpectedText })
2543+
else {
2544+
return
2545+
}
2546+
lastIndex = lastUnexpectedIndex
25212547
}
25222548

25232549
var verbatimText = ""
2524-
for piece in trailingTrivia[...lastUnexpectedIndex] {
2550+
for piece in trailingTrivia[...lastIndex] {
25252551
switch piece {
25262552
case .shebang, .unexpectedText, .spaces, .tabs, .formfeeds, .verticalTabs:
25272553
piece.write(to: &verbatimText)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// TODO: Add more tests and figure out how we want to wrap keypaths. Right now, they just get
2+
// printed without breaks.
3+
final class KeyPathExprTests: PrettyPrintTestCase {
4+
func testSimple() {
5+
let input =
6+
#"""
7+
let x = \.foo
8+
let y = \.foo.bar
9+
let z = a.map(\.foo.bar)
10+
"""#
11+
12+
let expected =
13+
#"""
14+
let x = \.foo
15+
let y = \.foo.bar
16+
let z = a.map(\.foo.bar)
17+
18+
"""#
19+
20+
assertPrettyPrintEqual(input: input, expected: expected, linelength: 80)
21+
}
22+
23+
func testWithType() {
24+
let input =
25+
#"""
26+
let x = \Type.foo
27+
let y = \Type.foo.bar
28+
let z = a.map(\Type.foo.bar)
29+
"""#
30+
31+
let expected =
32+
#"""
33+
let x = \Type.foo
34+
let y = \Type.foo.bar
35+
let z = a.map(\Type.foo.bar)
36+
37+
"""#
38+
39+
assertPrettyPrintEqual(input: input, expected: expected, linelength: 80)
40+
}
41+
42+
func testOptionalUnwrap() {
43+
let input =
44+
#"""
45+
let x = \.foo?
46+
let y = \.foo!.bar
47+
let z = a.map(\.foo!.bar)
48+
"""#
49+
50+
let expected =
51+
#"""
52+
let x = \.foo?
53+
let y = \.foo!.bar
54+
let z = a.map(\.foo!.bar)
55+
56+
"""#
57+
58+
assertPrettyPrintEqual(input: input, expected: expected, linelength: 80)
59+
}
60+
61+
func testSubscript() {
62+
let input =
63+
#"""
64+
let x = \.foo[0]
65+
let y = \.foo[0].bar
66+
let z = a.map(\.foo[0].bar)
67+
"""#
68+
69+
let expected =
70+
#"""
71+
let x = \.foo[0]
72+
let y = \.foo[0].bar
73+
let z = a.map(\.foo[0].bar)
74+
75+
"""#
76+
77+
assertPrettyPrintEqual(input: input, expected: expected, linelength: 80)
78+
}
79+
80+
func testImplicitSelfUnwrap() {
81+
let input =
82+
#"""
83+
//let x = \.?.foo
84+
//let y = \.?.foo.bar
85+
let z = a.map(\.?.foo.bar)
86+
"""#
87+
88+
let expected =
89+
#"""
90+
//let x = \.?.foo
91+
//let y = \.?.foo.bar
92+
let z = a.map(\.?.foo.bar)
93+
94+
"""#
95+
96+
assertPrettyPrintEqual(input: input, expected: expected, linelength: 80)
97+
}
98+
}

0 commit comments

Comments
 (0)