Skip to content

Commit 02a366d

Browse files
committed
Update swift-format to account for new multiline string tree structure.
This is a companion to swiftlang/swift-syntax#1255. The new structure of multiline strings yielded some nice cleanup of the way we handle those strings *directly*, but to keep the existing indentation decisions, some parts of multiline string processing bled out into other areas. Such is life.
1 parent 7272f19 commit 02a366d

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

@@ -1774,8 +1792,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
17741792

17751793
// If the rhs starts with a parenthesized expression, stack indentation around it.
17761794
// Otherwise, use regular continuation breaks.
1777-
if let (unindentingNode, _) = stackedIndentationBehavior(after: binOp, rhs: rhs) {
1778-
beforeTokens = [.break(.open(kind: .continuation))]
1795+
if let (unindentingNode, _, breakKind) = stackedIndentationBehavior(after: binOp, rhs: rhs)
1796+
{
1797+
beforeTokens = [.break(.open(kind: breakKind))]
17791798
after(unindentingNode.lastToken, tokens: [.break(.close(mustBreak: false), size: 0)])
17801799
} else {
17811800
beforeTokens = [.break(.continue)]
@@ -1790,7 +1809,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
17901809
}
17911810

17921811
after(binOp.lastToken, tokens: beforeTokens)
1793-
} else if let (unindentingNode, shouldReset) =
1812+
} else if let (unindentingNode, shouldReset, breakKind) =
17941813
stackedIndentationBehavior(after: binOp, rhs: rhs)
17951814
{
17961815
// For parenthesized expressions and for unparenthesized usages of `&&` and `||`, we don't
@@ -1800,7 +1819,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
18001819
// use open-continuation/close pairs around such operators and their right-hand sides so
18011820
// that the continuation breaks inside those scopes "stack", instead of receiving the
18021821
// usual single-level "continuation line or not" behavior.
1803-
let openBreakTokens: [Token] = [.break(.open(kind: .continuation)), .open]
1822+
let openBreakTokens: [Token] = [.break(.open(kind: breakKind)), .open]
18041823
if wrapsBeforeOperator {
18051824
before(binOp.firstToken, tokens: openBreakTokens)
18061825
} else {
@@ -1921,8 +1940,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
19211940
if let initializer = node.initializer {
19221941
let expr = initializer.value
19231942

1924-
if let (unindentingNode, _) = stackedIndentationBehavior(rhs: expr) {
1925-
after(initializer.equal, tokens: .break(.open(kind: .continuation)))
1943+
if let (unindentingNode, _, breakKind) = stackedIndentationBehavior(rhs: expr) {
1944+
after(initializer.equal, tokens: .break(.open(kind: breakKind)))
19261945
after(unindentingNode.lastToken, tokens: .break(.close(mustBreak: false), size: 0))
19271946
} else {
19281947
after(initializer.equal, tokens: .break(.continue))
@@ -2100,32 +2119,48 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
21002119

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

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

21312166
override func visit(_ node: AssociatedtypeDeclSyntax) -> SyntaxVisitorContinueKind {
@@ -2343,9 +2378,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
23432378
extractLeadingTrivia(token)
23442379
closeScopeTokens.forEach(appendToken)
23452380

2346-
if let pendingSegmentIndex = pendingMultilineStringSegmentPrefixLengths.index(forKey: token) {
2347-
appendMultilineStringSegments(at: pendingSegmentIndex)
2348-
} else if !ignoredTokens.contains(token) {
2381+
if !ignoredTokens.contains(token) {
23492382
// Otherwise, it's just a regular token, so add the text as-is.
23502383
appendToken(.syntax(token.presence == .present ? token.text : ""))
23512384
}
@@ -2357,48 +2390,6 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
23572390
return .skipChildren
23582391
}
23592392

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

3173+
/// Walks the expression and returns the leftmost multiline string literal (which might be the
3174+
/// expression itself) if the leftmost child is a multiline string literal.
3175+
///
3176+
/// - Parameter expr: The expression whose leftmost multiline string literal should be returned.
3177+
/// - Returns: The leftmost multiline string literal, or nil if the leftmost subexpression was
3178+
/// not a multiline string literal.
3179+
private func leftmostMultilineStringLiteral(of expr: ExprSyntax) -> StringLiteralExprSyntax? {
3180+
switch Syntax(expr).as(SyntaxEnum.self) {
3181+
case .stringLiteralExpr(let stringLiteralExpr)
3182+
where stringLiteralExpr.openQuote.tokenKind == .multilineStringQuote:
3183+
return stringLiteralExpr
3184+
case .infixOperatorExpr(let infixOperatorExpr):
3185+
return leftmostMultilineStringLiteral(of: infixOperatorExpr.leftOperand)
3186+
case .ternaryExpr(let ternaryExpr):
3187+
return leftmostMultilineStringLiteral(of: ternaryExpr.conditionExpression)
3188+
default:
3189+
return nil
3190+
}
3191+
}
3192+
31823193
/// Returns the outermost node enclosing the given node whose closing delimiter(s) must be kept
31833194
/// alongside the last token of the given node. Any tokens between `node.lastToken` and the
31843195
/// returned node's `lastToken` are delimiter tokens that shouldn't be preceded by a break.
@@ -3208,7 +3219,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32083219
private func stackedIndentationBehavior(
32093220
after operatorExpr: ExprSyntax? = nil,
32103221
rhs: ExprSyntax
3211-
) -> (unindentingNode: Syntax, shouldReset: Bool)? {
3222+
) -> (unindentingNode: Syntax, shouldReset: Bool, breakKind: OpenBreakKind)? {
32123223
// Check for logical operators first, and if it's that kind of operator, stack indentation
32133224
// around the entire right-hand-side. We have to do this check before checking the RHS for
32143225
// parentheses because if the user writes something like `... && (foo) > bar || ...`, we don't
@@ -3227,9 +3238,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32273238
// paren into the right hand side by unindenting after the final closing paren. This glues
32283239
// the paren to the last token of `rhs`.
32293240
if let unindentingParenExpr = outermostEnclosingNode(from: Syntax(rhs)) {
3230-
return (unindentingNode: unindentingParenExpr, shouldReset: true)
3241+
return (
3242+
unindentingNode: unindentingParenExpr, shouldReset: true, breakKind: .continuation)
32313243
}
3232-
return (unindentingNode: Syntax(rhs), shouldReset: true)
3244+
return (unindentingNode: Syntax(rhs), shouldReset: true, breakKind: .continuation)
32333245
}
32343246
}
32353247

@@ -3238,7 +3250,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32383250
if let ternaryExpr = rhs.as(TernaryExprSyntax.self) {
32393251
// We don't try to absorb any parens in this case, because the condition of a ternary cannot
32403252
// be grouped with any exprs outside of the condition.
3241-
return (unindentingNode: Syntax(ternaryExpr.conditionExpression), shouldReset: false)
3253+
return (
3254+
unindentingNode: Syntax(ternaryExpr.conditionExpression), shouldReset: false,
3255+
breakKind: .continuation)
32423256
}
32433257

32443258
// If the right-hand-side of the operator is or starts with a parenthesized expression, stack
@@ -3249,9 +3263,26 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32493263
// paren into the right hand side by unindenting after the final closing paren. This glues the
32503264
// paren to the last token of `rhs`.
32513265
if let unindentingParenExpr = outermostEnclosingNode(from: Syntax(rhs)) {
3252-
return (unindentingNode: unindentingParenExpr, shouldReset: true)
3266+
return (unindentingNode: unindentingParenExpr, shouldReset: true, breakKind: .continuation)
3267+
}
3268+
3269+
if let innerExpr = parenthesizedExpr.elementList.first?.expression,
3270+
let stringLiteralExpr = innerExpr.as(StringLiteralExprSyntax.self),
3271+
stringLiteralExpr.openQuote.tokenKind == .multilineStringQuote
3272+
{
3273+
pendingMultilineStringBreakKinds[stringLiteralExpr] = .continue
3274+
return nil
32533275
}
3254-
return (unindentingNode: Syntax(parenthesizedExpr), shouldReset: false)
3276+
3277+
return (
3278+
unindentingNode: Syntax(parenthesizedExpr), shouldReset: false, breakKind: .continuation)
3279+
}
3280+
3281+
// If the expression is a multiline string that is unparenthesized, create a block-based
3282+
// indentation scope and have the segments aligned inside it.
3283+
if let stringLiteralExpr = leftmostMultilineStringLiteral(of: rhs) {
3284+
pendingMultilineStringBreakKinds[stringLiteralExpr] = .same
3285+
return (unindentingNode: Syntax(stringLiteralExpr), shouldReset: false, breakKind: .block)
32553286
}
32563287

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

0 commit comments

Comments
 (0)