Skip to content

Commit 4937312

Browse files
committed
Sligthly clean up FixIt.Change and FixIt.Changes
The existing API with `FixIt.Change` and `FixIt.Changes` and the fact that we passed `[FixIt.Changes]` around what fairly weird. Ideally, we would only have `FixIt.Change` and make it powerful enough to represent all the trivia transfer that `FixIt.Changes` is doing. But given that that will be a larger effort, let’s make some smaller changes now that reduce the badness of the public API impact: - Move `FixIt.Changes` to `SwiftParserDiagnostics`, make it internal and rename it to `MultiNodeChange`. That way this type is no longer exposed in the public API, so we can iterate on it or remove it without breaking clients. The only thing that remains exposed is `FixIt.Change`, which we want to continue to support. We will probably add more cases to it in the future. - Make `FixIt.Changes` no longer conform to `ExpressibleByArrayLiteral`. That just makes everything a lot more explicit and easier to follow. - Remove some unecessary initializations of `FixIt.Changes` where `FixIt.Change` was sufficient. - Make `presence` on `TokenSyntax` settable so you can do `token.with(\.presence, .missing)` - Slightly unrelated: Rename SyntaxOtherNodes.swift to TokenSyntax.swift because that’s the only node it contains.
1 parent 96e6850 commit 4937312

12 files changed

+106
-63
lines changed

Sources/SwiftDiagnostics/FixIt.swift

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,6 @@ public protocol FixItMessage {
2525

2626
/// A Fix-It that can be applied to resolve a diagnostic.
2727
public struct FixIt {
28-
public struct Changes: ExpressibleByArrayLiteral {
29-
public var changes: [Change]
30-
31-
public init(changes: [Change]) {
32-
self.changes = changes
33-
}
34-
35-
public init(arrayLiteral elements: FixIt.Change...) {
36-
self.init(changes: elements)
37-
}
38-
39-
public init(combining: [Changes]) {
40-
self.init(changes: combining.flatMap(\.changes))
41-
}
42-
}
43-
4428
public enum Change {
4529
/// Replace `oldNode` by `newNode`.
4630
case replace(oldNode: Syntax, newNode: Syntax)
@@ -54,10 +38,10 @@ public struct FixIt {
5438
public let message: FixItMessage
5539

5640
/// The changes that need to be performed when the Fix-It is applied.
57-
public let changes: Changes
41+
public let changes: [Change]
5842

59-
public init(message: FixItMessage, changes: Changes) {
60-
precondition(!changes.changes.isEmpty, "A Fix-It must have at least one change associated with it")
43+
public init(message: FixItMessage, changes: [Change]) {
44+
precondition(!changes.isEmpty, "A Fix-It must have at least one change associated with it")
6145
self.message = message
6246
self.changes = changes
6347
}

Sources/SwiftParserDiagnostics/DiagnosticExtensions.swift

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,48 @@ import SwiftBasicFormat
1515
import SwiftSyntax
1616

1717
extension FixIt {
18-
public init(message: FixItMessage, changes: [Changes]) {
19-
self.init(message: message, changes: FixIt.Changes(combining: changes))
18+
/// A more complex set of changes that affects multiple syntax nodes and thus
19+
/// produces multiple `FixIt.Change`s. This allows us to e.g. mark a node as
20+
/// missing but keep the trivia by transferring it to the previous or next
21+
/// token.
22+
struct MultiNodeChange {
23+
var primitiveChanges: [Change]
24+
25+
init(primitiveChanges: [Change]) {
26+
self.primitiveChanges = primitiveChanges
27+
}
28+
29+
init(_ primitiveChanges: Change...) {
30+
self.init(primitiveChanges: primitiveChanges)
31+
}
32+
33+
init(combining: [MultiNodeChange]) {
34+
self.init(primitiveChanges: combining.flatMap(\.primitiveChanges))
35+
}
36+
}
37+
38+
init(message: FixItMessage, changes: [MultiNodeChange]) {
39+
self.init(message: message, changes: MultiNodeChange(combining: changes))
40+
}
41+
42+
init(message: FixItMessage, changes: MultiNodeChange) {
43+
self.init(message: message, changes: changes.primitiveChanges)
2044
}
2145

2246
// These overloads shouldn't be needed, but are currently required for the
2347
// Swift 5.5 compiler to handle non-trivial FixIt initializations using
2448
// leading-dot syntax.
2549
// TODO: These can be dropped once we require a minimum of Swift 5.6 to
2650
// compile the library.
27-
init(message: StaticParserFixIt, changes: Changes) {
28-
self.init(message: message as FixItMessage, changes: changes)
51+
init(message: StaticParserFixIt, changes: MultiNodeChange) {
52+
self.init(message: message as FixItMessage, changes: changes.primitiveChanges)
2953
}
30-
init(message: StaticParserFixIt, changes: [Changes]) {
31-
self.init(message: message as FixItMessage, changes: FixIt.Changes(combining: changes))
54+
init(message: StaticParserFixIt, changes: [MultiNodeChange]) {
55+
self.init(message: message as FixItMessage, changes: MultiNodeChange(combining: changes).primitiveChanges)
3256
}
3357
}
3458

35-
extension FixIt.Changes {
59+
extension FixIt.MultiNodeChange {
3660
/// Replaced a present token with a missing node.
3761
/// If `transferTrivia` is `true`, the leading and trailing trivia of the
3862
/// removed node will be transferred to the trailing trivia of the previous token.
@@ -49,26 +73,26 @@ extension FixIt.Changes {
4973
var changes = tokens.map {
5074
FixIt.Change.replace(
5175
oldNode: Syntax($0),
52-
newNode: Syntax(TokenSyntax($0.tokenKind, leadingTrivia: [], trailingTrivia: [], presence: .missing))
76+
newNode: Syntax($0.with(\.presence, .missing))
5377
)
5478
}
5579
if transferTrivia {
56-
changes += FixIt.Changes.transferTriviaAtSides(from: tokens).changes
80+
changes += FixIt.MultiNodeChange.transferTriviaAtSides(from: tokens).primitiveChanges
5781
}
58-
return FixIt.Changes(changes: changes)
82+
return FixIt.MultiNodeChange(primitiveChanges: changes)
5983
}
6084

6185
/// If `transferTrivia` is `true`, the leading and trailing trivia of the
6286
/// removed node will be transferred to the trailing trivia of the previous token.
6387
static func makeMissing<SyntaxType: SyntaxProtocol>(_ node: SyntaxType?, transferTrivia: Bool = true) -> Self {
6488
guard let node = node else {
65-
return FixIt.Changes(changes: [])
89+
return FixIt.MultiNodeChange(primitiveChanges: [])
6690
}
6791
var changes = [FixIt.Change.replace(oldNode: Syntax(node), newNode: MissingMaker().visit(Syntax(node)))]
6892
if transferTrivia {
69-
changes += FixIt.Changes.transferTriviaAtSides(from: [node]).changes
93+
changes += FixIt.MultiNodeChange.transferTriviaAtSides(from: [node]).primitiveChanges
7094
}
71-
return FixIt.Changes(changes: changes)
95+
return FixIt.MultiNodeChange(primitiveChanges: changes)
7296
}
7397

7498
/// Make a node present. If `leadingTrivia` or `trailingTrivia` is specified,
@@ -89,13 +113,13 @@ extension FixIt.Changes {
89113
let nextToken = node.nextToken(viewMode: .sourceAccurate),
90114
leadingTrivia == nil
91115
{
92-
return [
116+
return FixIt.MultiNodeChange(
93117
.replace(
94118
oldNode: Syntax(node),
95119
newNode: Syntax(presentNode).with(\.leadingTrivia, nextToken.leadingTrivia)
96120
),
97-
.replaceLeadingTrivia(token: nextToken, newTrivia: []),
98-
]
121+
.replaceLeadingTrivia(token: nextToken, newTrivia: [])
122+
)
99123
} else if node.leadingTrivia?.isEmpty ?? true,
100124
let previousToken = node.previousToken(viewMode: .fixedUp),
101125
previousToken.presence == .present,
@@ -105,19 +129,19 @@ extension FixIt.Changes {
105129
{
106130
/// If neither this nor the previous token are punctionation make sure they
107131
/// are separated by a space.
108-
return [
132+
return FixIt.MultiNodeChange(
109133
.replace(
110134
oldNode: Syntax(node),
111135
newNode: Syntax(presentNode).with(\.leadingTrivia, .space)
112136
)
113-
]
137+
)
114138
} else {
115-
return [
139+
return FixIt.MultiNodeChange(
116140
.replace(
117141
oldNode: Syntax(node),
118142
newNode: Syntax(presentNode)
119143
)
120-
]
144+
)
121145
}
122146
}
123147

@@ -128,10 +152,10 @@ extension FixIt.Changes {
128152
if !previousToken.trailingTrivia.isEmpty {
129153
presentToken = presentToken.with(\.trailingTrivia, previousToken.trailingTrivia)
130154
}
131-
return [
155+
return FixIt.MultiNodeChange(
132156
.replaceTrailingTrivia(token: previousToken, newTrivia: []),
133-
.replace(oldNode: Syntax(token), newNode: Syntax(presentToken)),
134-
]
157+
.replace(oldNode: Syntax(token), newNode: Syntax(presentToken))
158+
)
135159
} else {
136160
return .makePresent(token)
137161
}
@@ -149,11 +173,11 @@ extension FixIt.Changes {
149173
// Punctuation is generally not followed by spaces in Swift.
150174
// If this action would only add spaces to the punctuation, drop it.
151175
// This generally yields better results.
152-
return []
176+
return FixIt.MultiNodeChange()
153177
}
154-
return [.replaceTrailingTrivia(token: previousToken, newTrivia: mergedTrivia)]
178+
return FixIt.MultiNodeChange(.replaceTrailingTrivia(token: previousToken, newTrivia: mergedTrivia))
155179
} else {
156-
return []
180+
return FixIt.MultiNodeChange()
157181
}
158182
}
159183
}

Sources/SwiftParserDiagnostics/LexerDiagnosticMessages.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ public extension SwiftSyntax.TokenDiagnostic {
184184
.with(\.leadingTrivia, Trivia(pieces: token.leadingTrivia.map(replaceNonBreakingSpace)))
185185
.with(\.trailingTrivia, Trivia(pieces: token.trailingTrivia.map(replaceNonBreakingSpace)))
186186
return [
187-
FixIt(message: .replaceNonBreakingSpaceBySpace, changes: [[.replace(oldNode: Syntax(token), newNode: Syntax(fixedToken))]])
187+
FixIt(message: .replaceNonBreakingSpaceBySpace, changes: [.replace(oldNode: Syntax(token), newNode: Syntax(fixedToken))])
188188
]
189189
case .unicodeCurlyQuote:
190190
let (rawKind, text) = token.tokenKind.decomposeToRaw()
@@ -198,7 +198,7 @@ public extension SwiftSyntax.TokenDiagnostic {
198198

199199
let fixedToken = token.withKind(TokenKind.fromRaw(kind: rawKind, text: replacedText))
200200
return [
201-
FixIt(message: .replaceCurlyQuoteByNormalQuote, changes: [[.replace(oldNode: Syntax(token), newNode: Syntax(fixedToken))]])
201+
FixIt(message: .replaceCurlyQuoteByNormalQuote, changes: [.replace(oldNode: Syntax(token), newNode: Syntax(fixedToken))])
202202
]
203203
default:
204204
return []

Sources/SwiftParserDiagnostics/MissingNodesError.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ extension ParseDiagnosticsGenerator {
314314
func handleMissingSyntax<T: SyntaxProtocol>(
315315
_ node: T,
316316
overridePosition: AbsolutePosition? = nil,
317-
additionalChanges: [FixIt.Changes] = [],
317+
additionalChanges: [FixIt.MultiNodeChange] = [],
318318
additionalHandledNodes: [SyntaxIdentifier] = []
319319
) -> SyntaxVisitorContinueKind {
320320
if shouldSkip(node) {
@@ -354,7 +354,7 @@ extension ParseDiagnosticsGenerator {
354354
}
355355
}
356356

357-
let changes = missingNodes.enumerated().map { (index, missingNode) -> FixIt.Changes in
357+
let changes = missingNodes.enumerated().map { (index, missingNode) -> FixIt.MultiNodeChange in
358358
if index == 0,
359359
let token = missingNode.as(TokenSyntax.self),
360360
let previousTokenKind = missingNode.previousToken(viewMode: .sourceAccurate)?.tokenKind

Sources/SwiftParserDiagnostics/MissingTokenError.swift

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

9898
private func handleInvalidPeriod(invalidToken: TokenSyntax, missingToken: TokenSyntax, invalidTokenContainer: UnexpectedNodesSyntax) -> Bool {
9999
// Trailing trivia is the cause of this diagnostic, don't transfer it.
100-
let changes: [FixIt.Changes] = [
100+
let changes: [FixIt.MultiNodeChange] = [
101101
.makeMissing(invalidToken, transferTrivia: false),
102102
.makePresent(missingToken),
103103
]

Sources/SwiftParserDiagnostics/MultiLineStringLiteralDiagnosticsGenerator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ final class MultiLineStringLiteralIndentatinDiagnosticsGenerator: SyntaxVisitor
115115
message: InvalidIndentationInMultiLineStringLiteralError(kind: currentDiagnostic.kind, lines: currentDiagnostic.lines),
116116
highlights: [],
117117
notes: [Note(node: Syntax(closeQuote), message: .shouldMatchIndentationOfClosingQuote)],
118-
fixIts: [FixIt(message: .changeIndentationToMatchClosingDelimiter, changes: FixIt.Changes(changes: currentDiagnostic.changes))]
118+
fixIts: [FixIt(message: .changeIndentationToMatchClosingDelimiter, changes: currentDiagnostic.changes)]
119119
)
120120

121121
finishedDiagnostics.append((diagnostic, currentDiagnostic.handledNodes))

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -143,23 +143,23 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
143143

144144
// Ignore `correctTokens` that are already present.
145145
let correctAndMissingTokens = correctTokens.filter({ $0.presence == .missing })
146-
var changes: [FixIt.Changes] = []
146+
var changes: [FixIt.MultiNodeChange] = []
147147
if let misplacedToken = misplacedTokens.only, let correctToken = correctTokens.only,
148148
misplacedToken.nextToken(viewMode: .all) == correctToken || misplacedToken.previousToken(viewMode: .all) == correctToken,
149149
correctToken.presence == .missing
150150
{
151151
// We are exchanging two adjacent tokens, transfer the trivia from the incorrect token to the corrected token.
152-
changes += misplacedTokens.map { FixIt.Changes.makeMissing($0, transferTrivia: false) }
152+
changes += misplacedTokens.map { FixIt.MultiNodeChange.makeMissing($0, transferTrivia: false) }
153153
changes.append(
154-
FixIt.Changes.makePresent(
154+
FixIt.MultiNodeChange.makePresent(
155155
correctToken,
156156
leadingTrivia: misplacedToken.leadingTrivia,
157157
trailingTrivia: misplacedToken.trailingTrivia
158158
)
159159
)
160160
} else {
161-
changes += misplacedTokens.map { FixIt.Changes.makeMissing($0) }
162-
changes += correctAndMissingTokens.map { FixIt.Changes.makePresent($0) }
161+
changes += misplacedTokens.map { FixIt.MultiNodeChange.makeMissing($0) }
162+
changes += correctAndMissingTokens.map { FixIt.MultiNodeChange.makePresent($0) }
163163
}
164164
var fixIts: [FixIt] = []
165165
if changes.count > 1 {
@@ -309,7 +309,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
309309
node,
310310
.unexpectedSemicolon,
311311
fixIts: [
312-
FixIt(message: RemoveNodesFixIt(semicolons), changes: semicolons.map { FixIt.Changes.makeMissing($0) })
312+
FixIt(message: RemoveNodesFixIt(semicolons), changes: semicolons.map { FixIt.MultiNodeChange.makeMissing($0) })
313313
]
314314
)
315315
} else if node.first?.as(TokenSyntax.self)?.tokenKind.isIdentifier == true,
@@ -327,7 +327,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
327327
FixIt(
328328
message: .joinIdentifiers,
329329
changes: [
330-
[.replace(oldNode: Syntax(previousToken), newNode: Syntax(TokenSyntax(.identifier(joined), presence: .present)))],
330+
FixIt.MultiNodeChange(.replace(oldNode: Syntax(previousToken), newNode: Syntax(TokenSyntax(.identifier(joined), presence: .present)))),
331331
.makeMissing(tokens),
332332
]
333333
)
@@ -338,7 +338,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
338338
FixIt(
339339
message: .joinIdentifiersWithCamelCase,
340340
changes: [
341-
[.replace(oldNode: Syntax(previousToken), newNode: Syntax(TokenSyntax(.identifier(joinedUsingCamelCase), presence: .present)))],
341+
FixIt.MultiNodeChange(.replace(oldNode: Syntax(previousToken), newNode: Syntax(TokenSyntax(.identifier(joinedUsingCamelCase), presence: .present)))),
342342
.makeMissing(tokens),
343343
]
344344
)
@@ -821,7 +821,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
821821
message: RemoveNodesFixIt(unexpectedName),
822822
changes: [
823823
.makeMissing(unexpectedName),
824-
FixIt.Changes(changes: [.replaceTrailingTrivia(token: previous, newTrivia: .zero)]),
824+
FixIt.MultiNodeChange(.replaceTrailingTrivia(token: previous, newTrivia: .zero)),
825825
]
826826
)
827827
],

Sources/SwiftSyntax/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ add_swift_host_library(SwiftSyntax
1919
SyntaxArena.swift
2020
SyntaxChildren.swift
2121
SyntaxData.swift
22-
SyntaxOtherNodes.swift
2322
SyntaxText.swift
2423
SyntaxTreeViewMode.swift
2524
TokenDiagnostic.swift
25+
TokenSyntax.swift
2626
Utils.swift
2727

2828
Raw/RawSyntax.swift

Sources/SwiftSyntax/Raw/RawSyntaxTokenView.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,28 @@ public struct RawSyntaxTokenView {
197197
}
198198
}
199199

200+
/// Returns a `RawSyntax` node with the presence changed to `newValue`.
201+
@_spi(RawSyntax)
202+
public func withPresence(_ newValue: SourcePresence, arena: SyntaxArena) -> RawSyntax {
203+
switch raw.rawData.payload {
204+
case .parsedToken(_):
205+
// The wholeText can't be continuous anymore. Make a materialized token.
206+
return .makeMaterializedToken(
207+
kind: formKind(),
208+
leadingTrivia: formLeadingTrivia(),
209+
trailingTrivia: formTrailingTrivia(),
210+
presence: newValue,
211+
tokenDiagnostic: tokenDiagnostic,
212+
arena: arena
213+
)
214+
case .materializedToken(var payload):
215+
payload.presence = newValue
216+
return RawSyntax(arena: arena, payload: .materializedToken(payload))
217+
default:
218+
preconditionFailure("'withKind()' is called on non-token raw syntax")
219+
}
220+
}
221+
200222
/// The length of the token without leading or trailing trivia, assuming this
201223
/// is a token node.
202224
@_spi(RawSyntax)

Sources/SwiftSyntax/SyntaxData.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,14 @@ struct SyntaxData {
336336
return self
337337
}
338338
}
339+
340+
func withPresence(_ presence: SourcePresence, arena: SyntaxArena) -> SyntaxData {
341+
if let raw = raw.tokenView?.withPresence(presence, arena: arena) {
342+
return replacingSelf(raw, arena: arena)
343+
} else {
344+
return self
345+
}
346+
}
339347
}
340348

341349
#if DEBUG

Sources/SwiftSyntax/SyntaxOtherNodes.swift renamed to Sources/SwiftSyntax/TokenSyntax.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ public struct TokenSyntax: SyntaxProtocol, SyntaxHashable {
5555
}
5656

5757
public var presence: SourcePresence {
58-
return tokenView.presence
58+
get {
59+
return tokenView.presence
60+
}
61+
set {
62+
self = TokenSyntax(data.withPresence(newValue, arena: SyntaxArena()))
63+
}
5964
}
6065

6166
/// The text of the token as written in the source code, without any trivia.

Tests/SwiftParserTest/Assertions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ class FixItApplier: SyntaxRewriter {
285285
return true
286286
}
287287
}
288-
.flatMap { $0.changes.changes }
288+
.flatMap { $0.changes }
289289
}
290290

291291
public override func visitAny(_ node: Syntax) -> Syntax? {

0 commit comments

Comments
 (0)