Skip to content

Commit 93f509c

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 9df5d69 commit 93f509c

14 files changed

+105
-66
lines changed

Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ extension PluginMessage.Diagnostic {
8484
self.fixIts = syntaxDiag.fixIts.compactMap {
8585
PluginMessage.Diagnostic.FixIt(
8686
message: $0.message.message,
87-
changes: $0.changes.changes.compactMap {
87+
changes: $0.changes.compactMap {
8888
let range: SourceManager.SourceRange?
8989
let text: String
9090
switch $0 {

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: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,51 @@ 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)
56+
}
57+
public init(message: StaticParserFixIt, changes: [Change]) {
58+
self.init(message: message as FixItMessage, changes: changes)
3259
}
3360
}
3461

35-
extension FixIt.Changes {
62+
extension FixIt.MultiNodeChange {
3663
/// Replaced a present token with a missing node.
3764
/// If `transferTrivia` is `true`, the leading and trailing trivia of the
3865
/// removed node will be transferred to the trailing trivia of the previous token.
@@ -49,26 +76,26 @@ extension FixIt.Changes {
4976
var changes = tokens.map {
5077
FixIt.Change.replace(
5178
oldNode: Syntax($0),
52-
newNode: Syntax(TokenSyntax($0.tokenKind, leadingTrivia: [], trailingTrivia: [], presence: .missing))
79+
newNode: Syntax($0.with(\.presence, .missing))
5380
)
5481
}
5582
if transferTrivia {
56-
changes += FixIt.Changes.transferTriviaAtSides(from: tokens).changes
83+
changes += FixIt.MultiNodeChange.transferTriviaAtSides(from: tokens).primitiveChanges
5784
}
58-
return FixIt.Changes(changes: changes)
85+
return FixIt.MultiNodeChange(primitiveChanges: changes)
5986
}
6087

6188
/// If `transferTrivia` is `true`, the leading and trailing trivia of the
6289
/// removed node will be transferred to the trailing trivia of the previous token.
6390
static func makeMissing<SyntaxType: SyntaxProtocol>(_ node: SyntaxType?, transferTrivia: Bool = true) -> Self {
6491
guard let node = node else {
65-
return FixIt.Changes(changes: [])
92+
return FixIt.MultiNodeChange(primitiveChanges: [])
6693
}
6794
var changes = [FixIt.Change.replace(oldNode: Syntax(node), newNode: MissingMaker().visit(Syntax(node)))]
6895
if transferTrivia {
69-
changes += FixIt.Changes.transferTriviaAtSides(from: [node]).changes
96+
changes += FixIt.MultiNodeChange.transferTriviaAtSides(from: [node]).primitiveChanges
7097
}
71-
return FixIt.Changes(changes: changes)
98+
return FixIt.MultiNodeChange(primitiveChanges: changes)
7299
}
73100

74101
/// Make a node present. If `leadingTrivia` or `trailingTrivia` is specified,
@@ -89,13 +116,13 @@ extension FixIt.Changes {
89116
let nextToken = node.nextToken(viewMode: .sourceAccurate),
90117
leadingTrivia == nil
91118
{
92-
return [
119+
return FixIt.MultiNodeChange(
93120
.replace(
94121
oldNode: Syntax(node),
95122
newNode: Syntax(presentNode).with(\.leadingTrivia, nextToken.leadingTrivia)
96123
),
97-
.replaceLeadingTrivia(token: nextToken, newTrivia: []),
98-
]
124+
.replaceLeadingTrivia(token: nextToken, newTrivia: [])
125+
)
99126
} else if node.leadingTrivia?.isEmpty ?? true,
100127
let previousToken = node.previousToken(viewMode: .fixedUp),
101128
previousToken.presence == .present,
@@ -105,19 +132,19 @@ extension FixIt.Changes {
105132
{
106133
/// If neither this nor the previous token are punctionation make sure they
107134
/// are separated by a space.
108-
return [
135+
return FixIt.MultiNodeChange(
109136
.replace(
110137
oldNode: Syntax(node),
111138
newNode: Syntax(presentNode).with(\.leadingTrivia, .space)
112139
)
113-
]
140+
)
114141
} else {
115-
return [
142+
return FixIt.MultiNodeChange(
116143
.replace(
117144
oldNode: Syntax(node),
118145
newNode: Syntax(presentNode)
119146
)
120-
]
147+
)
121148
}
122149
}
123150

@@ -128,10 +155,10 @@ extension FixIt.Changes {
128155
if !previousToken.trailingTrivia.isEmpty {
129156
presentToken = presentToken.with(\.trailingTrivia, previousToken.trailingTrivia)
130157
}
131-
return [
158+
return FixIt.MultiNodeChange(
132159
.replaceTrailingTrivia(token: previousToken, newTrivia: []),
133-
.replace(oldNode: Syntax(token), newNode: Syntax(presentToken)),
134-
]
160+
.replace(oldNode: Syntax(token), newNode: Syntax(presentToken))
161+
)
135162
} else {
136163
return .makePresent(token)
137164
}
@@ -149,11 +176,11 @@ extension FixIt.Changes {
149176
// Punctuation is generally not followed by spaces in Swift.
150177
// If this action would only add spaces to the punctuation, drop it.
151178
// This generally yields better results.
152-
return []
179+
return FixIt.MultiNodeChange()
153180
}
154-
return [.replaceTrailingTrivia(token: previousToken, newTrivia: mergedTrivia)]
181+
return FixIt.MultiNodeChange(.replaceTrailingTrivia(token: previousToken, newTrivia: mergedTrivia))
155182
} else {
156-
return []
183+
return FixIt.MultiNodeChange()
157184
}
158185
}
159186
}

Sources/SwiftParserDiagnostics/LexerDiagnosticMessages.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ public extension SwiftSyntax.TokenDiagnostic {
186186
.with(\.leadingTrivia, Trivia(pieces: token.leadingTrivia.map(replaceNonBreakingSpace)))
187187
.with(\.trailingTrivia, Trivia(pieces: token.trailingTrivia.map(replaceNonBreakingSpace)))
188188
return [
189-
FixIt(message: .replaceNonBreakingSpaceBySpace, changes: [[.replace(oldNode: Syntax(token), newNode: Syntax(fixedToken))]])
189+
FixIt(message: .replaceNonBreakingSpaceBySpace, changes: [.replace(oldNode: Syntax(token), newNode: Syntax(fixedToken))])
190190
]
191191
case .unicodeCurlyQuote:
192192
let (rawKind, text) = token.tokenKind.decomposeToRaw()
@@ -200,7 +200,7 @@ public extension SwiftSyntax.TokenDiagnostic {
200200

201201
let fixedToken = token.withKind(TokenKind.fromRaw(kind: rawKind, text: replacedText))
202202
return [
203-
FixIt(message: .replaceCurlyQuoteByNormalQuote, changes: [[.replace(oldNode: Syntax(token), newNode: Syntax(fixedToken))]])
203+
FixIt(message: .replaceCurlyQuoteByNormalQuote, changes: [.replace(oldNode: Syntax(token), newNode: Syntax(fixedToken))])
204204
]
205205
case .equalMustHaveConsistentWhitespaceOnBothSides:
206206
let hasLeadingSpace = token.previousToken(viewMode: .all)?.trailingTrivia.contains(where: { $0.isSpaceOrTab }) ?? false
@@ -220,7 +220,7 @@ public extension SwiftSyntax.TokenDiagnostic {
220220
}
221221

222222
return [
223-
FixIt(message: .insertWhitespace, changes: FixIt.Changes(changes: changes))
223+
FixIt(message: .insertWhitespace, changes: changes)
224224
]
225225
default:
226226
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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,21 @@ 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(var payload):
205+
payload.presence = newValue
206+
return RawSyntax(arena: arena, payload: .parsedToken(payload))
207+
case .materializedToken(var payload):
208+
payload.presence = newValue
209+
return RawSyntax(arena: arena, payload: .materializedToken(payload))
210+
default:
211+
preconditionFailure("'withKind()' is called on non-token raw syntax")
212+
}
213+
}
214+
200215
/// The length of the token without leading or trailing trivia, assuming this
201216
/// is a token node.
202217
@_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.

0 commit comments

Comments
 (0)