Skip to content

Commit f3168d0

Browse files
committed
Improve parser recovery and diagnostics for typed throws
1 parent 7af0beb commit f3168d0

File tree

5 files changed

+206
-8
lines changed

5 files changed

+206
-8
lines changed

Sources/SwiftParser/Specifiers.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -652,10 +652,10 @@ extension Parser {
652652
let (unexpected, throwsKw) = self.eat(handle)
653653
unexpectedBeforeThrows.append(contentsOf: unexpected?.elements ?? [])
654654
throwsKeyword = throwsKw
655-
}
656655

657-
if throwsKeyword != nil && self.at(.leftParen) && experimentalFeatures.contains(.typedThrows) {
658-
thrownType = parseThrownTypeClause()
656+
if self.at(.leftParen) && experimentalFeatures.contains(.typedThrows) {
657+
thrownType = parseThrownTypeClause()
658+
}
659659
}
660660

661661
var unexpectedAfterThrownTypeLoopProgress = LoopProgressCondition()
@@ -675,7 +675,9 @@ extension Parser {
675675
}
676676
}
677677

678-
if unexpectedBeforeAsync.isEmpty && asyncKeyword == nil && unexpectedBeforeThrows.isEmpty && throwsKeyword == nil && thrownType == nil && unexpectedAfterThrownType.isEmpty {
678+
if unexpectedBeforeAsync.isEmpty && asyncKeyword == nil && unexpectedBeforeThrows.isEmpty && throwsKeyword == nil && thrownType == nil
679+
&& unexpectedAfterThrownType.isEmpty
680+
{
679681
return nil
680682
}
681683

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,10 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
258258
(node.throwsSpecifier, { ThrowsEffectSpecifier(token: $0) != nil }, StaticParserError.misspelledThrows),
259259
]
260260

261-
let unexpectedNodes = [node.unexpectedBeforeAsyncSpecifier, node.unexpectedBetweenAsyncSpecifierAndThrowsSpecifier,node.unexpectedBetweenThrowsSpecifierAndThrownType, node.unexpectedAfterThrownType]
261+
let unexpectedNodes = [
262+
node.unexpectedBeforeAsyncSpecifier, node.unexpectedBetweenAsyncSpecifierAndThrowsSpecifier, node.unexpectedBetweenThrowsSpecifierAndThrownType,
263+
node.unexpectedAfterThrownType,
264+
]
262265

263266
// Diagnostics that are emitted later silence previous diagnostics, so check
264267
// for the most contextual (and thus helpful) diagnostics last.
@@ -279,7 +282,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
279282
}
280283
}
281284

282-
if let throwsSpecifier = node.throwsSpecifier, node.thrownType == nil {
285+
if let throwsSpecifier = node.throwsSpecifier {
283286
exchangeTokens(
284287
unexpected: node.unexpectedAfterThrownType,
285288
unexpectedTokenCondition: { AsyncEffectSpecifier(token: $0) != nil },

Tests/SwiftParserTest/DeclarationTests.swift

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,115 @@ final class DeclarationTests: ParserTestCase {
905905
""",
906906
experimentalFeatures: [.typedThrows]
907907
)
908+
909+
assertParse(
910+
"func test() throws(MyError) 1️⃣async {}",
911+
diagnostics: [
912+
DiagnosticSpec(message: "'async' must precede 'throws'", fixIts: ["move 'async' in front of 'throws'"])
913+
],
914+
fixedSource: "func test() async throws(MyError) {}",
915+
experimentalFeatures: [.typedThrows]
916+
)
917+
assertParse(
918+
"func test() throws 1️⃣async2️⃣(MyError) {}",
919+
diagnostics: [
920+
DiagnosticSpec(locationMarker: "1️⃣", message: "'async' must precede 'throws'", fixIts: ["move 'async' in front of 'throws'"]),
921+
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '(MyError)' in function"),
922+
],
923+
fixedSource: "func test() async throws (MyError) {}",
924+
experimentalFeatures: [.typedThrows]
925+
)
926+
assertParse(
927+
"func test() 1️⃣try2️⃣(MyError) async {}",
928+
diagnostics: [
929+
DiagnosticSpec(locationMarker: "1️⃣", message: "expected throwing specifier; did you mean 'throws'?", fixIts: ["replace 'try' with 'throws'"]),
930+
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '(MyError) async' in function"),
931+
],
932+
fixedSource: "func test() throws (MyError) async {}",
933+
experimentalFeatures: [.typedThrows]
934+
)
935+
assertParse(
936+
"func test() 1️⃣try 2️⃣async3️⃣(MyError) {}",
937+
diagnostics: [
938+
DiagnosticSpec(locationMarker: "1️⃣", message: "expected throwing specifier; did you mean 'throws'?", fixIts: ["replace 'try' with 'throws'"]),
939+
DiagnosticSpec(locationMarker: "2️⃣", message: "'async' must precede 'throws'", fixIts: ["move 'async' in front of 'throws'"]),
940+
DiagnosticSpec(locationMarker: "3️⃣", message: "unexpected code '(MyError)' in function"),
941+
],
942+
fixedSource: "func test() async throws (MyError) {}",
943+
experimentalFeatures: [.typedThrows]
944+
)
945+
assertParse(
946+
"func test() throws(MyError) 1️⃣await {}",
947+
diagnostics: [
948+
DiagnosticSpec(locationMarker: "1️⃣", message: "'await' must precede 'throws'", fixIts: ["move 'await' in front of 'throws'"])
949+
],
950+
fixedSource: "func test() async throws(MyError) {}",
951+
experimentalFeatures: [.typedThrows]
952+
)
953+
assertParse(
954+
"func test() throws 1️⃣await2️⃣(MyError) {}",
955+
diagnostics: [
956+
DiagnosticSpec(locationMarker: "1️⃣", message: "'await' must precede 'throws'", fixIts: ["move 'await' in front of 'throws'"]),
957+
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '(MyError)' in function"),
958+
],
959+
fixedSource: "func test() async throws (MyError) {}",
960+
experimentalFeatures: [.typedThrows]
961+
)
962+
assertParse(
963+
"func test() 1️⃣try2️⃣(MyError) await {}",
964+
diagnostics: [
965+
DiagnosticSpec(locationMarker: "1️⃣", message: "expected throwing specifier; did you mean 'throws'?", fixIts: ["replace 'try' with 'throws'"]),
966+
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '(MyError) await' in function"),
967+
],
968+
fixedSource: "func test() throws (MyError) await {}",
969+
experimentalFeatures: [.typedThrows]
970+
)
971+
assertParse(
972+
"func test() 1️⃣try await2️⃣(MyError) {}",
973+
diagnostics: [
974+
DiagnosticSpec(locationMarker: "1️⃣", message: "expected throwing specifier; did you mean 'throws'?", fixIts: ["replace 'try' with 'throws'"]),
975+
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '(MyError)' in function"),
976+
],
977+
fixedSource: "func test() awaitthrows (MyError) {}", // FIXME: spacing
978+
experimentalFeatures: [.typedThrows]
979+
)
980+
assertParse(
981+
"func test() async1️⃣(MyError) {}",
982+
diagnostics: [
983+
DiagnosticSpec(message: "unexpected code '(MyError)' in function")
984+
],
985+
experimentalFeatures: [.typedThrows]
986+
)
987+
assertParse(
988+
"func test() 1️⃣await2️⃣(MyError) {}",
989+
diagnostics: [
990+
DiagnosticSpec(locationMarker: "1️⃣", message: "expected async specifier; did you mean 'async'?", fixIts: ["replace 'await' with 'async'"]),
991+
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '(MyError)' in function"),
992+
],
993+
fixedSource: "func test() async (MyError) {}",
994+
experimentalFeatures: [.typedThrows]
995+
)
996+
assertParse(
997+
"func test() 1️⃣try2️⃣(MyError) {}",
998+
diagnostics: [
999+
DiagnosticSpec(locationMarker: "1️⃣", message: "expected throwing specifier; did you mean 'throws'?", fixIts: ["replace 'try' with 'throws'"]),
1000+
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '(MyError)' in function"),
1001+
],
1002+
fixedSource: "func test() throws (MyError) {}",
1003+
experimentalFeatures: [.typedThrows]
1004+
)
1005+
assertParse(
1006+
"func test() throws(MyError) {}",
1007+
experimentalFeatures: [.typedThrows]
1008+
)
1009+
assertParse(
1010+
"func test() throws(MyError)1️⃣async {}", // no space between closing parenthesis and `async`
1011+
diagnostics: [
1012+
DiagnosticSpec(message: "'async' must precede 'throws'", fixIts: ["move 'async' in front of 'throws'"])
1013+
],
1014+
fixedSource: "func test() async throws(MyError){}",
1015+
experimentalFeatures: [.typedThrows]
1016+
)
9081017
}
9091018

9101019
func testExtraneousRightBraceRecovery() {

Tests/SwiftParserTest/ExpressionTests.swift

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
@_spi(RawSyntax) import SwiftParser
13+
@_spi(RawSyntax) @_spi(ExperimentalLanguageFeatures) import SwiftParser
1414
@_spi(RawSyntax) import SwiftSyntax
1515
import XCTest
1616

@@ -2742,4 +2742,88 @@ final class StatementExpressionTests: ParserTestCase {
27422742
"""
27432743
)
27442744
}
2745+
2746+
func testTypedThrowsDisambiguation() {
2747+
assertParse(
2748+
"[() throws(MyError) 1️⃣async -> Void]()",
2749+
diagnostics: [
2750+
DiagnosticSpec(message: "'async' must precede 'throws'", fixIts: ["move 'async' in front of 'throws'"])
2751+
],
2752+
fixedSource: "[() async throws(MyError) -> Void]()",
2753+
experimentalFeatures: .typedThrows
2754+
)
2755+
assertParse(
2756+
"[() throws 1️⃣async2️⃣(MyError) -> Void]()",
2757+
diagnostics: [
2758+
DiagnosticSpec(locationMarker: "1️⃣", message: "'async' must precede 'throws'", fixIts: ["move 'async' in front of 'throws'"]),
2759+
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '(MyError)' in array element"),
2760+
],
2761+
fixedSource: "[() async throws (MyError) -> Void]()",
2762+
experimentalFeatures: .typedThrows
2763+
)
2764+
assertParse(
2765+
"[() try(MyError) async -> Void]()",
2766+
experimentalFeatures: .typedThrows
2767+
)
2768+
assertParse(
2769+
"[() try async(MyError) -> Void]()",
2770+
experimentalFeatures: .typedThrows
2771+
)
2772+
assertParse(
2773+
"[() throws(MyError) 1️⃣await -> Void]()",
2774+
diagnostics: [
2775+
DiagnosticSpec(locationMarker: "1️⃣", message: "'await' must precede 'throws'", fixIts: ["move 'await' in front of 'throws'"])
2776+
],
2777+
fixedSource: "[() async throws(MyError) -> Void]()",
2778+
experimentalFeatures: .typedThrows
2779+
)
2780+
assertParse(
2781+
"[() throws 1️⃣await2️⃣(MyError) -> Void]()",
2782+
diagnostics: [
2783+
DiagnosticSpec(locationMarker: "1️⃣", message: "'await' must precede 'throws'", fixIts: ["move 'await' in front of 'throws'"]),
2784+
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '(MyError)' in array element"),
2785+
],
2786+
fixedSource: "[() async throws (MyError) -> Void]()",
2787+
experimentalFeatures: .typedThrows
2788+
)
2789+
assertParse(
2790+
"[() try(MyError) await 1️⃣-> Void]()",
2791+
diagnostics: [
2792+
DiagnosticSpec(
2793+
message: "expected expression in 'await' expression",
2794+
fixIts: ["insert expression"]
2795+
)
2796+
],
2797+
fixedSource: "[() try(MyError) await <#expression#> -> Void]()",
2798+
experimentalFeatures: .typedThrows
2799+
)
2800+
assertParse(
2801+
"[() try await(MyError) -> Void]()",
2802+
experimentalFeatures: .typedThrows
2803+
)
2804+
assertParse(
2805+
"[() async(MyError) -> Void]()",
2806+
experimentalFeatures: .typedThrows
2807+
)
2808+
assertParse(
2809+
"[() await(MyError) -> Void]()",
2810+
experimentalFeatures: .typedThrows
2811+
)
2812+
assertParse(
2813+
"[() try(MyError) -> Void]()",
2814+
experimentalFeatures: .typedThrows
2815+
)
2816+
assertParse(
2817+
"[() throws(MyError) -> Void]()",
2818+
experimentalFeatures: .typedThrows
2819+
)
2820+
assertParse(
2821+
"X<() throws(MyError) -> Int>()",
2822+
experimentalFeatures: .typedThrows
2823+
)
2824+
assertParse(
2825+
"X<() async throws(MyError) -> Int>()",
2826+
experimentalFeatures: .typedThrows
2827+
)
2828+
}
27452829
}

Tests/SwiftParserTest/TypeTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
@_spi(RawSyntax) @_spi(ExperimentalLanguageFeatures) import SwiftParser
13+
@_spi(RawSyntax) @_spi(ExperimentalLanguageFeatures) import SwiftParser
1414
@_spi(RawSyntax) import SwiftSyntax
1515
import XCTest
1616

0 commit comments

Comments
 (0)