Skip to content

Commit cc339ce

Browse files
committed
Adopt the new merging API of Trivia and fix trivia transfer
Fixed the erratic behaviors of `transferTriviaAtSides(from:)` by adopting the new trivia merging API. As a result, the issue of incorrect transfer of trivia after applying Fix-it has also been solved. Cleaned up the `FixIt.MultiNodeChange.makeMissing` methods.
1 parent 6567a47 commit cc339ce

File tree

5 files changed

+60
-56
lines changed

5 files changed

+60
-56
lines changed

Sources/SwiftParserDiagnostics/DiagnosticExtensions.swift

+37-35
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,39 @@ extension FixIt {
5252

5353
// MARK: - Make missing
5454

55+
private extension FixIt.Change {
56+
/// Transfers the leading and trailing trivia of `nodes` to the previous token
57+
/// While doing this, it tries to be smart, merging trivia where it makes sense
58+
/// and refusing to add e.g. a space after punctuation, where it usually
59+
/// doesn't make sense.
60+
static func transferTriviaAtSides(from nodes: [some SyntaxProtocol]) -> Self? {
61+
guard let first = nodes.first, let last = nodes.last else {
62+
preconditionFailure("nodes must not be empty")
63+
}
64+
let removedTriviaAtSides = first.leadingTrivia.mergingCommonSuffix(last.trailingTrivia)
65+
guard !removedTriviaAtSides.isEmpty, let previousToken = first.previousToken(viewMode: .sourceAccurate) else {
66+
return nil
67+
}
68+
let mergedTrivia = previousToken.trailingTrivia.mergingCommonPrefix(removedTriviaAtSides)
69+
// We merge with the common prefix instead of the common suffix to preserve the original shape of the previous
70+
// token's trailing trivia after merging.
71+
guard !previousToken.tokenKind.isPunctuation || !mergedTrivia.allSatisfy(\.isSpaceOrTab) else {
72+
// Punctuation is generally not followed by spaces in Swift.
73+
// If this action would only add spaces to the punctuation, drop it.
74+
// This generally yields better results.
75+
return nil
76+
}
77+
return .replaceTrailingTrivia(token: previousToken, newTrivia: mergedTrivia)
78+
}
79+
}
80+
5581
extension FixIt.MultiNodeChange {
56-
/// Replaced a present token with a missing node.
82+
/// Replaced a present token with a missing token.
5783
///
5884
/// 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.
85+
/// removed token will be transferred to the trailing trivia of the previous token.
6086
static func makeMissing(_ token: TokenSyntax, transferTrivia: Bool = true) -> Self {
61-
return makeMissing([token], transferTrivia: transferTrivia)
87+
self.makeMissing([token], transferTrivia: transferTrivia)
6288
}
6389

6490
/// Replace present tokens with missing tokens.
@@ -68,57 +94,33 @@ extension FixIt.MultiNodeChange {
6894
/// tokens.
6995
static func makeMissing(_ tokens: [TokenSyntax], transferTrivia: Bool = true) -> Self {
7096
precondition(tokens.allSatisfy(\.isPresent))
71-
return .makeMissing(tokens.map(Syntax.init), transferTrivia: transferTrivia)
97+
return self.makeMissing(tokens.map(Syntax.init), transferTrivia: transferTrivia)
7298
}
7399

74100
/// If `transferTrivia` is `true`, the leading and trailing trivia of the
75101
/// removed node will be transferred to the trailing trivia of the previous token.
76102
static func makeMissing(_ node: (some SyntaxProtocol)?, transferTrivia: Bool = true) -> Self {
77-
guard let node = node else {
103+
guard let node else {
78104
return FixIt.MultiNodeChange(primitiveChanges: [])
79105
}
80-
var changes = [FixIt.Change.replace(oldNode: Syntax(node), newNode: MissingMaker().rewrite(node, detach: true))]
81-
if transferTrivia {
82-
changes += FixIt.MultiNodeChange.transferTriviaAtSides(from: [node]).primitiveChanges
83-
}
84-
return FixIt.MultiNodeChange(primitiveChanges: changes)
85-
}
86-
87-
/// Transfers the leading and trailing trivia of `nodes` to the previous token
88-
/// While doing this, it tries to be smart, merging trivia where it makes sense
89-
/// and refusing to add e.g. a space after punctuation, where it usually
90-
/// doesn't make sense.
91-
private static func transferTriviaAtSides(from nodes: [some SyntaxProtocol]) -> Self {
92-
let removedTriviaAtSides = (nodes.first?.leadingTrivia ?? []).merging(nodes.last?.trailingTrivia ?? [])
93-
if !removedTriviaAtSides.isEmpty, let previousToken = nodes.first?.previousToken(viewMode: .sourceAccurate) {
94-
let mergedTrivia = previousToken.trailingTrivia.merging(removedTriviaAtSides)
95-
if previousToken.tokenKind.isPunctuation, mergedTrivia.allSatisfy({ $0.isSpaceOrTab }) {
96-
// Punctuation is generally not followed by spaces in Swift.
97-
// If this action would only add spaces to the punctuation, drop it.
98-
// This generally yields better results.
99-
return FixIt.MultiNodeChange()
100-
}
101-
return FixIt.MultiNodeChange(.replaceTrailingTrivia(token: previousToken, newTrivia: mergedTrivia))
102-
} else {
103-
return FixIt.MultiNodeChange()
104-
}
106+
return self.makeMissing([node], transferTrivia: transferTrivia)
105107
}
106108

107-
/// Replace present nodes with their missing equivalents.
109+
/// Replace present nodes with missing nodes.
108110
///
109111
/// If `transferTrivia` is `true`, the leading trivia of the first node and
110112
/// the trailing trivia of the last node will be transferred to their adjecent
111113
/// tokens.
112-
static func makeMissing(_ nodes: [Syntax], transferTrivia: Bool = true) -> Self {
114+
static func makeMissing(_ nodes: [some SyntaxProtocol], transferTrivia: Bool = true) -> Self {
113115
precondition(!nodes.isEmpty)
114116
var changes = nodes.map {
115117
FixIt.Change.replace(
116-
oldNode: $0,
118+
oldNode: Syntax($0),
117119
newNode: MissingMaker().rewrite($0, detach: true)
118120
)
119121
}
120-
if transferTrivia {
121-
changes += FixIt.MultiNodeChange.transferTriviaAtSides(from: nodes).primitiveChanges
122+
if transferTrivia, let transferredTrivia = FixIt.Change.transferTriviaAtSides(from: nodes) {
123+
changes.append(transferredTrivia)
122124
}
123125
return FixIt.MultiNodeChange(primitiveChanges: changes)
124126
}

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
213213
)
214214
changes.append(FixIt.MultiNodeChange.makeMissing(misplacedTokens, transferTrivia: false))
215215
} else {
216-
changes += misplacedTokens.map { FixIt.MultiNodeChange.makeMissing($0) }
216+
changes += [.makeMissing(misplacedTokens)]
217217
changes += correctAndMissingNodes.map { FixIt.MultiNodeChange.makePresent($0) }
218218
}
219219
var fixIts: [FixIt] = []

Sources/SwiftRefactor/CallToTrailingClosures.swift

+15-14
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ extension FunctionCallExprSyntax {
7878
// Trailing comma won't exist any more, move its trivia to the end of
7979
// the closure instead
8080
if let comma = arg.trailingComma {
81-
closure.trailingTrivia = closure.trailingTrivia.merging(triviaOf: comma)
81+
closure.trailingTrivia = closure.trailingTrivia.mergingCommonSuffix(triviaOf: comma)
8282
}
8383
closures.append((arg, closure))
8484
}
@@ -88,12 +88,12 @@ extension FunctionCallExprSyntax {
8888
}
8989

9090
// First trailing closure won't have label/colon. Transfer their trivia.
91-
var trailingClosure = closures.first!.closure
91+
let (firstOriginal, firstClosure) = closures.first!
92+
var trailingClosure = firstClosure
9293
trailingClosure.leadingTrivia =
93-
Trivia()
94-
.merging(triviaOf: closures.first!.original.label)
95-
.merging(triviaOf: closures.first!.original.colon)
96-
.merging(closures.first!.closure.leadingTrivia)
94+
(firstOriginal.label?.triviaByMergingCommonSuffix ?? [])
95+
.mergingCommonSuffix(triviaOf: firstOriginal.colon)
96+
.mergingCommonSuffix(firstClosure.leadingTrivia)
9797
.droppingLeadingWhitespace
9898
let additionalTrailingClosures = closures.dropFirst().map {
9999
MultipleTrailingClosureElementSyntax(
@@ -118,21 +118,20 @@ extension FunctionCallExprSyntax {
118118
// No left paren any more, right paren is handled below since it makes
119119
// sense to keep its trivia of the end of the call, regardless of whether
120120
// it was removed or not.
121-
if let leftParen = leftParen {
122-
trailingClosure.leadingTrivia = Trivia()
123-
.merging(triviaOf: leftParen)
124-
.merging(trailingClosure.leadingTrivia)
121+
if let leftParen {
122+
trailingClosure.leadingTrivia = leftParen.triviaByMergingCommonSuffix
123+
.mergingCommonSuffix(trailingClosure.leadingTrivia)
125124
}
126125
// No right paren anymore. Attach its trivia to the end of the call.
127-
if let rightParen = rightParen {
128-
additionalTriviaAtEndOfCall = Trivia().merging(triviaOf: rightParen)
126+
if let rightParen {
127+
additionalTriviaAtEndOfCall = rightParen.triviaByMergingCommonSuffix
129128
}
130129
} else {
131130
let last = argList.last!
132131
// Move the trailing trivia of the closing parenthesis to the end of the call after the last trailing, instead of
133132
// keeping it in the middle of the call where the new closing parenthesis lives.
134133
// Also ensure that we don't drop trivia from any comma we remove.
135-
converted.rightParen?.trailingTrivia = Trivia().merging(triviaOf: last.trailingComma)
134+
converted.rightParen?.trailingTrivia = last.trailingComma?.triviaByMergingCommonSuffix ?? []
136135
additionalTriviaAtEndOfCall = rightParen?.trailingTrivia
137136
argList[argList.count - 1] = last.with(\.trailingComma, nil)
138137
}
@@ -145,7 +144,9 @@ extension FunctionCallExprSyntax {
145144
}
146145

147146
if let additionalTriviaAtEndOfCall {
148-
converted.trailingTrivia = converted.trailingTrivia.merging(additionalTriviaAtEndOfCall.droppingLeadingWhitespace)
147+
converted.trailingTrivia = converted.trailingTrivia.mergingCommonSuffix(
148+
additionalTriviaAtEndOfCall.droppingLeadingWhitespace
149+
)
149150
}
150151

151152
return converted

Tests/SwiftParserTest/DeclarationTests.swift

+5-5
Original file line numberDiff line numberDiff line change
@@ -960,18 +960,18 @@ final class DeclarationTests: ParserTestCase {
960960
fixIts: ["move 'throws(any Error)' in front of '->'"]
961961
)
962962
],
963-
fixedSource: "func test() throws(any Error) -> Int"
963+
fixedSource: "func test() throws(any Error) -> Int"
964964
)
965965

966966
assertParse(
967-
"func test() -> 1️⃣throws(any Error Int",
967+
"func test() -> 1️⃣throws(any Error/* */ Int",
968968
diagnostics: [
969969
DiagnosticSpec(
970970
message: "'throws(any Error' must precede '->'",
971971
fixIts: ["move 'throws(any Error' in front of '->'"]
972972
)
973973
],
974-
fixedSource: "func test() throws(any Error -> Int"
974+
fixedSource: "func test() throws(any Error -> /* */ Int"
975975
)
976976

977977
assertParse(
@@ -986,14 +986,14 @@ final class DeclarationTests: ParserTestCase {
986986
)
987987

988988
assertParse(
989-
"func test() -> 1️⃣throws (any Error) Int",
989+
"func test() -> 1️⃣throws/**/ (any Error) Int",
990990
diagnostics: [
991991
DiagnosticSpec(
992992
message: "'throws' must precede '->'",
993993
fixIts: ["move 'throws' in front of '->'"]
994994
)
995995
],
996-
fixedSource: "func test() throws -> (any Error) Int"
996+
fixedSource: "func test() throws -> /**/ (any Error) Int"
997997
)
998998
}
999999

Tests/SwiftParserTest/translated/RecoveryTests.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,8 @@ final class RecoveryTests: ParserTestCase {
774774
),
775775
],
776776
fixedSource: """
777-
for <#pattern#> in <#expression#> {
777+
for
778+
<#pattern#> in <#expression#> {
778779
}
779780
"""
780781
)

0 commit comments

Comments
 (0)