Skip to content

Commit 43e728a

Browse files
authored
Merge pull request #1004 from ahoppen/ahoppen/review-comments
Address review comments from merged PRs
2 parents 97c5990 + f558569 commit 43e728a

File tree

10 files changed

+120
-45
lines changed

10 files changed

+120
-45
lines changed

Sources/SwiftParser/Diagnostics/DiagnosticExtensions.swift

Lines changed: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,17 @@ extension FixIt {
3232
}
3333

3434
extension FixIt.Changes {
35-
/// Replaced a present token with a missing node
36-
static func makeMissing(_ token: TokenSyntax) -> Self {
37-
return makeMissing([token])
35+
/// Replaced a present token with a missing node.
36+
/// If `transferTrivia` is `true`, the leading and trailing trivia of the
37+
/// removed node will be transferred to the trailing trivia of the previous token.
38+
static func makeMissing(_ token: TokenSyntax, transferTrivia: Bool = true) -> Self {
39+
return makeMissing([token], transferTrivia: transferTrivia)
3840
}
3941

40-
/// Replace present tokens with missing tokens
41-
static func makeMissing(_ tokens: [TokenSyntax]) -> Self {
42+
/// Replace present tokens with missing tokens.
43+
/// If `transferTrivia` is `true`, the leading and trailing trivia of the
44+
/// removed node will be transferred to the trailing trivia of the previous token.
45+
static func makeMissing(_ tokens: [TokenSyntax], transferTrivia: Bool = true) -> Self {
4246
assert(!tokens.isEmpty)
4347
assert(tokens.allSatisfy({ $0.presence == .present }))
4448
var changes = tokens.map {
@@ -47,19 +51,20 @@ extension FixIt.Changes {
4751
newNode: Syntax(TokenSyntax($0.tokenKind, leadingTrivia: [], trailingTrivia: [], presence: .missing))
4852
)
4953
}
50-
if let firstToken = tokens.first,
51-
firstToken.leadingTrivia.isEmpty == false,
52-
let nextToken = tokens.last?.nextToken(viewMode: .sourceAccurate),
53-
!nextToken.leadingTrivia.contains(where: { $0.isNewline }) {
54-
changes.append(.replaceLeadingTrivia(token: nextToken, newTrivia: firstToken.leadingTrivia))
54+
if transferTrivia {
55+
changes += FixIt.Changes.transferTriviaAtSides(from: tokens).changes
5556
}
5657
return FixIt.Changes(changes: changes)
5758
}
5859

59-
static func makeMissing<SyntaxType: SyntaxProtocol>(_ node: SyntaxType) -> Self {
60-
return FixIt.Changes(changes: [
61-
.replace(oldNode: Syntax(node), newNode: MissingMaker().visit(Syntax(node)))
62-
])
60+
/// If `transferTrivia` is `true`, the leading and trailing trivia of the
61+
/// removed node will be transferred to the trailing trivia of the previous token.
62+
static func makeMissing<SyntaxType: SyntaxProtocol>(_ node: SyntaxType, transferTrivia: Bool = true) -> Self {
63+
var changes = [FixIt.Change.replace(oldNode: Syntax(node), newNode: MissingMaker().visit(Syntax(node)))]
64+
if transferTrivia {
65+
changes += FixIt.Changes.transferTriviaAtSides(from: [node]).changes
66+
}
67+
return FixIt.Changes(changes: changes)
6368
}
6469

6570
/// Make a node present. If `leadingTrivia` or `trailingTrivia` is specified,
@@ -109,4 +114,74 @@ extension FixIt.Changes {
109114
return .makePresent(token)
110115
}
111116
}
117+
118+
/// Transfers the leading and trivia trivia of `nodes` to the trailing trivia
119+
/// of the previous token. While doing this, it tries to be smart, merging trivia
120+
/// where it makes sense and refusing to add e.g. a space after punctuation,
121+
/// where it usually doesn't make sense.
122+
static func transferTriviaAtSides<SyntaxType: SyntaxProtocol>(from nodes: [SyntaxType]) -> Self {
123+
let removedTriviaAtSides = Trivia.merged(nodes.first?.leadingTrivia ?? [], nodes.last?.trailingTrivia ?? [])
124+
if !removedTriviaAtSides.isEmpty, let previousToken = nodes.first?.previousToken(viewMode: .sourceAccurate) {
125+
let mergedTrivia = Trivia.merged(previousToken.trailingTrivia, removedTriviaAtSides)
126+
if previousToken.tokenKind.isPunctuation, mergedTrivia.allSatisfy({ $0.isSpaceOrTab }) {
127+
// Punctuation is generally not followed by spaces in Swift.
128+
// If this action would only add spaces to the punctuation, drop it.
129+
// This generally yields better results.
130+
return []
131+
}
132+
return [.replaceTrailingTrivia(token: previousToken, newTrivia: mergedTrivia)]
133+
} else {
134+
return []
135+
}
136+
}
137+
}
138+
139+
extension Trivia {
140+
/// Decomposes the trivia into pieces that all have count 1
141+
var decomposed: Trivia {
142+
let pieces = self.flatMap({ (piece: TriviaPiece) -> [TriviaPiece] in
143+
switch piece {
144+
case .spaces(let count):
145+
return Array(repeating: TriviaPiece.spaces(1), count: count)
146+
case .tabs(let count):
147+
return Array(repeating: TriviaPiece.tabs(1), count: count)
148+
case .verticalTabs(let count):
149+
return Array(repeating: TriviaPiece.verticalTabs(1), count: count)
150+
case .formfeeds(let count):
151+
return Array(repeating: TriviaPiece.formfeeds(1), count: count)
152+
case .newlines(let count):
153+
return Array(repeating: TriviaPiece.newlines(1), count: count)
154+
case .carriageReturns(let count):
155+
return Array(repeating: TriviaPiece.carriageReturns(1), count: count)
156+
case .carriageReturnLineFeeds(let count):
157+
return Array(repeating: TriviaPiece.carriageReturnLineFeeds(1), count: count)
158+
case .lineComment, .blockComment, .docLineComment, .docBlockComment, .unexpectedText, .shebang:
159+
return [piece]
160+
}
161+
})
162+
return Trivia(pieces: pieces)
163+
}
164+
165+
/// Concatenate `lhs` and `rhs`, merging an infix that is shared between both trivia pieces.
166+
static func merged(_ lhs: Trivia, _ rhs: Trivia) -> Self {
167+
let lhs = lhs.decomposed
168+
let rhs = rhs.decomposed
169+
for infixLength in (0...Swift.min(lhs.count, rhs.count)).reversed() {
170+
if lhs.suffix(infixLength) == rhs.suffix(infixLength) {
171+
return lhs + Trivia(pieces: Array(rhs.dropFirst(infixLength)))
172+
}
173+
}
174+
return lhs + rhs
175+
}
176+
}
177+
178+
extension TriviaPiece {
179+
var isSpaceOrTab: Bool {
180+
switch self {
181+
case .spaces, .tabs:
182+
return true
183+
default:
184+
return false
185+
}
186+
}
112187
}

Sources/SwiftParser/Diagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -96,22 +96,19 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
9696

9797
// Ignore `correctTokens` that are already present.
9898
let correctAndMissingTokens = correctTokens.filter({ $0.presence == .missing })
99-
var changes = misplacedTokens.map(FixIt.Changes.makeMissing)
100-
for correctToken in correctAndMissingTokens {
101-
if misplacedTokens.count == 1, let misplacedToken = misplacedTokens.first,
102-
misplacedToken.nextToken(viewMode: .all) == correctToken || misplacedToken.previousToken(viewMode: .all) == correctToken {
103-
changes.append(FixIt.Changes.makePresent(
104-
correctToken,
105-
leadingTrivia: misplacedToken.leadingTrivia,
106-
trailingTrivia: misplacedToken.trailingTrivia
107-
))
108-
} else {
109-
changes.append(FixIt.Changes.makePresent(
110-
correctToken,
111-
leadingTrivia: nil,
112-
trailingTrivia: nil
113-
))
114-
}
99+
var changes: [FixIt.Changes] = []
100+
if let misplacedToken = misplacedTokens.only, let correctToken = correctTokens.only,
101+
misplacedToken.nextToken(viewMode: .all) == correctToken || misplacedToken.previousToken(viewMode: .all) == correctToken {
102+
// We are exchanging two adjacent tokens, transfer the trivia from the incorrect token to the corrected token.
103+
changes += misplacedTokens.map { FixIt.Changes.makeMissing($0, transferTrivia: false) }
104+
changes.append(FixIt.Changes.makePresent(
105+
correctToken,
106+
leadingTrivia: misplacedToken.leadingTrivia,
107+
trailingTrivia: misplacedToken.trailingTrivia
108+
))
109+
} else {
110+
changes += misplacedTokens.map { FixIt.Changes.makeMissing($0) }
111+
changes += correctAndMissingTokens.map { FixIt.Changes.makePresent($0) }
115112
}
116113
var fixIts: [FixIt] = []
117114
if changes.count > 1 {
@@ -170,7 +167,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
170167
addDiagnostic(node, TryCannotBeUsed(nextToken: nextToken))
171168
} else if let semicolons = node.onlyTokens(satisfying: { $0.tokenKind == .semicolon }) {
172169
addDiagnostic(node, .unexpectedSemicolon, fixIts: [
173-
FixIt(message: RemoveNodesFixIt(semicolons), changes: semicolons.map(FixIt.Changes.makeMissing))
170+
FixIt(message: RemoveNodesFixIt(semicolons), changes: semicolons.map { FixIt.Changes.makeMissing($0) })
174171
])
175172
} else if node.first?.as(TokenSyntax.self)?.tokenKind.isIdentifier == true,
176173
let previousToken = node.previousToken(viewMode: .sourceAccurate),

Sources/SwiftParser/Expressions.swift

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2188,13 +2188,9 @@ extension Parser {
21882188
let unexpectedBeforeLabel: RawUnexpectedNodesSyntax?
21892189
let label: RawTokenSyntax?
21902190
let colon: RawTokenSyntax?
2191-
if let labelAndColon = self.consume(if: { $0.canBeArgumentLabel() }, followedBy: { $0.tokenKind == .colon }) {
2192-
unexpectedBeforeLabel = nil
2193-
(label, colon) = labelAndColon
2194-
} else if let dollarLabelAndColon = self.consume(if: .dollarIdentifier, followedBy: .colon) {
2195-
unexpectedBeforeLabel = RawUnexpectedNodesSyntax([dollarLabelAndColon.0], arena: self.arena)
2196-
label = missingToken(.identifier)
2197-
colon = dollarLabelAndColon.1
2191+
if currentToken.canBeArgumentLabel(allowDollarIdentifier: true) && self.peek().tokenKind == .colon {
2192+
(unexpectedBeforeLabel, label) = parseArgumentLabel()
2193+
colon = consumeAnyToken()
21982194
} else {
21992195
unexpectedBeforeLabel = nil
22002196
label = nil

Sources/SwiftSyntax/gyb_generated/syntax_nodes/SyntaxStmtNodes.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1573,7 +1573,7 @@ public struct GuardStmtSyntax: StmtSyntaxProtocol, SyntaxHashable {
15731573
case 2:
15741574
return nil
15751575
case 3:
1576-
return "conditions"
1576+
return "condition"
15771577
case 4:
15781578
return nil
15791579
case 5:

Tests/SwiftParserTest/translated/ForeachAsyncTests.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ final class ForeachAsyncTests: XCTestCase {
7373
sum = sum + i + j
7474
}
7575
// Parse errors
76-
// FIXME: Bad diagnostics; should be just 'expected 'in' after for-each patter'.
7776
for await i 1️⃣r {
7877
}
7978
for await i in r 2️⃣sum = sum + i;3️⃣

Tests/SwiftParserTest/translated/ForeachTests.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ final class ForeachTests: XCTestCase {
5050
sum = sum + i + j
5151
}
5252
// Parse errors
53-
// FIXME: Bad diagnostics; should be just 'expected 'in' after for-each patter'.
5453
for i 1️⃣r {
5554
}
5655
for i in r 2️⃣sum = sum + i;3️⃣

Tests/SwiftParserTest/translated/GuardTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ final class GuardTests: XCTestCase {
3636
}
3737
""",
3838
diagnostics: [
39-
DiagnosticSpec(message: "expected conditions in 'guard' statement"),
39+
DiagnosticSpec(message: "expected condition in 'guard' statement"),
4040
]
4141
)
4242
}

Tests/SwiftParserTest/translated/InitDeinitTests.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
// This test file has been translated from swift/test/Parse/init_deinit.swift
1414

15+
import SwiftSyntax
16+
1517
import XCTest
1618

1719
final class InitDeinitTests: XCTestCase {
@@ -166,7 +168,7 @@ final class InitDeinitTests: XCTestCase {
166168
DiagnosticSpec(message: "deinitializers cannot have parameters", fixIts: ["remove function signature"]),
167169
], fixedSource: """
168170
class FooClassDeinitializerA {
169-
deinit{}
171+
deinit {}
170172
}
171173
"""
172174
)
@@ -392,7 +394,13 @@ final class InitDeinitTests: XCTestCase {
392394
deinit
393395
final func foo()
394396
}
395-
"""
397+
""",
398+
substructure: Syntax(DeinitializerDeclSyntax(
399+
attributes: nil,
400+
modifiers: nil,
401+
deinitKeyword: .deinitKeyword(),
402+
body: nil
403+
))
396404
)
397405
}
398406
}

Tests/SwiftParserTest/translated/SwitchTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ final class SwitchTests: XCTestCase {
227227
], fixedSource: """
228228
switch x {
229229
case 0:
230+
230231
case 1:
231232
x = 0
232233
}

gyb_syntax_support/StmtNodes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
traits=['WithCodeBlock'],
6464
children=[
6565
Child('GuardKeyword', kind='GuardToken'),
66-
Child('Conditions', kind='ConditionElementList', name_for_diagnostics='conditions',
66+
Child('Conditions', kind='ConditionElementList', name_for_diagnostics='condition',
6767
collection_element_name='Condition'),
6868
Child('ElseKeyword', kind='ElseToken'),
6969
Child('Body', kind='CodeBlock', name_for_diagnostics='body'),

0 commit comments

Comments
 (0)