Skip to content

Commit 47eaedd

Browse files
authored
Merge pull request #480 from allevato/fix-multiline-strings
Update swift-format to account for new multiline string tree structure.
2 parents 072768f + 02a366d commit 47eaedd

File tree

4 files changed

+282
-91
lines changed

4 files changed

+282
-91
lines changed

Sources/SwiftFormatCore/LegacyTriviaBehavior.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ private final class LegacyTriviaBehaviorRewriter: SyntaxRewriter {
3636
/// behavior.
3737
private func shouldTriviaPieceBeMoved(_ piece: TriviaPiece) -> Bool {
3838
switch piece {
39-
case .spaces, .tabs, .unexpectedText:
39+
case .spaces, .tabs, .unexpectedText, .backslashes:
4040
return false
4141
default:
4242
return true

Sources/SwiftFormatCore/Trivia+Convenience.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,14 @@ extension Trivia {
153153
return false
154154
})
155155
}
156+
157+
/// Returns `true` if this trivia contains any backslahes (used for multiline string newline
158+
/// suppression).
159+
public var containsBackslashes: Bool {
160+
return contains(
161+
where: {
162+
if case .backslashes = $0 { return true }
163+
return false
164+
})
165+
}
156166
}

Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift

Lines changed: 120 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
3333
/// appended since that break.
3434
private var canMergeNewlinesIntoLastBreak = false
3535

36-
/// Keeps track of the prefix length of multiline string segments when they are visited so that
37-
/// the prefix can be stripped at the beginning of lines before the text is added to the token
38-
/// stream.
39-
private var pendingMultilineStringSegmentPrefixLengths = [TokenSyntax: Int]()
36+
/// Keeps track of the kind of break that should be used inside a multiline string. This differs
37+
/// depending on surrounding context due to some tricky special cases, so this lets us pass that
38+
/// information down to the strings that need it.
39+
private var pendingMultilineStringBreakKinds = [StringLiteralExprSyntax: BreakKind]()
4040

4141
/// Lists tokens that shouldn't be appended to the token stream as `syntax` tokens. They will be
4242
/// printed conditionally using a different type of token.
@@ -659,7 +659,14 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
659659
}
660660

661661
override func visit(_ node: ReturnStmtSyntax) -> SyntaxVisitorContinueKind {
662-
before(node.expression?.firstToken, tokens: .break)
662+
if let expression = node.expression {
663+
if leftmostMultilineStringLiteral(of: expression) != nil {
664+
before(expression.firstToken, tokens: .break(.open))
665+
after(expression.lastToken, tokens: .break(.close(mustBreak: false)))
666+
} else {
667+
before(expression.firstToken, tokens: .break)
668+
}
669+
}
663670
return .visitChildren
664671
}
665672

@@ -1035,21 +1042,32 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
10351042
before(node.firstToken, tokens: .open)
10361043
}
10371044

1038-
// If we have an open delimiter following the colon, use a space instead of a continuation
1039-
// break so that we don't awkwardly shift the delimiter down and indent it further if it
1040-
// wraps.
1041-
let tokenAfterColon: Token = startsWithOpenDelimiter(Syntax(node.expression)) ? .space : .break
1045+
var additionalEndTokens = [Token]()
1046+
if let colon = node.colon {
1047+
// If we have an open delimiter following the colon, use a space instead of a continuation
1048+
// break so that we don't awkwardly shift the delimiter down and indent it further if it
1049+
// wraps.
1050+
var tokensAfterColon: [Token] = [
1051+
startsWithOpenDelimiter(Syntax(node.expression)) ? .space : .break
1052+
]
10421053

1043-
after(node.colon, tokens: tokenAfterColon)
1054+
if leftmostMultilineStringLiteral(of: node.expression) != nil {
1055+
tokensAfterColon.append(.break(.open(kind: .block), size: 0))
1056+
additionalEndTokens = [.break(.close(mustBreak: false), size: 0)]
1057+
}
1058+
1059+
after(colon, tokens: tokensAfterColon)
1060+
}
10441061

10451062
if let trailingComma = node.trailingComma {
1063+
before(trailingComma, tokens: additionalEndTokens)
10461064
var afterTrailingComma: [Token] = [.break(.same)]
10471065
if shouldGroup {
10481066
afterTrailingComma.insert(.close, at: 0)
10491067
}
10501068
after(trailingComma, tokens: afterTrailingComma)
10511069
} else if shouldGroup {
1052-
after(node.lastToken, tokens: .close)
1070+
after(node.lastToken, tokens: additionalEndTokens + [.close])
10531071
}
10541072
}
10551073

@@ -1781,8 +1799,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
17811799

17821800
// If the rhs starts with a parenthesized expression, stack indentation around it.
17831801
// Otherwise, use regular continuation breaks.
1784-
if let (unindentingNode, _) = stackedIndentationBehavior(after: binOp, rhs: rhs) {
1785-
beforeTokens = [.break(.open(kind: .continuation))]
1802+
if let (unindentingNode, _, breakKind) = stackedIndentationBehavior(after: binOp, rhs: rhs)
1803+
{
1804+
beforeTokens = [.break(.open(kind: breakKind))]
17861805
after(unindentingNode.lastToken, tokens: [.break(.close(mustBreak: false), size: 0)])
17871806
} else {
17881807
beforeTokens = [.break(.continue)]
@@ -1797,7 +1816,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
17971816
}
17981817

17991818
after(binOp.lastToken, tokens: beforeTokens)
1800-
} else if let (unindentingNode, shouldReset) =
1819+
} else if let (unindentingNode, shouldReset, breakKind) =
18011820
stackedIndentationBehavior(after: binOp, rhs: rhs)
18021821
{
18031822
// For parenthesized expressions and for unparenthesized usages of `&&` and `||`, we don't
@@ -1807,7 +1826,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
18071826
// use open-continuation/close pairs around such operators and their right-hand sides so
18081827
// that the continuation breaks inside those scopes "stack", instead of receiving the
18091828
// usual single-level "continuation line or not" behavior.
1810-
let openBreakTokens: [Token] = [.break(.open(kind: .continuation)), .open]
1829+
let openBreakTokens: [Token] = [.break(.open(kind: breakKind)), .open]
18111830
if wrapsBeforeOperator {
18121831
before(binOp.firstToken, tokens: openBreakTokens)
18131832
} else {
@@ -1928,8 +1947,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
19281947
if let initializer = node.initializer {
19291948
let expr = initializer.value
19301949

1931-
if let (unindentingNode, _) = stackedIndentationBehavior(rhs: expr) {
1932-
after(initializer.equal, tokens: .break(.open(kind: .continuation)))
1950+
if let (unindentingNode, _, breakKind) = stackedIndentationBehavior(rhs: expr) {
1951+
after(initializer.equal, tokens: .break(.open(kind: breakKind)))
19331952
after(unindentingNode.lastToken, tokens: .break(.close(mustBreak: false), size: 0))
19341953
} else {
19351954
after(initializer.equal, tokens: .break(.continue))
@@ -2107,32 +2126,48 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
21072126

21082127
override func visit(_ node: StringLiteralExprSyntax) -> SyntaxVisitorContinueKind {
21092128
if node.openQuote.tokenKind == .multilineStringQuote {
2110-
// If it's a multiline string, the last segment of the literal will end with a newline and
2111-
// zero or more whitespace that indicates the amount of whitespace stripped from each line of
2112-
// the string literal.
2113-
if let lastSegment = node.segments.last?.as(StringSegmentSyntax.self),
2114-
let lastLine
2115-
= lastSegment.content.text.split(separator: "\n", omittingEmptySubsequences: false).last
2116-
{
2117-
let prefixCount = lastLine.count
2118-
2119-
// Segments may be `StringSegmentSyntax` or `ExpressionSegmentSyntax`; for the purposes of
2120-
// newline handling and whitespace stripping, we only need to handle the former.
2121-
for segmentSyntax in node.segments {
2122-
guard let segment = segmentSyntax.as(StringSegmentSyntax.self) else {
2123-
continue
2124-
}
2125-
// Register the content tokens of the segments and the amount of leading whitespace to
2126-
// strip; this will be retrieved when we visit the token.
2127-
pendingMultilineStringSegmentPrefixLengths[segment.content] = prefixCount
2128-
}
2129-
}
2129+
// Looks up the correct break kind based on prior context.
2130+
let breakKind = pendingMultilineStringBreakKinds[node, default: .same]
2131+
after(node.openQuote, tokens: .break(breakKind, size: 0, newlines: .hard(count: 1)))
2132+
before(node.closeQuote, tokens: .break(breakKind, newlines: .hard(count: 1)))
21302133
}
21312134
return .visitChildren
21322135
}
21332136

21342137
override func visit(_ node: StringSegmentSyntax) -> SyntaxVisitorContinueKind {
2135-
return .visitChildren
2138+
// Looks up the correct break kind based on prior context.
2139+
func breakKind() -> BreakKind {
2140+
if let stringLiteralSegments = node.parent?.as(StringLiteralSegmentsSyntax.self),
2141+
let stringLiteralExpr = stringLiteralSegments.parent?.as(StringLiteralExprSyntax.self)
2142+
{
2143+
return pendingMultilineStringBreakKinds[stringLiteralExpr, default: .same]
2144+
} else {
2145+
return .same
2146+
}
2147+
}
2148+
2149+
let segmentText = node.content.text
2150+
if segmentText.hasSuffix("\n") {
2151+
// If this is a multiline string segment, it will end in a newline. Remove the newline and
2152+
// append the rest of the string, followed by a break if it's not the last line before the
2153+
// closing quotes. (The `StringLiteralExpr` above does the closing break.)
2154+
let remainder = node.content.text.dropLast()
2155+
if !remainder.isEmpty {
2156+
appendToken(.syntax(String(remainder)))
2157+
}
2158+
appendToken(.break(breakKind(), newlines: .hard(count: 1)))
2159+
} else {
2160+
appendToken(.syntax(segmentText))
2161+
}
2162+
2163+
if node.trailingTrivia?.containsBackslashes == true {
2164+
// Segments with trailing backslashes won't end with a literal newline; the backslash is
2165+
// considered trivia. To preserve the original text and wrapping, we need to manually render
2166+
// the backslash and a break into the token stream.
2167+
appendToken(.syntax("\\"))
2168+
appendToken(.break(breakKind(), newlines: .hard(count: 1)))
2169+
}
2170+
return .skipChildren
21362171
}
21372172

21382173
override func visit(_ node: AssociatedtypeDeclSyntax) -> SyntaxVisitorContinueKind {
@@ -2350,9 +2385,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
23502385
extractLeadingTrivia(token)
23512386
closeScopeTokens.forEach(appendToken)
23522387

2353-
if let pendingSegmentIndex = pendingMultilineStringSegmentPrefixLengths.index(forKey: token) {
2354-
appendMultilineStringSegments(at: pendingSegmentIndex)
2355-
} else if !ignoredTokens.contains(token) {
2388+
if !ignoredTokens.contains(token) {
23562389
// Otherwise, it's just a regular token, so add the text as-is.
23572390
appendToken(.syntax(token.presence == .present ? token.text : ""))
23582391
}
@@ -2364,48 +2397,6 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
23642397
return .skipChildren
23652398
}
23662399

2367-
/// Appends the contents of the pending multiline string segment at the given index in the
2368-
/// registration dictionary (removing it from that dictionary) to the token stream, splitting it
2369-
/// into lines along with required line breaks and stripping the leading whitespace.
2370-
private func appendMultilineStringSegments(at index: Dictionary<TokenSyntax, Int>.Index) {
2371-
let (token, prefixCount) = pendingMultilineStringSegmentPrefixLengths[index]
2372-
pendingMultilineStringSegmentPrefixLengths.remove(at: index)
2373-
2374-
let lines = token.text.split(separator: "\n", omittingEmptySubsequences: false)
2375-
2376-
// The first "line" is a special case. If it is non-empty, then it is a piece of text that
2377-
// immediately followed an interpolation segment on the same line of the string, like the
2378-
// " baz" in "foo bar \(x + y) baz". If that is the case, we need to insert that text before
2379-
// anything else.
2380-
let firstLine = lines.first!
2381-
if !firstLine.isEmpty {
2382-
appendToken(.syntax(String(firstLine)))
2383-
}
2384-
2385-
// Add the remaining lines of the segment, preceding each with a newline and stripping the
2386-
// leading whitespace so that the pretty-printer can re-indent the string according to the
2387-
// standard rules that it would apply.
2388-
for line in lines.dropFirst() as ArraySlice {
2389-
appendNewlines(.hard)
2390-
2391-
// Verify that the characters to be stripped are all spaces. If they are not, the string
2392-
// is not valid (no line should contain less leading whitespace than the line with the
2393-
// closing quotes), but the parser still allows this and it's flagged as an error later during
2394-
// compilation, so we don't want to destroy the user's text in that case.
2395-
let stringToAppend: Substring
2396-
if (line.prefix(prefixCount).allSatisfy { $0 == " " }) {
2397-
stringToAppend = line.dropFirst(prefixCount)
2398-
} else {
2399-
// Only strip as many spaces as we have. This will force the misaligned line to line up with
2400-
// the others; let's assume that's what the user wanted anyway.
2401-
stringToAppend = line.drop { $0 == " " }
2402-
}
2403-
if !stringToAppend.isEmpty {
2404-
appendToken(.syntax(String(stringToAppend)))
2405-
}
2406-
}
2407-
}
2408-
24092400
/// Appends the before-tokens of the given syntax token to the token stream.
24102401
private func appendBeforeTokens(_ token: TokenSyntax) {
24112402
if let before = beforeMap.removeValue(forKey: token) {
@@ -3186,6 +3177,26 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
31863177
}
31873178
}
31883179

3180+
/// Walks the expression and returns the leftmost multiline string literal (which might be the
3181+
/// expression itself) if the leftmost child is a multiline string literal.
3182+
///
3183+
/// - Parameter expr: The expression whose leftmost multiline string literal should be returned.
3184+
/// - Returns: The leftmost multiline string literal, or nil if the leftmost subexpression was
3185+
/// not a multiline string literal.
3186+
private func leftmostMultilineStringLiteral(of expr: ExprSyntax) -> StringLiteralExprSyntax? {
3187+
switch Syntax(expr).as(SyntaxEnum.self) {
3188+
case .stringLiteralExpr(let stringLiteralExpr)
3189+
where stringLiteralExpr.openQuote.tokenKind == .multilineStringQuote:
3190+
return stringLiteralExpr
3191+
case .infixOperatorExpr(let infixOperatorExpr):
3192+
return leftmostMultilineStringLiteral(of: infixOperatorExpr.leftOperand)
3193+
case .ternaryExpr(let ternaryExpr):
3194+
return leftmostMultilineStringLiteral(of: ternaryExpr.conditionExpression)
3195+
default:
3196+
return nil
3197+
}
3198+
}
3199+
31893200
/// Returns the outermost node enclosing the given node whose closing delimiter(s) must be kept
31903201
/// alongside the last token of the given node. Any tokens between `node.lastToken` and the
31913202
/// returned node's `lastToken` are delimiter tokens that shouldn't be preceded by a break.
@@ -3215,7 +3226,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32153226
private func stackedIndentationBehavior(
32163227
after operatorExpr: ExprSyntax? = nil,
32173228
rhs: ExprSyntax
3218-
) -> (unindentingNode: Syntax, shouldReset: Bool)? {
3229+
) -> (unindentingNode: Syntax, shouldReset: Bool, breakKind: OpenBreakKind)? {
32193230
// Check for logical operators first, and if it's that kind of operator, stack indentation
32203231
// around the entire right-hand-side. We have to do this check before checking the RHS for
32213232
// parentheses because if the user writes something like `... && (foo) > bar || ...`, we don't
@@ -3234,9 +3245,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32343245
// paren into the right hand side by unindenting after the final closing paren. This glues
32353246
// the paren to the last token of `rhs`.
32363247
if let unindentingParenExpr = outermostEnclosingNode(from: Syntax(rhs)) {
3237-
return (unindentingNode: unindentingParenExpr, shouldReset: true)
3248+
return (
3249+
unindentingNode: unindentingParenExpr, shouldReset: true, breakKind: .continuation)
32383250
}
3239-
return (unindentingNode: Syntax(rhs), shouldReset: true)
3251+
return (unindentingNode: Syntax(rhs), shouldReset: true, breakKind: .continuation)
32403252
}
32413253
}
32423254

@@ -3245,7 +3257,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32453257
if let ternaryExpr = rhs.as(TernaryExprSyntax.self) {
32463258
// We don't try to absorb any parens in this case, because the condition of a ternary cannot
32473259
// be grouped with any exprs outside of the condition.
3248-
return (unindentingNode: Syntax(ternaryExpr.conditionExpression), shouldReset: false)
3260+
return (
3261+
unindentingNode: Syntax(ternaryExpr.conditionExpression), shouldReset: false,
3262+
breakKind: .continuation)
32493263
}
32503264

32513265
// If the right-hand-side of the operator is or starts with a parenthesized expression, stack
@@ -3256,9 +3270,26 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32563270
// paren into the right hand side by unindenting after the final closing paren. This glues the
32573271
// paren to the last token of `rhs`.
32583272
if let unindentingParenExpr = outermostEnclosingNode(from: Syntax(rhs)) {
3259-
return (unindentingNode: unindentingParenExpr, shouldReset: true)
3273+
return (unindentingNode: unindentingParenExpr, shouldReset: true, breakKind: .continuation)
3274+
}
3275+
3276+
if let innerExpr = parenthesizedExpr.elementList.first?.expression,
3277+
let stringLiteralExpr = innerExpr.as(StringLiteralExprSyntax.self),
3278+
stringLiteralExpr.openQuote.tokenKind == .multilineStringQuote
3279+
{
3280+
pendingMultilineStringBreakKinds[stringLiteralExpr] = .continue
3281+
return nil
32603282
}
3261-
return (unindentingNode: Syntax(parenthesizedExpr), shouldReset: false)
3283+
3284+
return (
3285+
unindentingNode: Syntax(parenthesizedExpr), shouldReset: false, breakKind: .continuation)
3286+
}
3287+
3288+
// If the expression is a multiline string that is unparenthesized, create a block-based
3289+
// indentation scope and have the segments aligned inside it.
3290+
if let stringLiteralExpr = leftmostMultilineStringLiteral(of: rhs) {
3291+
pendingMultilineStringBreakKinds[stringLiteralExpr] = .same
3292+
return (unindentingNode: Syntax(stringLiteralExpr), shouldReset: false, breakKind: .block)
32623293
}
32633294

32643295
// Otherwise, don't stack--use regular continuation breaks instead.

0 commit comments

Comments
 (0)