Skip to content

Commit a6ac31b

Browse files
authored
Merge pull request #881 from ahoppen/ahoppen/throws-after-arrow-expr
Add diagnostic to move 'async' and 'throws' in 'ArrowExpr' in front of '->'
2 parents 2ec9844 + a4088dd commit a6ac31b

File tree

26 files changed

+8886
-1049
lines changed

26 files changed

+8886
-1049
lines changed

CodeGeneration/Sources/generate-swiftbasicformat/BasicFormatFile.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ private func createTokenFormatFunction() -> FunctionDecl {
168168
) {
169169
VariableDecl("var node = node")
170170
SwitchStmt(expression: MemberAccessExpr(base: "node", name: "tokenKind")) {
171-
for token in SYNTAX_TOKENS {
171+
for token in SYNTAX_TOKENS where token.name != "ContextualKeyword" {
172172
SwitchCase(label: SwitchCaseLabel(caseItems: CaseItem(pattern: ExpressionPattern(expression: MemberAccessExpr(name: token.swiftKind))))) {
173173
if token.requiresLeadingSpace {
174174
IfStmt(
@@ -196,6 +196,20 @@ private func createTokenFormatFunction() -> FunctionDecl {
196196
SwitchCase(label: SwitchCaseLabel(caseItems: CaseItem(pattern: ExpressionPattern(expression: MemberAccessExpr(name: "eof"))))) {
197197
BreakStmt("break")
198198
}
199+
SwitchCase(label: SwitchCaseLabel(caseItems: CaseItem(pattern: ExpressionPattern(expression: MemberAccessExpr(name: "contextualKeyword"))))) {
200+
SwitchStmt(
201+
"""
202+
switch node.text {
203+
case "async":
204+
if node.trailingTrivia.isEmpty {
205+
node.trailingTrivia += .space
206+
}
207+
default:
208+
break
209+
}
210+
"""
211+
)
212+
}
199213
}
200214
SequenceExpr("node.leadingTrivia = node.leadingTrivia.indented(indentation: indentation)")
201215
SequenceExpr("node.trailingTrivia = node.trailingTrivia.indented(indentation: indentation)")

Sources/SwiftBasicFormat/generated/BasicFormat.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3147,8 +3147,6 @@ open class BasicFormat: SyntaxRewriter {
31473147
break
31483148
case .dollarIdentifier:
31493149
break
3150-
case .contextualKeyword:
3151-
break
31523150
case .rawStringDelimiter:
31533151
break
31543152
case .stringSegment:
@@ -3159,6 +3157,15 @@ open class BasicFormat: SyntaxRewriter {
31593157
break
31603158
case .eof:
31613159
break
3160+
case .contextualKeyword:
3161+
switch node.text {
3162+
case "async":
3163+
if node.trailingTrivia.isEmpty {
3164+
node.trailingTrivia += .space
3165+
}
3166+
default:
3167+
break
3168+
}
31623169
}
31633170
node.leadingTrivia = node.leadingTrivia.indented(indentation: indentation)
31643171
node.trailingTrivia = node.trailingTrivia.indented(indentation: indentation)

Sources/SwiftDiagnostics/FixIt.swift

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,39 @@ public protocol FixItMessage {
2626

2727
/// A Fix-It that can be applied to resolve a diagnostic.
2828
public struct FixIt {
29+
public struct Changes: ExpressibleByArrayLiteral {
30+
public var changes: [Change]
31+
32+
public init(changes: [Change]) {
33+
self.changes = changes
34+
}
35+
36+
public init(arrayLiteral elements: FixIt.Change...) {
37+
self.init(changes: elements)
38+
}
39+
40+
public init(combining: [Changes]) {
41+
self.init(changes: combining.flatMap(\.changes))
42+
}
43+
}
44+
2945
public enum Change {
3046
/// Replace `oldNode` by `newNode`.
3147
case replace(oldNode: Syntax, newNode: Syntax)
32-
/// Remove the trailing trivia of the given token
33-
case removeTrailingTrivia(TokenSyntax)
48+
/// Replace the leading trivia on the given token
49+
case replaceLeadingTrivia(token: TokenSyntax, newTrivia: Trivia)
50+
/// Replace the trailing trivia on the given token
51+
case replaceTrailingTrivia(token: TokenSyntax, newTrivia: Trivia)
3452
}
3553

3654
/// A description of what this Fix-It performs.
3755
public let message: FixItMessage
3856

3957
/// The changes that need to be performed when the Fix-It is applied.
40-
public let changes: [Change]
58+
public let changes: Changes
4159

42-
public init(message: FixItMessage, changes: [Change]) {
43-
assert(!changes.isEmpty, "A Fix-It must have at least one change associated with it")
60+
public init(message: FixItMessage, changes: Changes) {
61+
assert(!changes.changes.isEmpty, "A Fix-It must have at least one change associated with it")
4462
self.message = message
4563
self.changes = changes
4664
}

Sources/SwiftParser/Diagnostics/DiagnosticExtensions.swift

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,42 +14,72 @@ import SwiftDiagnostics
1414
import SwiftSyntax
1515

1616
extension FixIt {
17-
init(message: StaticParserFixIt, changes: [Change]) {
17+
init(message: StaticParserFixIt, changes: Changes) {
1818
self.init(message: message as FixItMessage, changes: changes)
1919
}
20+
21+
public init(message: FixItMessage, changes: [Changes]) {
22+
self.init(message: message, changes: FixIt.Changes(combining: changes))
23+
}
24+
25+
init(message: StaticParserFixIt, changes: [Changes]) {
26+
self.init(message: message as FixItMessage, changes: FixIt.Changes(combining: changes))
27+
}
2028
}
2129

22-
extension FixIt.Change {
30+
extension FixIt.Changes {
2331
/// Replaced a present node with a missing node
24-
static func makeMissing(node: TokenSyntax) -> FixIt.Change {
32+
static func makeMissing(node: TokenSyntax) -> Self {
2533
assert(node.presence == .present)
26-
return .replace(
27-
oldNode: Syntax(node),
28-
newNode: Syntax(TokenSyntax(node.tokenKind, leadingTrivia: [], trailingTrivia: [], presence: .missing))
29-
)
34+
var changes = [
35+
FixIt.Change.replace(
36+
oldNode: Syntax(node),
37+
newNode: Syntax(TokenSyntax(node.tokenKind, leadingTrivia: [], trailingTrivia: [], presence: .missing))
38+
)
39+
]
40+
if !node.leadingTrivia.isEmpty, let nextToken = node.nextToken(viewMode: .sourceAccurate) {
41+
changes.append(.replaceLeadingTrivia(token: nextToken, newTrivia: node.leadingTrivia))
42+
}
43+
return FixIt.Changes(changes: changes)
3044
}
3145

32-
static func makePresent<T: SyntaxProtocol>(node: T) -> FixIt.Change {
33-
return .replace(
46+
static func makePresent<T: SyntaxProtocol>(node: T) -> Self {
47+
return [.replace(
3448
oldNode: Syntax(node),
3549
newNode: PresentMaker().visit(Syntax(node))
36-
)
50+
)]
51+
}
52+
53+
/// Make a token present. If `leadingTrivia` or `trailingTrivia` is specified,
54+
/// override the default leading/trailing trivia inferred from `BasicFormat`.
55+
static func makePresent(
56+
node: TokenSyntax,
57+
leadingTrivia: Trivia? = nil,
58+
trailingTrivia: Trivia? = nil
59+
) -> Self {
60+
var presentNode = PresentMaker().visit(Syntax(node)).as(TokenSyntax.self)!
61+
if let leadingTrivia = leadingTrivia {
62+
presentNode.leadingTrivia = leadingTrivia
63+
}
64+
if let trailingTrivia = trailingTrivia {
65+
presentNode.trailingTrivia = trailingTrivia
66+
}
67+
return [.replace(
68+
oldNode: Syntax(node),
69+
newNode: Syntax(presentNode)
70+
)]
3771
}
38-
}
3972

40-
extension Array where Element == FixIt.Change {
4173
/// Makes the `token` present, moving it in front of the previous token's trivia.
42-
static func makePresentBeforeTrivia(token: TokenSyntax) -> [FixIt.Change] {
74+
static func makePresentBeforeTrivia(token: TokenSyntax) -> Self {
4375
if let previousToken = token.previousToken(viewMode: .sourceAccurate) {
4476
let presentToken = PresentMaker().visit(token).withTrailingTrivia(previousToken.trailingTrivia)
4577
return [
46-
.removeTrailingTrivia(previousToken),
78+
.replaceTrailingTrivia(token: previousToken, newTrivia: []),
4779
.replace(oldNode: Syntax(token), newNode: presentToken),
4880
]
4981
} else {
50-
return [
51-
.makePresent(node: token)
52-
]
82+
return .makePresent(node: token)
5383
}
5484
}
5585
}

Sources/SwiftParser/Diagnostics/MissingNodesError.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ import SwiftBasicFormat
1717
// MARK: - Shared code
1818

1919
/// Returns a string that describes `missingNodes`.
20-
/// `missingNodes` are expected to all be children of `commonParent`.
21-
private func missingNodesDescription(missingNodes: [Syntax], commonParent: Syntax?) -> String {
22-
assert(missingNodes.allSatisfy({ $0.parent == commonParent }))
20+
/// If `commonParent` is not `nil`, `missingNodes` are expected to all be children of `commonParent`.
21+
func missingNodesDescription(missingNodes: [Syntax], commonParent: Syntax?) -> String {
22+
if commonParent != nil {
23+
assert(missingNodes.allSatisfy({ $0.parent == commonParent }))
24+
}
2325

2426
// If all tokens in the parent are missing, return the parent type name.
2527
if let commonParent = commonParent,
@@ -256,7 +258,7 @@ extension ParseDiagnosticsGenerator {
256258

257259
let fixIt = FixIt(
258260
message: InsertTokenFixIt(missingNodes: missingNodes),
259-
changes: missingNodes.map(FixIt.Change.makePresent)
261+
changes: missingNodes.map(FixIt.Changes.makePresent)
260262
)
261263

262264
var notes: [Note] = []

Sources/SwiftParser/Diagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,51 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
9595
return handledNodes.contains(node.id)
9696
}
9797

98+
/// Utility function to emit a diagnostic that removes a misplaced token and instead inserts an equivalent token at the corrected location.
99+
///
100+
/// If `incorrectContainer` contains only tokens that satisfy `unexpectedTokenCondition`, emit a diagnostic with message `message` that marks this token as misplaced.
101+
/// If `correctTokens` contains missing tokens, also emit a Fix-It with message `fixIt` that marks the unexpected token as missing and instead inserts `correctTokens`.
102+
public func exchangeTokens(
103+
unexpected: UnexpectedNodesSyntax?,
104+
unexpectedTokenCondition: (TokenSyntax) -> Bool,
105+
correctTokens: [TokenSyntax?],
106+
message: (_ misplacedTokens: [TokenSyntax]) -> DiagnosticMessage,
107+
fixIt: (_ misplacedTokens: [TokenSyntax]) -> FixItMessage
108+
) {
109+
guard let incorrectContainer = unexpected,
110+
let misplacedTokens = incorrectContainer.onlyTokens(satisfying: unexpectedTokenCondition) else {
111+
// If there are no unexpected nodes or the unexpected contain multiple tokens, don't emit a diagnostic.
112+
return
113+
}
114+
115+
// Ignore `correctTokens` that are already present.
116+
let correctTokens = correctTokens.compactMap({ $0 }).filter({ $0.presence == .missing })
117+
var changes = misplacedTokens.map(FixIt.Changes.makeMissing)
118+
for correctToken in correctTokens {
119+
if misplacedTokens.count == 1, let misplacedToken = misplacedTokens.first,
120+
misplacedToken.nextToken(viewMode: .all) == correctToken || misplacedToken.previousToken(viewMode: .all) == correctToken {
121+
changes.append(FixIt.Changes.makePresent(
122+
node: correctToken,
123+
leadingTrivia: misplacedToken.leadingTrivia,
124+
trailingTrivia: misplacedToken.trailingTrivia
125+
))
126+
} else {
127+
changes.append(FixIt.Changes.makePresent(
128+
node: correctToken,
129+
leadingTrivia: nil,
130+
trailingTrivia: nil
131+
))
132+
}
133+
}
134+
var fixIts: [FixIt] = []
135+
if changes.count > 1 {
136+
// Only emit a Fix-It if we are moving a token, i.e. also making a token present.
137+
fixIts.append(FixIt(message: fixIt(misplacedTokens), changes: changes))
138+
}
139+
140+
addDiagnostic(incorrectContainer, message(misplacedTokens), fixIts: fixIts, handledNodes: [incorrectContainer.id] + correctTokens.map(\.id))
141+
}
142+
98143
// MARK: - Generic diagnostic generation
99144

100145
public override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
@@ -180,6 +225,20 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
180225
return .visitChildren
181226
}
182227

228+
public override func visit(_ node: ArrowExprSyntax) -> SyntaxVisitorContinueKind {
229+
if shouldSkip(node) {
230+
return .skipChildren
231+
}
232+
exchangeTokens(
233+
unexpected: node.unexpectedAfterArrowToken,
234+
unexpectedTokenCondition: { $0.tokenKind == .contextualKeyword("async") || $0.tokenKind == .throwsKeyword },
235+
correctTokens: [node.asyncKeyword, node.throwsToken],
236+
message: { EffectsSpecifierAfterArrow(effectsSpecifiersAfterArrow: $0) },
237+
fixIt: { MoveTokensInFrontOfFixIt(movedTokens: $0, inFrontOf: .arrow) }
238+
)
239+
return .visitChildren
240+
}
241+
183242
public override func visit(_ node: ForInStmtSyntax) -> SyntaxVisitorContinueKind {
184243
if shouldSkip(node) {
185244
return .skipChildren
@@ -215,19 +274,13 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
215274
if shouldSkip(node) {
216275
return .skipChildren
217276
}
218-
if let output = node.output,
219-
let missingThrowsKeyword = node.throwsOrRethrowsKeyword,
220-
missingThrowsKeyword.presence == .missing,
221-
let unexpectedBeforeReturnType = output.unexpectedBetweenArrowAndReturnType,
222-
let throwsInReturnPosition = unexpectedBeforeReturnType.tokens(withKind: .throwsKeyword).first {
223-
addDiagnostic(throwsInReturnPosition, .throwsInReturnPosition, fixIts: [
224-
FixIt(message: .moveThrowBeforeArrow, changes: [
225-
.makeMissing(node: throwsInReturnPosition),
226-
.makePresent(node: missingThrowsKeyword),
227-
])
228-
], handledNodes: [unexpectedBeforeReturnType.id, missingThrowsKeyword.id, throwsInReturnPosition.id])
229-
return .visitChildren
230-
}
277+
exchangeTokens(
278+
unexpected: node.output?.unexpectedBetweenArrowAndReturnType,
279+
unexpectedTokenCondition: { $0.tokenKind == .throwsKeyword },
280+
correctTokens: [node.throwsOrRethrowsKeyword],
281+
message: { _ in StaticParserError.throwsInReturnPosition },
282+
fixIt: { MoveTokensInFrontOfFixIt(movedTokens: $0, inFrontOf: .arrow) }
283+
)
231284
return .visitChildren
232285
}
233286

Sources/SwiftParser/Diagnostics/ParserDiagnosticMessages.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ public enum StaticParserError: String, DiagnosticMessage {
8383

8484
// MARK: - Diagnostics (please sort alphabetically)
8585

86+
public struct EffectsSpecifierAfterArrow: ParserError {
87+
public let effectsSpecifiersAfterArrow: [TokenSyntax]
88+
89+
public var message: String {
90+
"\(missingNodesDescription(missingNodes: effectsSpecifiersAfterArrow.map(Syntax.init), commonParent: nil)) may only occur before '->'"
91+
}
92+
}
93+
8694
public struct ExtaneousCodeAtTopLevel: ParserError {
8795
public let extraneousCode: UnexpectedNodesSyntax
8896

@@ -141,11 +149,23 @@ public struct UnexpectedNodesError: ParserError {
141149
public enum StaticParserFixIt: String, FixItMessage {
142150
case insertSemicolon = "insert ';'"
143151
case insertAttributeArguments = "insert attribute argument"
144-
case moveThrowBeforeArrow = "move 'throws' before '->'"
145152

146153
public var message: String { self.rawValue }
147154

148155
public var fixItID: MessageID {
149156
MessageID(domain: diagnosticDomain, id: "\(type(of: self)).\(self)")
150157
}
151158
}
159+
160+
public struct MoveTokensInFrontOfFixIt: ParserFixIt {
161+
/// The token that should be moved
162+
public let movedTokens: [TokenSyntax]
163+
164+
/// The token after which 'try' should be moved
165+
public let inFrontOf: RawTokenKind
166+
167+
public var message: String {
168+
"move \(missingNodesDescription(missingNodes: movedTokens.map(Syntax.init), commonParent: nil)) in front of '\(inFrontOf.nameForDiagnostics)'"
169+
}
170+
}
171+

Sources/SwiftParser/Diagnostics/SyntaxExtensions.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,30 @@ extension UnexpectedNodesSyntax {
2020
func tokens(withKind kind: TokenKind) -> [TokenSyntax] {
2121
return self.tokens(satisfying: { $0.tokenKind == kind })
2222
}
23+
24+
/// If this only contains a single item that is a token, return that token, otherwise return `nil`.
25+
var onlyToken: TokenSyntax? {
26+
return onlyToken(where: { _ in true })
27+
}
28+
29+
/// If this only contains a single item, which is a token satisfying `condition`, return that token, otherwise return `nil`.
30+
func onlyToken(where condition: (TokenSyntax) -> Bool) -> TokenSyntax? {
31+
if self.count == 1, let token = self.first?.as(TokenSyntax.self), condition(token) {
32+
return token
33+
} else {
34+
return nil
35+
}
36+
}
37+
38+
/// If this only contains tokens satisfying `condition`, return an array containing those tokens, otherwise return `nil`.
39+
func onlyTokens(satisfying condition: (TokenSyntax) -> Bool) -> [TokenSyntax]? {
40+
let tokens = tokens(satisfying: condition)
41+
if tokens.count == self.count {
42+
return tokens
43+
} else {
44+
return nil
45+
}
46+
}
2347
}
2448

2549
extension Syntax {

0 commit comments

Comments
 (0)