Skip to content

Commit da0c744

Browse files
committed
[Diagnostics] fix trivia merging and transfer
- deprecated the incorrectly implemented `Trivia.merging(_:)` and `Trivia.merging(triviaOf:)` - fixed incorrect transfers of trivia in Fix-it applications
1 parent 984ec6d commit da0c744

File tree

7 files changed

+120
-46
lines changed

7 files changed

+120
-46
lines changed

Sources/SwiftParserDiagnostics/DiagnosticExtensions.swift

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,32 @@ 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()
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 {
5682
/// Replaced a present token with a missing node.
5783
///
@@ -78,32 +104,12 @@ extension FixIt.MultiNodeChange {
78104
return FixIt.MultiNodeChange(primitiveChanges: [])
79105
}
80106
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
107+
if transferTrivia, let transferredTrivia = FixIt.Change.transferTriviaAtSides(from: [node]) {
108+
changes.append(transferredTrivia)
83109
}
84110
return FixIt.MultiNodeChange(primitiveChanges: changes)
85111
}
86112

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-
}
105-
}
106-
107113
/// Replace present nodes with their missing equivalents.
108114
///
109115
/// If `transferTrivia` is `true`, the leading trivia of the first node and
@@ -117,8 +123,8 @@ extension FixIt.MultiNodeChange {
117123
newNode: MissingMaker().rewrite($0, detach: true)
118124
)
119125
}
120-
if transferTrivia {
121-
changes += FixIt.MultiNodeChange.transferTriviaAtSides(from: nodes).primitiveChanges
126+
if transferTrivia, let transferredTrivia = FixIt.Change.transferTriviaAtSides(from: nodes) {
127+
changes.append(transferredTrivia)
122128
}
123129
return FixIt.MultiNodeChange(primitiveChanges: changes)
124130
}

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 2 additions & 2 deletions
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.map(Syntax.init))]
217217
changes += correctAndMissingNodes.map { FixIt.MultiNodeChange.makePresent($0) }
218218
}
219219
var fixIts: [FixIt] = []
@@ -757,7 +757,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
757757
node,
758758
.standaloneSemicolonStatement,
759759
fixIts: [
760-
FixIt(message: RemoveNodesFixIt(semicolon), changes: .makeMissing(semicolon))
760+
FixIt(message: RemoveNodesFixIt(semicolon), changes: .makeMissing(semicolon, transferTrivia: false))
761761
],
762762
handledNodes: [node.item.id]
763763
)

Sources/SwiftRefactor/CallToTrailingClosures.swift

Lines changed: 15 additions & 14 deletions
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

Sources/SwiftSyntax/Trivia.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public struct Trivia: Sendable {
6969

7070
/// Creates a new ``Trivia`` by merging in the given trivia. Only includes one
7171
/// copy of a common prefix of `self` and `trivia`.
72+
@available(*, deprecated, message: "Use mergingCommonPrefix(trivia) or mergingCommonSuffix(trivia) instead")
7273
public func merging(_ trivia: Trivia?) -> Trivia {
7374
guard let trivia else {
7475
return self
@@ -84,16 +85,69 @@ public struct Trivia: Sendable {
8485
return lhs.appending(rhs)
8586
}
8687

88+
/// Creates a new ``Trivia`` by merging in the given trivia. Only includes one
89+
/// copy of the common prefix of `self` and `trivia`.
90+
public func mergingCommonPrefix(_ trivia: Trivia?) -> Trivia {
91+
guard let trivia else {
92+
return self
93+
}
94+
95+
let lhs = self.decomposed
96+
let rhs = trivia.decomposed
97+
let commonPrefix = zip(lhs, rhs).prefix(while: ==)
98+
if commonPrefix.isEmpty {
99+
return lhs + rhs
100+
} else {
101+
return lhs + Trivia(pieces: rhs.dropFirst(commonPrefix.count))
102+
}
103+
}
104+
105+
/// Creates a new ``Trivia`` by merging in the given trivia. Only includes one
106+
/// copy of the common suffix of `self` and `trivia`.
107+
public func mergingCommonSuffix(_ trivia: Trivia?) -> Trivia {
108+
guard let trivia else {
109+
return self
110+
}
111+
112+
let lhs = self.decomposed
113+
let rhs = trivia.decomposed
114+
let commonSuffix = zip(lhs.reversed(), rhs.reversed()).prefix(while: ==)
115+
if commonSuffix.isEmpty {
116+
return lhs + rhs
117+
} else {
118+
return Trivia(pieces: lhs.dropLast(commonSuffix.count)) + rhs
119+
}
120+
}
121+
87122
/// Creates a new ``Trivia`` by merging the leading and trailing ``Trivia``
88123
/// of `triviaOf` into the end of `self`. Only includes one copy of any
89124
/// common prefixes.
125+
@available(*, deprecated, message: "Use mergingCommonPrefix(triviaOf:) or mergingCommonSuffix(triviaOf:) instead")
90126
public func merging(triviaOf node: (some SyntaxProtocol)?) -> Trivia {
91127
guard let node else {
92128
return self
93129
}
94130
return merging(node.leadingTrivia).merging(node.trailingTrivia)
95131
}
96132

133+
/// Creates a new ``Trivia`` by merging ``SyntaxProtocol/leadingTrivia`` and ``SyntaxProtocol/trailingTrivia`` of
134+
/// `node` into the end of `self`. Only includes one copy of any common prefixes.
135+
public func mergingCommonPrefix(triviaOf node: (some SyntaxProtocol)?) -> Trivia {
136+
guard let node else {
137+
return self
138+
}
139+
return self.mergingCommonPrefix(node.leadingTrivia).mergingCommonPrefix(node.trailingTrivia)
140+
}
141+
142+
/// Creates a new ``Trivia`` by merging ``SyntaxProtocol/leadingTrivia`` and ``SyntaxProtocol/trailingTrivia`` of
143+
/// `node` into the end of `self`. Only includes one copy of any common suffixes.
144+
public func mergingCommonSuffix(triviaOf node: (some SyntaxProtocol)?) -> Trivia {
145+
guard let node else {
146+
return self
147+
}
148+
return self.mergingCommonSuffix(node.leadingTrivia).mergingCommonSuffix(node.trailingTrivia)
149+
}
150+
97151
/// Concatenates two collections of ``Trivia`` into one collection.
98152
public static func + (lhs: Trivia, rhs: Trivia) -> Trivia {
99153
return lhs.appending(rhs)
@@ -215,3 +269,17 @@ extension RawTriviaPiece: CustomDebugStringConvertible {
215269
TriviaPiece(raw: self).debugDescription
216270
}
217271
}
272+
273+
public extension SyntaxProtocol {
274+
/// Create a new ``Trivia`` by merging ``SyntaxProtocol/leadingTrivia`` and ``SyntaxProtocol/trailingTrivia`` of
275+
/// this node, including only one copy of their common prefix.
276+
var triviaByMergingCommonPrefix: Trivia {
277+
self.leadingTrivia.mergingCommonPrefix(self.trailingTrivia)
278+
}
279+
280+
/// Create a new ``Trivia`` by merging ``SyntaxProtocol/leadingTrivia`` and ``SyntaxProtocol/trailingTrivia`` of
281+
/// this node, including only one copy of their common suffix.
282+
var triviaByMergingCommonSuffix: Trivia {
283+
self.leadingTrivia.mergingCommonSuffix(self.trailingTrivia)
284+
}
285+
}

Tests/SwiftParserTest/DeclarationTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -953,14 +953,14 @@ final class DeclarationTests: ParserTestCase {
953953

954954
func testTypedThrowsPlacedAfterArrow() {
955955
assertParse(
956-
"func test() -> 1️⃣throws(any Error) Int",
956+
"func test() -> 1️⃣throws(any Error)/* */ Int",
957957
diagnostics: [
958958
DiagnosticSpec(
959959
message: "'throws(any Error)' must precede '->'",
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(
@@ -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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,7 @@ final class RecoveryTests: ParserTestCase {
774774
),
775775
],
776776
fixedSource: """
777-
for <#pattern#> in <#expression#> {
777+
for <#pattern#> in <#expression#> {
778778
}
779779
"""
780780
)

Tests/SwiftParserTest/translated/SwitchTests.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,6 @@ final class SwitchTests: ParserTestCase {
276276
fixedSource: """
277277
switch x {
278278
case 0:
279-
280279
case 1:
281280
x = 0
282281
}

0 commit comments

Comments
 (0)