Skip to content

Commit db662d8

Browse files
Unify PresentMaker and MissingNodesBasicFormatter, fixing a formatting bug in string literals
The underlying issues of the malformatted multi-line string literal are that 1. We are setting an `anchorPoint` for empty lines. Anchor points are meant to be starting points at which the user didn’t provide any manual indentation relative to which we should format the rest of the tree. But for empty lines it’s not like the user didn’t provide any indentation. There simply wasn’t anything to indent. 2. `leadingTrivia.trimmingTrailingWhitespaceBeforeNewline` was not considering whether the token text itself contained a newline in `isBeforeNewline`. Now, to detect if the token is followed by a newline, we should check if its text is empty, which is the case for empty string literals. Unfortunately, this is also the case for missing identifiers, which also have an empty text but whose empty text will be replace by a placeholder by `PresentMaker`. To distinguish between the two, I merged `PresentMaker` and `MissingNodesBasicFormatter` so that the `BasicFormat` also performs the text replacement to placeholders. Fixes #1959 Co-Authored-By: kishikawa katsumi <[email protected]>
1 parent e04c5c1 commit db662d8

File tree

4 files changed

+136
-36
lines changed

4 files changed

+136
-36
lines changed

Sources/SwiftBasicFormat/BasicFormat.swift

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
import SwiftSyntax
13+
@_spi(RawSyntax) import SwiftSyntax
1414

1515
/// A rewriter that performs a "basic" format of the passed tree.
1616
///
@@ -271,7 +271,7 @@ open class BasicFormat: SyntaxRewriter {
271271
(.keyword(.`init`), .leftParen), // init()
272272
(.keyword(.self), .period), // self.someProperty
273273
(.keyword(.Self), .period), // self.someProperty
274-
(.keyword(.set), .leftParen), // var mYar: Int { set(value) {} }
274+
(.keyword(.set), .leftParen), // var mVar: Int { set(value) {} }
275275
(.keyword(.subscript), .leftParen), // subscript(x: Int)
276276
(.keyword(.super), .period), // super.someProperty
277277
(.leftBrace, .rightBrace), // {}
@@ -348,6 +348,26 @@ open class BasicFormat: SyntaxRewriter {
348348
return true
349349
}
350350

351+
/// Change the text of a token during formatting.
352+
///
353+
/// This allows formats to e.g. replace missing tokens by placeholder tokens.
354+
///
355+
/// - Parameter token: The token whose text should be changed
356+
/// - Returns: The new text or `nil` if the text should not be changed
357+
open func transformTokenText(_ token: TokenSyntax) -> String? {
358+
return nil
359+
}
360+
361+
/// Change the presence of a token during formatting.
362+
///
363+
/// This allows formats to e.g. replace missing tokens by placeholder tokens.
364+
///
365+
/// - Parameter token: The token whose presence should be changed
366+
/// - Returns: The new presence or `nil` if the presence should not be changed
367+
open func transformTokenPresence(_ token: TokenSyntax) -> SourcePresence? {
368+
return nil
369+
}
370+
351371
// MARK: - Formatting a token
352372

353373
open override func visit(_ token: TokenSyntax) -> TokenSyntax {
@@ -357,6 +377,8 @@ open class BasicFormat: SyntaxRewriter {
357377
let isInitialToken = self.previousToken == nil
358378
let previousToken = self.previousToken ?? token.previousToken(viewMode: viewMode)
359379
let nextToken = token.nextToken(viewMode: viewMode)
380+
let transformedTokenText = self.transformTokenText(token)
381+
let transformedTokenPresence = self.transformTokenPresence(token)
360382

361383
/// In addition to existing trivia of `previousToken`, also considers
362384
/// `previousToken` as ending with whitespace if it and `token` should be
@@ -372,7 +394,7 @@ open class BasicFormat: SyntaxRewriter {
372394
|| (requiresWhitespace(between: previousToken, and: token) && isMutable(previousToken))
373395
}()
374396

375-
/// This method does not consider any posssible mutations to `previousToken`
397+
/// This method does not consider any possible mutations to `previousToken`
376398
/// because newlines should be added to the next token's leading trivia.
377399
let previousTokenWillEndWithNewline: Bool = {
378400
guard let previousToken = previousToken else {
@@ -416,6 +438,14 @@ open class BasicFormat: SyntaxRewriter {
416438
if nextToken.leadingTrivia.startsWithNewline {
417439
return true
418440
}
441+
if nextToken.leadingTrivia.isEmpty {
442+
if nextToken.text.first?.isNewline ?? false {
443+
return true
444+
}
445+
if nextToken.text.isEmpty && nextToken.trailingTrivia.startsWithNewline {
446+
return true
447+
}
448+
}
419449
if requiresNewline(between: token, and: nextToken),
420450
isMutable(nextToken),
421451
!token.trailingTrivia.endsWithNewline,
@@ -436,6 +466,19 @@ open class BasicFormat: SyntaxRewriter {
436466
return trailingTrivia + Trivia(pieces: nextTokenLeadingWhitespace)
437467
}()
438468

469+
/// Whether the leading trivia of the token is followed by a newline.
470+
let leadingTriviaIsFollowedByNewline: Bool = {
471+
if (transformedTokenText ?? token.text).isEmpty && token.trailingTrivia.startsWithNewline {
472+
return true
473+
} else if token.text.first?.isNewline ?? false {
474+
return true
475+
} else if (transformedTokenText ?? token.text).isEmpty && token.trailingTrivia.isEmpty && nextTokenWillStartWithNewline {
476+
return true
477+
} else {
478+
return false
479+
}
480+
}()
481+
439482
if requiresNewline(between: previousToken, and: token) {
440483
// Add a leading newline if the token requires it unless
441484
// - it already starts with a newline or
@@ -455,7 +498,8 @@ open class BasicFormat: SyntaxRewriter {
455498
}
456499
}
457500

458-
if leadingTrivia.indentation(isOnNewline: isInitialToken || previousTokenWillEndWithNewline) == [] {
501+
let isEmptyLine = token.leadingTrivia.isEmpty && leadingTriviaIsFollowedByNewline
502+
if leadingTrivia.indentation(isOnNewline: isInitialToken || previousTokenWillEndWithNewline) == [] && !isEmptyLine {
459503
// If the token starts on a new line and does not have indentation, this
460504
// is the last non-indented token. Store its indentation level
461505
anchorPoints[token] = currentIndentationLevel
@@ -501,14 +545,24 @@ open class BasicFormat: SyntaxRewriter {
501545
leadingTrivia = leadingTrivia.indented(indentation: leadingTriviaIndentation, isOnNewline: previousTokenIsStringLiteralEndingInNewline)
502546
trailingTrivia = trailingTrivia.indented(indentation: trailingTriviaIndentation, isOnNewline: false)
503547

504-
leadingTrivia = leadingTrivia.trimmingTrailingWhitespaceBeforeNewline(isBeforeNewline: false)
548+
leadingTrivia = leadingTrivia.trimmingTrailingWhitespaceBeforeNewline(isBeforeNewline: leadingTriviaIsFollowedByNewline)
505549
trailingTrivia = trailingTrivia.trimmingTrailingWhitespaceBeforeNewline(isBeforeNewline: nextTokenWillStartWithNewline)
506550

507-
if leadingTrivia == token.leadingTrivia && trailingTrivia == token.trailingTrivia {
508-
return token
551+
var result = token.detached
552+
if leadingTrivia != result.leadingTrivia {
553+
result = result.with(\.leadingTrivia, leadingTrivia)
509554
}
510-
511-
return token.detached.with(\.leadingTrivia, leadingTrivia).with(\.trailingTrivia, trailingTrivia)
555+
if trailingTrivia != result.trailingTrivia {
556+
result = result.with(\.trailingTrivia, trailingTrivia)
557+
}
558+
if let transformedTokenText {
559+
let newKind = TokenKind.fromRaw(kind: token.tokenKind.decomposeToRaw().rawKind, text: transformedTokenText)
560+
result = result.with(\.tokenKind, newKind).with(\.presence, .present)
561+
}
562+
if let transformedTokenPresence {
563+
result = result.with(\.presence, transformedTokenPresence)
564+
}
565+
return result
512566
}
513567
}
514568

Sources/SwiftParserDiagnostics/DiagnosticExtensions.swift

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import SwiftDiagnostics
1414
import SwiftBasicFormat
15-
import SwiftSyntax
15+
@_spi(RawSyntax) import SwiftSyntax
1616

1717
extension FixIt {
1818
/// A more complex set of changes that affects multiple syntax nodes and thus
@@ -108,11 +108,46 @@ extension FixIt.MultiNodeChange {
108108

109109
// MARK: - Make present
110110

111-
class MissingNodesBasicFormatter: BasicFormat {
111+
class PresentMakingFormatter: BasicFormat {
112+
init() {
113+
super.init(viewMode: .fixedUp)
114+
}
115+
112116
override func isMutable(_ token: TokenSyntax) -> Bool {
113117
// Assume that all missing nodes will be made present by the Fix-It.
114118
return token.isMissing
115119
}
120+
121+
/// Change the text of all missing tokens to a placeholder with their
122+
/// name for diagnostics.
123+
override func transformTokenText(_ token: TokenSyntax) -> String? {
124+
guard token.isMissing else {
125+
return nil
126+
}
127+
128+
let (rawKind, text) = token.tokenKind.decomposeToRaw()
129+
130+
guard let text = text else {
131+
// The token has a default text that we cannot change.
132+
return nil
133+
}
134+
135+
if text.isEmpty && rawKind != .stringSegment {
136+
// String segments are allowed to have empty text. Replace all other empty
137+
// tokens (e.g. missing identifiers) by a placeholder.
138+
return "<#\(token.tokenKind.nameForDiagnostics)#>"
139+
}
140+
141+
return nil
142+
}
143+
144+
/// Make all tokens present.
145+
override func transformTokenPresence(_ token: TokenSyntax) -> SourcePresence? {
146+
guard token.isMissing else {
147+
return nil
148+
}
149+
return .present
150+
}
116151
}
117152

118153
extension FixIt.MultiNodeChange {
@@ -123,8 +158,7 @@ extension FixIt.MultiNodeChange {
123158
leadingTrivia: Trivia? = nil,
124159
trailingTrivia: Trivia? = nil
125160
) -> Self {
126-
var presentNode = MissingNodesBasicFormatter(viewMode: .fixedUp).rewrite(node, detach: true)
127-
presentNode = PresentMaker().rewrite(presentNode)
161+
var presentNode = PresentMakingFormatter().rewrite(node, detach: true)
128162

129163
if let leadingTrivia {
130164
presentNode = presentNode.with(\.leadingTrivia, leadingTrivia)

Sources/SwiftParserDiagnostics/PresenceUtils.swift

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,29 +43,6 @@ extension SyntaxProtocol {
4343
}
4444
}
4545

46-
/// Transforms a syntax tree by making all missing tokens present.
47-
class PresentMaker: SyntaxRewriter {
48-
init() {
49-
super.init(viewMode: .fixedUp)
50-
}
51-
52-
override func visit(_ token: TokenSyntax) -> TokenSyntax {
53-
if token.isMissing {
54-
let presentToken: TokenSyntax
55-
let (rawKind, text) = token.tokenKind.decomposeToRaw()
56-
if let text = text, (!text.isEmpty || rawKind == .stringSegment) { // string segments can have empty text
57-
presentToken = token.with(\.presence, .present)
58-
} else {
59-
let newKind = TokenKind.fromRaw(kind: rawKind, text: rawKind.defaultText.map(String.init) ?? "<#\(token.tokenKind.nameForDiagnostics)#>")
60-
presentToken = token.with(\.tokenKind, newKind).with(\.presence, .present)
61-
}
62-
return presentToken
63-
} else {
64-
return token
65-
}
66-
}
67-
}
68-
6946
/// Transforms a syntax tree by making all present tokens missing.
7047
class MissingMaker: SyntaxRewriter {
7148
init() {

Tests/SwiftBasicFormatTest/BasicFormatTests.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,41 @@ final class BasicFormatTest: XCTestCase {
250250
)
251251
}
252252

253+
func testMultilineStringLiteralWithBlankLines() {
254+
let source = #"""
255+
assertionFailure("""
256+
257+
First line
258+
259+
Second line
260+
261+
""")
262+
"""#
263+
assertFormatted(source: source, expected: source)
264+
}
265+
266+
func testMultilineStringLiteralWithFirstLineBlank() {
267+
let source = #"""
268+
assertionFailure("""
269+
270+
""")
271+
"""#
272+
assertFormatted(source: source, expected: source)
273+
}
274+
275+
func testNestedMultilineStringLiterals() {
276+
let source = #"""
277+
assertionFailure("""
278+
279+
\("""
280+
First Line
281+
""")
282+
""")
283+
"""#
284+
285+
assertFormatted(source: source, expected: source)
286+
}
287+
253288
func testNestedUserDefinedIndentation() {
254289
assertFormatted(
255290
source: """

0 commit comments

Comments
 (0)