8000 Rewrite FixItApplier to be string based · swiftlang/swift-syntax@be75975 · GitHub
[go: up one dir, main page]

Skip to content

Commit be75975

Browse files
committed
Rewrite FixItApplier to be string based
1 parent 894fc62 commit be75975

File tree

6 files changed

+254
-148
lines changed

6 files changed

+254
-148
lines changed

Sources/SwiftParserDiagnostics/DiagnosticExtensions.swift

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,28 +48,21 @@ extension FixIt {
4848

4949
extension FixIt.MultiNodeChange {
5050
/// Replaced a present token with a missing node.
51+
///
5152
/// If `transferTrivia` is `true`, the leading and trailing trivia of the
5253
/// removed node will be transferred to the trailing trivia of the previous token.
5354
static func makeMissing(_ token: TokenSyntax, transferTrivia: Bool = true) -> Self {
5455
return makeMissing([token], transferTrivia: transferTrivia)
5556
}
5657

5758
/// Replace present tokens with missing tokens.
58-
/// If `transferTrivia` is `true`, the leading and trailing trivia of the
59-
/// removed node will be transferred to the trailing trivia of the previous token.
59+
///
60+
/// If `transferTrivia` is `true`, the leading trivia of the first token and
61+
/// the trailing trivia of the last token will be transferred to their adjecent
62+
/// tokens.
6063
static func makeMissing(_ tokens: [TokenSyntax], transferTrivia: Bool = true) -> Self {
61-
precondition(!tokens.isEmpty)
62-
precondition(tokens.allSatisfy({ $0.isPresent }))
63-
var changes = tokens.map {
64-
FixIt.Change.replace(
65-
oldNode: Syntax($0),
66-
newNode: Syntax($0.with(\.presence, .missing))
67-
)
68-
}
69-
if transferTrivia {
70-
changes += FixIt.MultiNodeChange.transferTriviaAtSides(from: tokens).primitiveChanges
71-
}
72-
return FixIt.MultiNodeChange(primitiveChanges: changes)
64+
precondition(tokens.allSatisfy(\.isPresent))
65+
return .makeMissing(tokens.map(Syntax.init), transferTrivia: transferTrivia)
7366
}
7467

7568
/// If `transferTrivia` is `true`, the leading and trailing trivia of the
@@ -104,6 +97,25 @@ extension FixIt.MultiNodeChange {
10497
return FixIt.MultiNodeChange()
10598
}
10699
}
100+
101+
/// Replace present nodes with their missing equivalents.
102+
///
103+
/// If `transferTrivia` is `true`, the leading trivia of the first node and
104+
/// the trailing trivia of the last node will be transferred to their adjecent
105+
/// tokens.
106+
static func makeMissing(_ nodes: [Syntax], transferTrivia: Bool = true) -> Self {
107+
precondition(!nodes.isEmpty)
108+
var changes = nodes.map {
109+
FixIt.Change.replace(
110+
oldNode: $0,
111+
newNode: MissingMaker().rewrite($0, detach: true)
112+
)
113+
}
114+
if transferTrivia {
115+
changes += FixIt.MultiNodeChange.transferTriviaAtSides(from: nodes).primitiveChanges
116+
}
117+
return FixIt.MultiNodeChange(primitiveChanges: changes)
118+
}
107119
}
108120

109121
// MARK: - Make present

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
182182
correctToken.isMissing
183183
{
184184
// We are exchanging two adjacent tokens, transfer the trivia from the incorrect token to the corrected token.
185-
changes += misplacedTokens.map { FixIt.MultiNodeChange.makeMissing($0, transferTrivia: false) }
185+
changes.append(FixIt.MultiNodeChange.makeMissing(misplacedTokens, transferTrivia: false))
186186
changes.append(
187187
FixIt.MultiNodeChange.makePresent(
188188
correctToken,
@@ -236,7 +236,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
236236
exchangeTokens(
237237
unexpected: misplacedSpecifiers,
238238
F438 unexpectedTokenCondition: { EffectSpecifier(token: $0) != nil },
239-
correctTokens: [effectSpecifiers?.throwsSpecifier, effectSpecifiers?.asyncSpecifier],
239+
correctTokens: [effectSpecifiers?.asyncSpecifier, effectSpecifiers?.throwsSpecifier],
240240
message: { EffectsSpecifierAfterArrow(effectsSpecifiersAfterArrow: $0) },
241241
moveFixIt: { MoveTokensInFrontOfFixIt(movedTokens: $0, inFrontOf: .arrow) },
242242
removeRedundantFixIt: { RemoveRedundantFixIt(removeTokens: $0) }
@@ -761,20 +761,17 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
761761
if let unexpected = node.unexpectedBetweenRequirementAndTrailingComma,
762762
let token = unexpected.presentTokens(satisfying: { $0.tokenKind == .binaryOperator("&&") }).first,
763763
let trailingComma = node.trailingComma,
764-
trailingComma.isMissing,
765-
let previous = node.unexpectedBetweenRequirementAndTrailingComma?.previousToken(viewMode: .sourceAccurate)
764+
trailingComma.isMissing
766765
{
767-
768766
addDiagnostic(
769767
unexpected,
770768
.expectedCommaInWhereClause,
771769
fixIts: [
772770
FixIt(
773771
message: ReplaceTokensFixIt(replaceTokens: [token], replacements: [.commaToken()]),
774772
changes: [
775-
.makeMissing(token),
776-
.makePresent(trailingComma),
777-
FixIt.MultiNodeChange(.replaceTrailingTrivia(token: previous, newTrivia: [])),
773+
.makeMissing(token, transferTrivia: false),
774+
.makePresent(trailingComma, leadingTrivia: token.leadingTrivia, trailingTrivia: token.trailingTrivia),
778775
]
779776
)
780777
],
@@ -815,7 +812,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
815812
fixIts: [
816813
FixIt(
817814
message: RemoveNodesFixIt(nodes),
818-
changes: nodes.map { .makeMissing($0) }
815+
changes: .makeMissing(nodes)
819816
)
820817
],
821818
handledNodes: nodes.map { $0.id }
@@ -1539,7 +1536,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
15391536
fixIts: [
15401537
FixIt(
15411538
message: RemoveNodesFixIt(rawDelimiters),
1542-
changes: rawDelimiters.map { .makeMissing($0) }
1539+
changes: .makeMissing(rawDelimiters)
15431540
)
15441541
],
15451542
handledNodes: rawDelimiters.map { $0.id }
@@ -1859,8 +1856,8 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
18591856
replacements: [node.colon]
18601857
),
18611858
changes: [
1862-
FixIt.MultiNodeChange.makeMissing(equalToken),
1863-
FixIt.MultiNodeChange.makePresent(node.colon),
1859+
.makeMissing(equalToken, transferTrivia: false),
1860+
.makePresent(node.colon, leadingTrivia: equalToken.leadingTrivia, trailingTrivia: equalToken.trailingTrivia),
18641861
]
18651862
)
18661863
],
@@ -1968,8 +1965,9 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
19681965
FixIt(
19691966
message: fixItMessage,
19701967
changes: [
1971-
FixIt.MultiNodeChange.makePresent(detail.detail)
1972-
] + unexpectedTokens.map { FixIt.MultiNodeChange.makeMissing($0) }
1968+
.makePresent(detail.detail),
1969+
.makeMissing(unexpectedTokens),
1970+
]
19731971
)
19741972
],
19751973
handledNodes: [detail.id] + unexpectedTokens.map(\.id)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftDiagnostics
14+
import SwiftSyntax
15+
16+
public enum FixItApplier {
17+
fileprivate struct Edit: Equatable {
18+
let startUtf8Offset: Int
19+
let endUtf8Offset: Int
20+
let replacement: String
21+
}
22+
23+
/// Applies selected or all Fix-Its from the provided diagnostics to a given syntax tree.
24+
///
25+
/// - Parameters:
26+
/// - diagnostics: An array of `Diagnostic` objects, each containing one or more Fix-Its.
27+
/// - filterByMessages: An optional array of message strings to filter which Fix-Its to apply.
28+
/// If `nil`, all Fix-Its in `diagnostics` are applied.
29+
/// - tree: The syntax tree to which the Fix-Its will be applied.
30+
///
31+
/// - Returns: A ``String`` representation of the modified syntax tree after applying the Fix-Its.
32+
public static func applyFixes(
33+
from diagnostics: [Diagnostic],
34+
filterByMessages messages: [String]?,
35+
to tree: any SyntaxProtocol
36+
) -> String {
37+
let messages = messages ?? diagnostics.compactMap { $0.fixIts.first?.message.message }
38+
39+
let changes =
40+
diagnostics
41+
.flatMap(\.fixIts)
42+
.filter { messages.contains($0.message.message) }
43+
.flatMap(\.changes)
44+
45+
var edits: [Edit] = []
46+
47+
for change in changes {
48+
switch change {
49+
case .replace(let oldNode, let newNode):
50+
edits.append(
51+
Edit(
52+
startUtf8Offset: oldNode.position.utf8Offset,
53+
endUtf8Offset: oldNode.endPosition.utf8Offset,
54+
replacement: newNode.description
55+
)
56+
)
57+
58+
case .replaceLeadingTrivia(let token, let newTrivia):
59+
edits.append(
60+
Edit(
61+
startUtf8Offset: token.position.utf8Offset,
62+
endUtf8Offset: token.endPosition.utf8Offset,
63+
replacement: token.with(\.leadingTrivia, newTrivia).description
64+
)
65+
)
66+
67+
case .replaceTrailingTrivia(let token, let newTrivia):
68+
edits.append(
69+
Edit(
70+
startUtf8Offset: token.position.utf8Offset,
71+
endUtf8Offset: token.endPosition.utf8Offset,
72+
replacement: token.with(\.trailingTrivia, newTrivia).description
73+
)
74+
)
75+
}
76+
}
77+
78+
var source = tree.description
79+
80+
// As we need to start apply the edits at the end of a source, start by reversing edit
81+
// and then sort edits by decrementing start offset. If they are equal then descrementing end offset.
82+
edits = edits.reversed().sorted(by: { edit1, edit2 in
83+
if edit1.startUtf8Offset == edit2.startUtf8Offset {
84+
return edit1.endUtf8Offset > edit2.endUtf8Offset
85+
} else {
86+
return edit1.startUtf8Offset > edit2.startUtf8Offset
87+
}
88+
})
89+
90+
for edit in edits {
91+
let startIndex = source.utf8.index(source.utf8.startIndex, offsetBy: edit.startUtf8Offset)
92+
let endIndex = source.utf8.index(source.utf8.startIndex, offsetBy: edit.endUtf8Offset)
93+
94+
source.replaceSubrange(startIndex..<endIndex, with: edit.replacement)
95+
}
96+
97+
return source
98+
}
99+
}

Tests/SwiftParserTest/Assertions.swift

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -276,58 +276,6 @@ struct DiagnosticSpec {
276276
}
277277
}
278278

279-
class FixItApplier: SyntaxRewriter {
280-
var changes: [FixIt.Change]
281-
282-
init(diagnostics: [Diagnostic], withMessages messages: [String]?) {
283-
let messages = messages ?? diagnostics.compactMap { $0.fixIts.first?.message.message }
284-
285-
self.changes =
286-
diagnostics
287-
.flatMap { $0.fixIts }
288-
.filter {
289-
return messages.contains($0.message.message)
290-
}
291-
.flatMap { $0.changes }
292-
293-
super.init(viewMode: .all)
294-
}
295-
296-
public override func visitAny(_ node: Syntax) -> Syntax? {
297-
for change in changes {
298-
switch change {
299-
case .replace(oldNode: let oldNode, newNode: let newNode) where oldNode.id == node.id:
300-
return newNode
301-
default:
302-
break
303-
}
304-
}
305-
return nil
306-
}
307-
308-
override func visit(_ node: TokenSyntax) -> TokenSyntax {
309-
var modifiedNode = node
310-
for change in changes {
311-
switch change {
312-
case .replaceLeadingTrivia(token: let changedNode, newTrivia: let newTrivia) where changedNode.id == node.id:
313-
modifiedNode = node.with(\.leadingTrivia, newTrivia)
314-
case .replaceTrailingTrivia(token: let changedNode, newTrivia: let newTrivia) where changedNode.id == node.id:
315-
modifiedNode = node.with(\.trailingTrivia, newTrivia)
316-
default:
317-
break
318-
}
319-
}
320-
return modifiedNode
321-
}
322-
323-
/// If `messages` is `nil`, applies all Fix-Its in `diagnostics` to `tree` and returns the fixed syntax tree.
324-
/// If `messages` is not `nil`, applies only Fix-Its whose message is in `messages`.
325-
public static func applyFixes<T: SyntaxProtocol>(in diagnostics: [Diagnostic], withMessages messages: [String]?, to tree: T) -> Syntax {
326-
let applier = FixItApplier(diagnostics: diagnostics, withMessages: messages)
327-
return applier.rewrite(tree)
328-
}
329-
}
330-
331279
/// Assert that `location` is the same as that of `locationMarker` in `tree`.
332280
func assertLocation<T: SyntaxProtocol>(
333281
_ location: SourceLocation,
@@ -659,7 +607,7 @@ extension ParserTestCase {
659607
if expectedDiagnostics.contains(where: { !$0.fixIts.isEmpty }) && expectedFixedSource == nil {
660608
XCTFail("Expected a fixed source if the test case produces diagnostics with Fix-Its", file: file, line: line)
661609
} else if let expectedFixedSource = expectedFixedSource {
662-
let fixedTree = FixItApplier.applyFixes(in: diags, withMessages: applyFixIts, to: tree)
610+
let fixedTree = FixItApplier.applyFixes(from: diags, filterByMessages: applyFixIts, to: tree)
663611
var fixedTreeDescription = fixedTree.description
664612
if options.contains(.normalizeNewlinesInFixedSource) {
665613
fixedTreeDescription =

Tests/SwiftParserTest/translated/AvailabilityQueryUnavailabilityTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@ final class AvailabilityQueryUnavailabilityTests: ParserTestCase {
572572
),
573573
],
574574
fixedSource: """
575-
if #unavailable(*) , true {
575+
if #unavailable(*), true {
576576
}
577577
"""
578578
)

0 commit comments

Comments
 (0)
0