Skip to content

Commit 4727d3c

Browse files
authored
Merge pull request swiftlang#1335 from kimdv/kimdv/fix-multi-line-string
2 parents 83b85ae + 057a711 commit 4727d3c

File tree

4 files changed

+218
-13
lines changed

4 files changed

+218
-13
lines changed

CodeGeneration/Sources/generate-swiftsyntax/templates/basicformat/BasicFormatFile.swift

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,14 @@ let basicFormatFile = SourceFileSyntax(leadingTrivia: generateCopyrightHeader(fo
9292
if requiresTrailingSpace(node) && trailingTrivia.isEmpty {
9393
trailingTrivia += .space
9494
}
95-
if let keyPath = getKeyPath(Syntax(node)), requiresLeadingNewline(keyPath), !(leadingTrivia.first?.isNewline ?? false) {
95+
if let keyPath = getKeyPath(Syntax(node)), requiresLeadingNewline(keyPath), !(leadingTrivia.first?.isNewline ?? false), !shouldOmitNewline(node) {
9696
leadingTrivia = .newline + leadingTrivia
9797
}
98-
leadingTrivia = leadingTrivia.indented(indentation: indentation)
99-
trailingTrivia = trailingTrivia.indented(indentation: indentation)
98+
var isOnNewline: Bool = (lastRewrittenToken?.trailingTrivia.pieces.last?.isNewline == true)
99+
if case .stringSegment(let text) = lastRewrittenToken?.tokenKind {
100+
isOnNewline = isOnNewline || (text.last?.isNewline == true)
101+
}
102+
leadingTrivia = leadingTrivia.indented(indentation: indentation, isOnNewline: isOnNewline)
100103
let rewritten = TokenSyntax(
101104
node.tokenKind,
102105
leadingTrivia: leadingTrivia,
@@ -110,6 +113,25 @@ let basicFormatFile = SourceFileSyntax(leadingTrivia: generateCopyrightHeader(fo
110113
"""
111114
)
112115

116+
DeclSyntax(
117+
"""
118+
/// If this returns `true`, ``BasicFormat`` will not wrap `node` to a new line. This can be used to e.g. keep string interpolation segments on a single line.
119+
/// - Parameter node: the node that is being visited
120+
/// - Returns: returns true if newline should be omitted
121+
open func shouldOmitNewline(_ node: TokenSyntax) -> Bool {
122+
var ancestor: Syntax = Syntax(node)
123+
while let parent = ancestor.parent {
124+
ancestor = parent
125+
if ancestor.is(ExpressionSegmentSyntax.self) {
126+
return true
127+
}
128+
}
129+
130+
return false
131+
}
132+
"""
133+
)
134+
113135
try FunctionDeclSyntax("open func shouldIndent(_ keyPath: AnyKeyPath) -> Bool") {
114136
try SwitchExprSyntax("switch keyPath") {
115137
for node in SYNTAX_NODES where !node.isBase {

Sources/SwiftBasicFormat/Trivia+Indented.swift

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,35 @@
1313
import SwiftSyntax
1414

1515
extension Trivia {
16-
func indented(indentation: TriviaPiece) -> Trivia {
16+
/// Makes sure each newline of this trivia is followed by `indentation`. If this is not the case, the existing indentation is extended to `indentation`.
17+
/// `isOnNewline` determines whether the trivia starts on a new line. If this is the case, the function makes sure that the returned trivia starts with `indentation`.
18+
func indented(indentation: TriviaPiece, isOnNewline: Bool = false) -> Trivia {
1719
var indentedPieces: [TriviaPiece] = []
1820
for (index, piece) in self.enumerated() {
19-
let nextPiece = index < pieces.count - 1 ? pieces[index + 1] : nil
20-
indentedPieces.append(piece)
21-
if piece.isNewline {
22-
switch (nextPiece, indentation) {
23-
case (.spaces(let nextPieceSpaces)?, .spaces(let indentationSpaces)):
21+
let previousPieceIsNewline: Bool
22+
if index == 0 {
23+
previousPieceIsNewline = isOnNewline
24+
} else {
25+
previousPieceIsNewline = pieces[index - 1].isNewline
26+
}
27+
if previousPieceIsNewline {
28+
switch (piece, indentation) {
29+
case (.spaces(let nextPieceSpaces), .spaces(let indentationSpaces)):
2430
if nextPieceSpaces < indentationSpaces {
2531
indentedPieces.append(.spaces(indentationSpaces - nextPieceSpaces))
2632
}
27-
case (.tabs(let nextPieceTabs)?, .tabs(let indentationTabs)):
33+
case (.tabs(let nextPieceTabs), .tabs(let indentationTabs)):
2834
if nextPieceTabs < indentationTabs {
2935
indentedPieces.append(.tabs(indentationTabs - nextPieceTabs))
3036
}
3137
default:
3238
indentedPieces.append(indentation)
3339
}
3440
}
41+
indentedPieces.append(piece)
42+
}
43+
if self.pieces.last?.isNewline == true {
44+
indentedPieces.append(indentation)
3545
}
3646
return Trivia(pieces: indentedPieces)
3747
}

Sources/SwiftBasicFormat/generated/BasicFormat.swift

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,14 @@ open class BasicFormat: SyntaxRewriter {
5454
if requiresTrailingSpace(node) && trailingTrivia.isEmpty {
5555
trailingTrivia += .space
5656
}
57-
if let keyPath = getKeyPath(Syntax(node)), requiresLeadingNewline(keyPath), !(leadingTrivia.first?.isNewline ?? false) {
57+
if let keyPath = getKeyPath(Syntax(node)), requiresLeadingNewline(keyPath), !(leadingTrivia.first?.isNewline ?? false), !shouldOmitNewline(node) {
5858
leadingTrivia = .newline + leadingTrivia
5959
}
60-
leadingTrivia = leadingTrivia.indented(indentation: indentation)
61-
trailingTrivia = trailingTrivia.indented(indentation: indentation)
60+
var isOnNewline: Bool = (lastRewrittenToken?.trailingTrivia.pieces.last?.isNewline == true)
61+
if case .stringSegment(let text) = lastRewrittenToken?.tokenKind {
62+
isOnNewline = isOnNewline || (text.last?.isNewline == true)
63+
}
64+
leadingTrivia = leadingTrivia.indented(indentation: indentation, isOnNewline: isOnNewline)
6265
let rewritten = TokenSyntax(
6366
node.tokenKind,
6467
leadingTrivia: leadingTrivia,
@@ -71,6 +74,21 @@ open class BasicFormat: SyntaxRewriter {
7174
return rewritten
7275
}
7376

77+
/// If this returns `true`, ``BasicFormat`` will not wrap `node` to a new line. This can be used to e.g. keep string interpolation segments on a single line.
78+
/// - Parameter node: the node that is being visited
79+
/// - Returns: returns true if newline should be omitted
80+
open func shouldOmitNewline(_ node: TokenSyntax) -> Bool {
81+
var ancestor: Syntax = Syntax(node)
82+
while let parent = ancestor.parent {
83+
ancestor = parent
84+
if ancestor.is(ExpressionSegmentSyntax.self) {
85+
return true
86+
}
87+
}
88+
89+
return false
90+
}
91+
7492
open func shouldIndent(_ keyPath: AnyKeyPath) -> Bool {
7593
switch keyPath {
7694
case \AccessorBlockSyntax.accessors:

Tests/SwiftSyntaxBuilderTest/StringLiteralTests.swift

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,159 @@ final class StringLiteralTests: XCTestCase {
170170
"""#
171171
)
172172
}
173+
174+
func testStringLiteralInExpr() {
175+
let buildable = ExprSyntax(
176+
#"""
177+
"Validation failures: \(nonNilErrors.map({ "- \($0.description)" }).joined(separator: "\n"))"
178+
"""#
179+
)
180+
181+
AssertBuildResult(
182+
buildable,
183+
#"""
184+
"Validation failures: \(nonNilErrors.map({ "- \($0.description)" }).joined(separator: "\n"))"
185+
"""#
186+
)
187+
}
188+
189+
func testStringSegmentWithCode() {
190+
let buildable = StringSegmentSyntax(content: .stringSegment(#"\(nonNilErrors.map({ "- \($0.description)" }).joined(separator: "\n"))"#))
191+
192+
AssertBuildResult(
193+
buildable,
194+
#"\(nonNilErrors.map({ "- \($0.description)" }).joined(separator: "\n"))"#
195+
)
196+
}
197+
198+
func testStringLiteralSegmentWithCode() {
199+
let buildable = StringLiteralSegmentsSyntax {
200+
StringSegmentSyntax(content: .stringSegment(#"Error validating child at index \(index) of \(nodeKind):"#), trailingTrivia: .newline)
201+
StringSegmentSyntax(content: .stringSegment(#"Node did not satisfy any node choice requirement."#), trailingTrivia: .newline)
202+
StringSegmentSyntax(content: .stringSegment(#"Validation failures:"#), trailingTrivia: .newline)
203+
StringSegmentSyntax(content: .stringSegment(#"\(nonNilErrors.map({ "- \($0.description)" }).joined(separator: "\n"))"#))
204+
}
205+
206+
AssertBuildResult(
207+
buildable,
208+
#"""
209+
Error validating child at index \(index) of \(nodeKind):
210+
Node did not satisfy any node choice requirement.
211+
Validation failures:
212+
\(nonNilErrors.map({ "- \($0.description)" }).joined(separator: "\n"))
213+
"""#
214+
)
215+
}
216+
217+
func testMultiLineStringWithResultBuilder() {
218+
let buildable = StringLiteralExprSyntax(
219+
openQuote: .multilineStringQuoteToken(trailingTrivia: .newline),
220+
segments: StringLiteralSegmentsSyntax {
221+
StringSegmentSyntax(content: .stringSegment(#"Error validating child at index \(index) of \(nodeKind):"#), trailingTrivia: .newline)
222+
StringSegmentSyntax(content: .stringSegment(#"Node did not satisfy any node choice requirement."#), trailingTrivia: .newline)
223+
StringSegmentSyntax(content: .stringSegment(#"Validation failures:"#), trailingTrivia: .newline)
224+
ExpressionSegmentSyntax(
225+
expressions: TupleExprElementListSyntax {
226+
TupleExprElementSyntax(expression: ExprSyntax(#"nonNilErrors.map({ "- \($0.description)" }).joined(separator: "\n"))"#))
227+
}
228+
)
229+
},
230+
closeQuote: .multilineStringQuoteToken(leadingTrivia: .newline)
231+
)
232+
233+
AssertBuildResult(
234+
buildable,
235+
#"""
236+
"""
237+
Error validating child at index \(index) of \(nodeKind):
238+
Node did not satisfy any node choice requirement.
239+
Validation failures:
240+
\(nonNilErrors.map({ "- \($0.description)" }).joined(separator: "\n")))
241+
"""
242+
"""#
243+
)
244+
}
245+
246+
func testMultiStringLiteralInExpr() {
247+
let buildable = ExprSyntax(
248+
#"""
249+
assertionFailure("""
250+
Error validating child at index \(index) of \(nodeKind):
251+
Node did not satisfy any node choice requirement.
252+
Validation failures:
253+
\(nonNilErrors.map({ "- \($0.description)" }).joined(separator: "\n"))
254+
""", file: file, line: line)
255+
"""#
256+
)
257+
258+
AssertBuildResult(
259+
buildable,
260+
#"""
261+
assertionFailure("""
262+
Error validating child at index \(index) of \(nodeKind):
263+
Node did not satisfy any node choice requirement.
264+
Validation failures:
265+
\(nonNilErrors.map({ "- \($0.description)" }).joined(separator: "\n"))
266+
""", file: file, line: line)
267+
"""#
268+
)
269+
}
270+
271+
func testMultiStringLiteralInIfExpr() {
272+
let buildable = ExprSyntax(
273+
#"""
274+
if true {
275+
assertionFailure("""
276+
Error validating child at index
277+
Node did not satisfy any node choice requirement.
278+
Validation failures:
279+
""")
280+
}
281+
"""#
282+
)
283+
284+
AssertBuildResult(
285+
buildable,
286+
#"""
287+
if true {
288+
assertionFailure("""
289+
Error validating child at index
290+
Node did not satisfy any node choice requirement.
291+
Validation failures:
292+
""")
293+
}
294+
"""#
295+
)
296+
}
297+
298+
func testMultiStringLiteralOnNewlineInIfExpr() {
299+
let buildable = ExprSyntax(
300+
#"""
301+
if true {
302+
assertionFailure(
303+
"""
304+
Error validating child at index
305+
Node did not satisfy any node choice requirement.
306+
Validation failures:
307+
"""
308+
)
309+
}
310+
"""#
311+
)
312+
313+
AssertBuildResult(
314+
buildable,
315+
#"""
316+
if true {
317+
assertionFailure(
318+
"""
319+
Error validating child at index
320+
Node did not satisfy any node choice requirement.
321+
Validation failures:
322+
"""
323+
)
324+
}
325+
"""#
326+
)
327+
}
173328
}

0 commit comments

Comments
 (0)