Skip to content

Commit 1983838

Browse files
committed
When making a token missing, transfer its leading triva to the next token
This makes sure we don’t drop newlines or comments when removing tokens.
1 parent 1e06a49 commit 1983838

File tree

4 files changed

+78
-27
lines changed

4 files changed

+78
-27
lines changed

Sources/SwiftDiagnostics/FixIt.swift

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,39 @@ public protocol FixItMessage {
2626

2727
/// A Fix-It that can be applied to resolve a diagnostic.
2828
public struct FixIt {
29+
public struct Changes: ExpressibleByArrayLiteral {
30+
public var changes: [Change]
31+
32+
public init(changes: [Change]) {
33+
self.changes = changes
34+
}
35+
36+
public init(arrayLiteral elements: FixIt.Change...) {
37+
self.init(changes: elements)
38+
}
39+
40+
public init(combining: [Changes]) {
41+
self.init(changes: combining.flatMap(\.changes))
42+
}
43+
}
44+
2945
public enum Change {
3046
/// Replace `oldNode` by `newNode`.
3147
case replace(oldNode: Syntax, newNode: Syntax)
32-
/// Remove the trailing trivia of the given token
33-
case removeTrailingTrivia(TokenSyntax)
48+
/// Replace the leading trivia on the given token
49+
case replaceLeadingTrivia(token: TokenSyntax, newTrivia: Trivia)
50+
/// Replace the trailing trivia on the given token
51+
case replaceTrailingTrivia(token: TokenSyntax, newTrivia: Trivia)
3452
}
3553

3654
/// A description of what this Fix-It performs.
3755
public let message: FixItMessage
3856

3957
/// The changes that need to be performed when the Fix-It is applied.
40-
public let changes: [Change]
58+
public let changes: Changes
4159

42-
public init(message: FixItMessage, changes: [Change]) {
43-
assert(!changes.isEmpty, "A Fix-It must have at least one change associated with it")
60+
public init(message: FixItMessage, changes: Changes) {
61+
assert(!changes.changes.isEmpty, "A Fix-It must have at least one change associated with it")
4462
self.message = message
4563
self.changes = changes
4664
}

Sources/SwiftParser/Diagnostics/DiagnosticExtensions.swift

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,42 +14,72 @@ import SwiftDiagnostics
1414
import SwiftSyntax
1515

1616
extension FixIt {
17-
init(message: StaticParserFixIt, changes: [Change]) {
17+
init(message: StaticParserFixIt, changes: Changes) {
1818
self.init(message: message as FixItMessage, changes: changes)
1919
}
20+
21+
public init(message: FixItMessage, changes: [Changes]) {
22+
self.init(message: message, changes: FixIt.Changes(combining: changes))
23+
}
24+
25+
init(message: StaticParserFixIt, changes: [Changes]) {
26+
self.init(message: message as FixItMessage, changes: FixIt.Changes(combining: changes))
27+
}
2028
}
2129

22-
extension FixIt.Change {
30+
extension FixIt.Changes {
2331
/// Replaced a present node with a missing node
24-
static func makeMissing(node: TokenSyntax) -> FixIt.Change {
32+
static func makeMissing(node: TokenSyntax) -> Self {
2533
assert(node.presence == .present)
26-
return .replace(
27-
oldNode: Syntax(node),
28-
newNode: Syntax(TokenSyntax(node.tokenKind, leadingTrivia: [], trailingTrivia: [], presence: .missing))
29-
)
34+
var changes = [
35+
FixIt.Change.replace(
36+
oldNode: Syntax(node),
37+
newNode: Syntax(TokenSyntax(node.tokenKind, leadingTrivia: [], trailingTrivia: [], presence: .missing))
38+
)
39+
]
40+
if !node.leadingTrivia.isEmpty, let nextToken = node.nextToken(viewMode: .sourceAccurate) {
41+
changes.append(.replaceLeadingTrivia(token: nextToken, newTrivia: node.leadingTrivia))
42+
}
43+
return FixIt.Changes(changes: changes)
3044
}
3145

32-
static func makePresent<T: SyntaxProtocol>(node: T) -> FixIt.Change {
33-
return .replace(
46+
static func makePresent<T: SyntaxProtocol>(node: T) -> Self {
47+
return [.replace(
3448
oldNode: Syntax(node),
3549
newNode: PresentMaker().visit(Syntax(node))
36-
)
50+
)]
51+
}
52+
53+
/// Make a token present. If `leadingTrivia` or `trailingTrivia` is specified,
54+
/// override the default leading/trailing trivia inferred from `BasicFormat`.
55+
static func makePresent(
56+
node: TokenSyntax,
57+
leadingTrivia: Trivia? = nil,
58+
trailingTrivia: Trivia? = nil
59+
) -> Self {
60+
var presentNode = PresentMaker().visit(Syntax(node)).as(TokenSyntax.self)!
61+
if let leadingTrivia = leadingTrivia {
62+
presentNode.leadingTrivia = leadingTrivia
63+
}
64+
if let trailingTrivia = trailingTrivia {
65+
presentNode.trailingTrivia = trailingTrivia
66+
}
67+
return [.replace(
68+
oldNode: Syntax(node),
69+
newNode: Syntax(presentNode)
70+
)]
3771
}
38-
}
3972

40-
extension Array where Element == FixIt.Change {
4173
/// Makes the `token` present, moving it in front of the previous token's trivia.
42-
static func makePresentBeforeTrivia(token: TokenSyntax) -> [FixIt.Change] {
74+
static func makePresentBeforeTrivia(token: TokenSyntax) -> Self {
4375
if let previousToken = token.previousToken(viewMode: .sourceAccurate) {
4476
let presentToken = PresentMaker().visit(token).withTrailingTrivia(previousToken.trailingTrivia)
4577
return [
46-
.removeTrailingTrivia(previousToken),
78+
.replaceTrailingTrivia(token: previousToken, newTrivia: []),
4779
.replace(oldNode: Syntax(token), newNode: presentToken),
4880
]
4981
} else {
50-
return [
51-
.makePresent(node: token)
52-
]
82+
return .makePresent(node: token)
5383
}
5484
}
5585
}

Sources/SwiftParser/Diagnostics/MissingNodesError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ extension ParseDiagnosticsGenerator {
256256

257257
let fixIt = FixIt(
258258
message: InsertTokenFixIt(missingNodes: missingNodes),
259-
changes: missingNodes.map(FixIt.Change.makePresent)
259+
changes: missingNodes.map(FixIt.Changes.makePresent)
260260
)
261261

262262
var notes: [Note] = []

Tests/SwiftParserTest/Assertions.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ class FixItApplier: SyntaxRewriter {
120120
var changes: [FixIt.Change]
121121

122122
init(diagnostics: [Diagnostic]) {
123-
self.changes = diagnostics.flatMap({ $0.fixIts }).flatMap({ $0.changes })
123+
self.changes = diagnostics.flatMap({ $0.fixIts }).flatMap({ $0.changes.changes })
124124
}
125125

126126
public override func visitAny(_ node: Syntax) -> Syntax? {
@@ -136,15 +136,18 @@ class FixItApplier: SyntaxRewriter {
136136
}
137137

138138
override func visit(_ node: TokenSyntax) -> Syntax {
139+
var modifiedNode = node
139140
for change in changes {
140141
switch change {
141-
case .removeTrailingTrivia(let changeNode) where changeNode.id == node.id:
142-
return Syntax(node.withTrailingTrivia([]))
142+
case .replaceLeadingTrivia(token: let changedNode, newTrivia: let newTrivia) where changedNode.id == node.id:
143+
modifiedNode = node.withLeadingTrivia(newTrivia)
144+
case .replaceTrailingTrivia(token: let changedNode, newTrivia: let newTrivia) where changedNode.id == node.id:
145+
modifiedNode = node.withTrailingTrivia(newTrivia)
143146
default:
144147
break
145148
}
146149
}
147-
return Syntax(node)
150+
return Syntax(modifiedNode)
148151
}
149152

150153
/// Applies all Fix-Its in `diagnostics` to `tree` and returns the fixed syntax tree.

0 commit comments

Comments
 (0)