Skip to content

Commit 91d33db

Browse files
authored
Merge pull request #1964 from ahoppen/ahoppen/present-making-formatter
2 parents a4d4881 + 0387483 commit 91d33db

File tree

4 files changed

+243
-40
lines changed

4 files changed

+243
-40
lines changed

Sources/SwiftBasicFormat/BasicFormat.swift

Lines changed: 94 additions & 13 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
///
@@ -48,6 +48,14 @@ open class BasicFormat: SyntaxRewriter {
4848
/// been visited yet.
4949
private var previousToken: TokenSyntax? = nil
5050

51+
/// The number of ancestors that are `StringLiteralExprSyntax`.
52+
private var stringLiteralNestingLevel = 0
53+
54+
/// Whether we are currently visiting the subtree of a `StringLiteralExprSyntax`.
55+
private var isInsideStringLiteral: Bool {
56+
return stringLiteralNestingLevel > 0
57+
}
58+
5159
public init(
5260
indentationWidth: Trivia? = nil,
5361
initialIndentation: Trivia = [],
@@ -86,6 +94,9 @@ open class BasicFormat: SyntaxRewriter {
8694
}
8795

8896
open override func visitPre(_ node: Syntax) {
97+
if node.is(StringLiteralExprSyntax.self) {
98+
stringLiteralNestingLevel += 1
99+
}
89100
if requiresIndent(node) {
90101
if let firstToken = node.firstToken(viewMode: viewMode),
91102
let tokenIndentation = firstToken.leadingTrivia.indentation(isOnNewline: false),
@@ -101,6 +112,9 @@ open class BasicFormat: SyntaxRewriter {
101112
}
102113

103114
open override func visitPost(_ node: Syntax) {
115+
if node.is(StringLiteralExprSyntax.self) {
116+
stringLiteralNestingLevel -= 1
117+
}
104118
if requiresIndent(node) {
105119
decreaseIndentationLevel()
106120
}
@@ -274,7 +288,7 @@ open class BasicFormat: SyntaxRewriter {
274288
(.keyword(.`init`), .leftParen), // init()
275289
(.keyword(.self), .period), // self.someProperty
276290
(.keyword(.Self), .period), // self.someProperty
277-
(.keyword(.set), .leftParen), // var mYar: Int { set(value) {} }
291+
(.keyword(.set), .leftParen), // var mVar: Int { set(value) {} }
278292
(.keyword(.subscript), .leftParen), // subscript(x: Int)
279293
(.keyword(.super), .period), // super.someProperty
280294
(.leftBrace, .rightBrace), // {}
@@ -351,6 +365,26 @@ open class BasicFormat: SyntaxRewriter {
351365
return true
352366
}
353367

368+
/// Change the text of a token during formatting.
369+
///
370+
/// This allows formats to e.g. replace missing tokens by placeholder tokens.
371+
///
372+
/// - Parameter token: The token whose text should be changed
373+
/// - Returns: The new text or `nil` if the text should not be changed
374+
open func transformTokenText(_ token: TokenSyntax) -> String? {
375+
return nil
376+
}
377+
378+
/// Change the presence of a token during formatting.
379+
///
380+
/// This allows formats to e.g. replace missing tokens by placeholder tokens.
381+
///
382+
/// - Parameter token: The token whose presence should be changed
383+
/// - Returns: The new presence or `nil` if the presence should not be changed
384+
open func transformTokenPresence(_ token: TokenSyntax) -> SourcePresence? {
385+
return nil
386+
}
387+
354388
// MARK: - Formatting a token
355389

356390
open override func visit(_ token: TokenSyntax) -> TokenSyntax {
@@ -360,6 +394,8 @@ open class BasicFormat: SyntaxRewriter {
360394
let isInitialToken = self.previousToken == nil
361395
let previousToken = self.previousToken ?? token.previousToken(viewMode: viewMode)
362396
let nextToken = token.nextToken(viewMode: viewMode)
397+
let transformedTokenText = self.transformTokenText(token)
398+
let transformedTokenPresence = self.transformTokenPresence(token)
363399

364400
/// In addition to existing trivia of `previousToken`, also considers
365401
/// `previousToken` as ending with whitespace if it and `token` should be
@@ -375,7 +411,7 @@ open class BasicFormat: SyntaxRewriter {
375411
|| (requiresWhitespace(between: previousToken, and: token) && isMutable(previousToken))
376412
}()
377413

378-
/// This method does not consider any posssible mutations to `previousToken`
414+
/// This method does not consider any possible mutations to `previousToken`
379415
/// because newlines should be added to the next token's leading trivia.
380416
let previousTokenWillEndWithNewline: Bool = {
381417
guard let previousToken = previousToken else {
@@ -419,6 +455,14 @@ open class BasicFormat: SyntaxRewriter {
419455
if nextToken.leadingTrivia.startsWithNewline {
420456
return true
421457
}
458+
if nextToken.leadingTrivia.isEmpty {
459+
if nextToken.text.first?.isNewline ?? false {
460+
return true
461+
}
462+
if nextToken.text.isEmpty && nextToken.trailingTrivia.startsWithNewline {
463+
return true
464+
}
465+
}
422466
if requiresNewline(between: token, and: nextToken),
423467
isMutable(nextToken),
424468
!token.trailingTrivia.endsWithNewline,
@@ -439,6 +483,19 @@ open class BasicFormat: SyntaxRewriter {
439483
return trailingTrivia + Trivia(pieces: nextTokenLeadingWhitespace)
440484
}()
441485

486+
/// Whether the leading trivia of the token is followed by a newline.
487+
let leadingTriviaIsFollowedByNewline: Bool = {
488+
if (transformedTokenText ?? token.text).isEmpty && token.trailingTrivia.startsWithNewline {
489+
return true
490+
} else if token.text.first?.isNewline ?? false {
491+
return true
492+
} else if (transformedTokenText ?? token.text).isEmpty && token.trailingTrivia.isEmpty && nextTokenWillStartWithNewline {
493+
return true
494+
} else {
495+
return false
496+
}
497+
}()
498+
442499
if requiresNewline(between: previousToken, and: token) {
443500
// Add a leading newline if the token requires it unless
444501
// - it already starts with a newline or
@@ -458,9 +515,12 @@ open class BasicFormat: SyntaxRewriter {
458515
}
459516
}
460517

461-
if leadingTrivia.indentation(isOnNewline: isInitialToken || previousTokenWillEndWithNewline) == [] {
518+
if leadingTrivia.indentation(isOnNewline: isInitialToken || previousTokenWillEndWithNewline) == [] && !token.isStringSegment {
462519
// If the token starts on a new line and does not have indentation, this
463-
// is the last non-indented token. Store its indentation level
520+
// is the last non-indented token. Store its indentation level.
521+
// But never consider string segments as anchor points since you can’t
522+
// indent individual lines of a multi-line string literals without breaking
523+
// their integrity.
464524
anchorPoints[token] = currentIndentationLevel
465525
}
466526

@@ -488,14 +548,17 @@ open class BasicFormat: SyntaxRewriter {
488548
var leadingTriviaIndentation = self.currentIndentationLevel
489549
var trailingTriviaIndentation = self.currentIndentationLevel
490550

491-
// If the trivia contain user-defined indentation, find their anchor point
551+
// If the trivia contains user-defined indentation, find their anchor point
492552
// and indent the token relative to that anchor point.
493-
if leadingTrivia.containsIndentation(isOnNewline: previousTokenWillEndWithNewline),
553+
// Always indent string literals relative to their anchor point because
554+
// their indentation has structural meaning and we just want to maintain
555+
// what the user wrote.
556+
if leadingTrivia.containsIndentation(isOnNewline: previousTokenWillEndWithNewline) || isInsideStringLiteral,
494557
let anchorPointIndentation = self.anchorPointIndentation(for: token)
495558
{
496559
leadingTriviaIndentation = anchorPointIndentation
497560
}
498-
if combinedTrailingTrivia.containsIndentation(isOnNewline: previousTokenWillEndWithNewline),
561+
if combinedTrailingTrivia.containsIndentation(isOnNewline: previousTokenWillEndWithNewline) || isInsideStringLiteral,
499562
let anchorPointIndentation = self.anchorPointIndentation(for: token)
500563
{
501564
trailingTriviaIndentation = anchorPointIndentation
@@ -504,18 +567,36 @@ open class BasicFormat: SyntaxRewriter {
504567
leadingTrivia = leadingTrivia.indented(indentation: leadingTriviaIndentation, isOnNewline: previousTokenIsStringLiteralEndingInNewline)
505568
trailingTrivia = trailingTrivia.indented(indentation: trailingTriviaIndentation, isOnNewline: false)
506569

507-
leadingTrivia = leadingTrivia.trimmingTrailingWhitespaceBeforeNewline(isBeforeNewline: false)
570+
leadingTrivia = leadingTrivia.trimmingTrailingWhitespaceBeforeNewline(isBeforeNewline: leadingTriviaIsFollowedByNewline)
508571
trailingTrivia = trailingTrivia.trimmingTrailingWhitespaceBeforeNewline(isBeforeNewline: nextTokenWillStartWithNewline)
509572

510-
if leadingTrivia == token.leadingTrivia && trailingTrivia == token.trailingTrivia {
511-
return token
573+
var result = token.detached
574+
if leadingTrivia != result.leadingTrivia {
575+
result = result.with(\.leadingTrivia, leadingTrivia)
512576
}
513-
514-
return token.detached.with(\.leadingTrivia, leadingTrivia).with(\.trailingTrivia, trailingTrivia)
577+
if trailingTrivia != result.trailingTrivia {
578+
result = result.with(\.trailingTrivia, trailingTrivia)
579+
}
580+
if let transformedTokenText {
581+
let newKind = TokenKind.fromRaw(kind: token.tokenKind.decomposeToRaw().rawKind, text: transformedTokenText)
582+
result = result.with(\.tokenKind, newKind).with(\.presence, .present)
583+
}
584+
if let transformedTokenPresence {
585+
result = result.with(\.presence, transformedTokenPresence)
586+
}
587+
return result
515588
}
516589
}
517590

518591
fileprivate extension TokenSyntax {
592+
var isStringSegment: Bool {
593+
if case .stringSegment = self.tokenKind {
594+
return true
595+
} else {
596+
return false
597+
}
598+
}
599+
519600
var isStringSegmentWithLastCharacterBeingNewline: Bool {
520601
switch self.tokenKind {
521602
case .stringSegment(let segment):

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() {

0 commit comments

Comments
 (0)