Skip to content

Commit d57c592

Browse files
committed
Retain leading and trailing trivia from removed tokens
1 parent fb78441 commit d57c592

File tree

4 files changed

+105
-32
lines changed

4 files changed

+105
-32
lines changed

Sources/SwiftParser/Diagnostics/DiagnosticExtensions.swift

Lines changed: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,17 @@ extension FixIt {
3232
}
3333

3434
extension FixIt.Changes {
35-
/// Replaced a present token with a missing node
36-
static func makeMissing(_ token: TokenSyntax) -> Self {
37-
return makeMissing([token])
35+
/// Replaced a present token with a missing node.
36+
/// If `transferTrivia` is `true`, the leading and trailing trivia of the
37+
/// removed node will be transferred to the trailing trivia of the previous token.
38+
static func makeMissing(_ token: TokenSyntax, transferTrivia: Bool = true) -> Self {
39+
return makeMissing([token], transferTrivia: transferTrivia)
3840
}
3941

40-
/// Replace present tokens with missing tokens
41-
static func makeMissing(_ tokens: [TokenSyntax]) -> Self {
42+
/// Replace present tokens with missing tokens.
43+
/// If `transferTrivia` is `true`, the leading and trailing trivia of the
44+
/// removed node will be transferred to the trailing trivia of the previous token.
45+
static func makeMissing(_ tokens: [TokenSyntax], transferTrivia: Bool = true) -> Self {
4246
assert(!tokens.isEmpty)
4347
assert(tokens.allSatisfy({ $0.presence == .present }))
4448
var changes = tokens.map {
@@ -47,19 +51,20 @@ extension FixIt.Changes {
4751
newNode: Syntax(TokenSyntax($0.tokenKind, leadingTrivia: [], trailingTrivia: [], presence: .missing))
4852
)
4953
}
50-
if let firstToken = tokens.first,
51-
firstToken.leadingTrivia.isEmpty == false,
52-
let nextToken = tokens.last?.nextToken(viewMode: .sourceAccurate),
53-
!nextToken.leadingTrivia.contains(where: { $0.isNewline }) {
54-
changes.append(.replaceLeadingTrivia(token: nextToken, newTrivia: firstToken.leadingTrivia))
54+
if transferTrivia {
55+
changes += FixIt.Changes.transferTriviaAtSides(from: tokens).changes
5556
}
5657
return FixIt.Changes(changes: changes)
5758
}
5859

59-
static func makeMissing<SyntaxType: SyntaxProtocol>(_ node: SyntaxType) -> Self {
60-
return FixIt.Changes(changes: [
61-
.replace(oldNode: Syntax(node), newNode: MissingMaker().visit(Syntax(node)))
62-
])
60+
/// If `transferTrivia` is `true`, the leading and trailing trivia of the
61+
/// removed node will be transferred to the trailing trivia of the previous token.
62+
static func makeMissing<SyntaxType: SyntaxProtocol>(_ node: SyntaxType, transferTrivia: Bool = true) -> Self {
63+
var changes = [FixIt.Change.replace(oldNode: Syntax(node), newNode: MissingMaker().visit(Syntax(node)))]
64+
if transferTrivia {
65+
changes += FixIt.Changes.transferTriviaAtSides(from: [node]).changes
66+
}
67+
return FixIt.Changes(changes: changes)
6368
}
6469

6570
/// Make a node present. If `leadingTrivia` or `trailingTrivia` is specified,
@@ -109,4 +114,74 @@ extension FixIt.Changes {
109114
return .makePresent(token)
110115
}
111116
}
117+
118+
/// Transfers the leading and trivia trivia of `nodes` to the trailing trivia
119+
/// of the previous token. While doing this, it tries to be smart, merging trivia
120+
/// where it makes sense and refusing to add e.g. a space after punctuation,
121+
/// where it usually doesn't make sense.
122+
static func transferTriviaAtSides<SyntaxType: SyntaxProtocol>(from nodes: [SyntaxType]) -> Self {
123+
let removedTriviaAtSides = Trivia.merged(nodes.first?.leadingTrivia ?? [], nodes.last?.trailingTrivia ?? [])
124+
if !removedTriviaAtSides.isEmpty, let previousToken = nodes.first?.previousToken(viewMode: .sourceAccurate) {
125+
let mergedTrivia = Trivia.merged(previousToken.trailingTrivia, removedTriviaAtSides)
126+
if previousToken.tokenKind.isPunctuation, mergedTrivia.allSatisfy({ $0.isSpaceOrTab }) {
127+
// Punctuation is generally not followed by spaces in Swift.
128+
// If this action would only add spaces to the punctuation, drop it.
129+
// This generally yields better results.
130+
return []
131+
}
132+
return [.replaceTrailingTrivia(token: previousToken, newTrivia: mergedTrivia)]
133+
} else {
134+
return []
135+
}
136+
}
137+
}
138+
139+
extension Trivia {
140+
/// Decomposes the trivia into pieces that all have count 1
141+
var decomposed: Trivia {
142+
let pieces = self.flatMap({
143+
switch $0 {
144+
case .spaces(let count):
145+
return Array(repeating: TriviaPiece.spaces(1), count: count)
146+
case .tabs(let count):
147+
return Array(repeating: TriviaPiece.tabs(1), count: count)
148+
case .verticalTabs(let count):
149+
return Array(repeating: TriviaPiece.verticalTabs(1), count: count)
150+
case .formfeeds(let count):
151+
return Array(repeating: TriviaPiece.formfeeds(1), count: count)
152+
case .newlines(let count):
153+
return Array(repeating: TriviaPiece.newlines(1), count: count)
154+
case .carriageReturns(let count):
155+
return Array(repeating: TriviaPiece.carriageReturns(1), count: count)
156+
case .carriageReturnLineFeeds(let count):
157+
return Array(repeating: TriviaPiece.carriageReturnLineFeeds(1), count: count)
158+
case .lineComment, .blockComment, .docLineComment, .docBlockComment, .unexpectedText, .shebang:
159+
return [$0]
160+
}
161+
})
162+
return Trivia(pieces: pieces)
163+
}
164+
165+
/// Concatenate `lhs` and `rhs`, merging an infix that is shared between both trivia pieces.
166+
static func merged(_ lhs: Trivia, _ rhs: Trivia) -> Self {
167+
let lhs = lhs.decomposed
168+
let rhs = rhs.decomposed
169+
for infixLength in (0...Swift.min(lhs.count, rhs.count)).reversed() {
170+
if lhs.suffix(infixLength) == rhs.suffix(infixLength) {
171+
return lhs + Trivia(pieces: Array(rhs.dropFirst(infixLength)))
172+
}
173+
}
174+
return lhs + rhs
175+
}
176+
}
177+
178+
extension TriviaPiece {
179+
var isSpaceOrTab: Bool {
180+
switch self {
181+
case .spaces, .tabs:
182+
return true
183+
default:
184+
return false
185+
}
186+
}
112187
}

Sources/SwiftParser/Diagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -96,22 +96,19 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
9696

9797
// Ignore `correctTokens` that are already present.
9898
let correctAndMissingTokens = correctTokens.filter({ $0.presence == .missing })
99-
var changes = misplacedTokens.map(FixIt.Changes.makeMissing)
100-
for correctToken in correctAndMissingTokens {
101-
if misplacedTokens.count == 1, let misplacedToken = misplacedTokens.first,
102-
misplacedToken.nextToken(viewMode: .all) == correctToken || misplacedToken.previousToken(viewMode: .all) == correctToken {
103-
changes.append(FixIt.Changes.makePresent(
104-
correctToken,
105-
leadingTrivia: misplacedToken.leadingTrivia,
106-
trailingTrivia: misplacedToken.trailingTrivia
107-
))
108-
} else {
109-
changes.append(FixIt.Changes.makePresent(
110-
correctToken,
111-
leadingTrivia: nil,
112-
trailingTrivia: nil
113-
))
114-
}
99+
var changes: [FixIt.Changes] = []
100+
if let misplacedToken = misplacedTokens.only, let correctToken = correctTokens.only,
101+
misplacedToken.nextToken(viewMode: .all) == correctToken || misplacedToken.previousToken(viewMode: .all) == correctToken {
102+
// We are exchanging two adjacent tokens, transfer the trivia from the incorrect token to the corrected token.
103+
changes += misplacedTokens.map { FixIt.Changes.makeMissing($0, transferTrivia: false) }
104+
changes.append(FixIt.Changes.makePresent(
105+
correctToken,
106+
leadingTrivia: misplacedToken.leadingTrivia,
107+
trailingTrivia: misplacedToken.trailingTrivia
108+
))
109+
} else {
110+
changes += misplacedTokens.map { FixIt.Changes.makeMissing($0) }
111+
changes += correctAndMissingTokens.map { FixIt.Changes.makePresent($0) }
115112
}
116113
var fixIts: [FixIt] = []
117114
if changes.count > 1 {
@@ -170,7 +167,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
170167
addDiagnostic(node, TryCannotBeUsed(nextToken: nextToken))
171168
} else if let semicolons = node.onlyTokens(satisfying: { $0.tokenKind == .semicolon }) {
172169
addDiagnostic(node, .unexpectedSemicolon, fixIts: [
173-
FixIt(message: RemoveNodesFixIt(semicolons), changes: semicolons.map(FixIt.Changes.makeMissing))
170+
FixIt(message: RemoveNodesFixIt(semicolons), changes: semicolons.map { FixIt.Changes.makeMissing($0) })
174171
])
175172
} else if node.first?.as(TokenSyntax.self)?.tokenKind.isIdentifier == true,
176173
let previousToken = node.previousToken(viewMode: .sourceAccurate),

Tests/SwiftParserTest/translated/InitDeinitTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ final class InitDeinitTests: XCTestCase {
156156
DiagnosticSpec(message: "deinitializers cannot have parameters", fixIts: ["remove function signature"]),
157157
], fixedSource: """
158158
class FooClassDeinitializerA {
159-
deinit{}
159+
deinit {}
160160
}
161161
"""
162162
)

Tests/SwiftParserTest/translated/SwitchTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ final class SwitchTests: XCTestCase {
215215
], fixedSource: """
216216
switch x {
217217
case 0:
218+
218219
case 1:
219220
x = 0
220221
}

0 commit comments

Comments
 (0)