Skip to content

Commit c54b023

Browse files
committed
Add correct diagnostic for missing parentheses
1 parent 777520e commit c54b023

File tree

2 files changed

+106
-27
lines changed

2 files changed

+106
-27
lines changed

Sources/SwiftParser/Expressions.swift

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1746,7 +1746,9 @@ extension Parser {
17461746
var effectSpecifiers: RawTypeEffectSpecifiersSyntax?
17471747
var returnClause: RawReturnClauseSyntax? = nil
17481748
if !self.at(.keyword(.in)) {
1749-
if self.at(.leftParen) {
1749+
// If the next token is ':', then it looks like the code contained a non-shorthand closure parameter with a type annotation.
1750+
// These need to be wrapped in parentheses.
1751+
if self.at(.leftParen) || self.peek(isAt: .colon) {
17501752
// Parse the closure arguments.
17511753
let params = self.parseParameterClause(RawClosureParameterClauseSyntax.self) { parser in
17521754
parser.parseClosureParameter()
@@ -2427,52 +2429,49 @@ extension Parser.Lookahead {
24272429
}
24282430

24292431
// Parse pattern-tuple func-signature-result? 'in'.
2430-
if lookahead.consume(if: .leftParen) != nil { // Consume the ')'.
2431-
2432+
if lookahead.consume(if: .leftParen) != nil { // Consume the '('.
24322433
// While we don't have '->' or ')', eat balanced tokens.
24332434
var skipProgress = LoopProgressCondition()
2434-
while !lookahead.at(.endOfFile, .rightParen) && lookahead.hasProgressed(&skipProgress) {
2435+
while !lookahead.at(.endOfFile, .rightParen, .keyword(.in)) && !lookahead.at(.arrow) && lookahead.hasProgressed(&skipProgress) {
24352436
lookahead.skipSingle()
24362437
}
2437-
2438-
// Consume the ')', if it's there.
2439-
if lookahead.consume(if: .rightParen) != nil {
2440-
lookahead.consumeEffectsSpecifiers()
2441-
2442-
// Parse the func-signature-result, if present.
2443-
if lookahead.consume(if: .arrow) != nil {
2444-
guard lookahead.canParseType() else {
2445-
return false
2446-
}
2447-
2448-
lookahead.consumeEffectsSpecifiers()
2449-
}
2450-
}
2451-
// Okay, we have a closure signature.
24522438
} else if lookahead.at(.identifier) || lookahead.at(.wildcard) {
24532439
// Parse identifier (',' identifier)*
24542440
lookahead.consumeAnyToken()
24552441

2442+
/// If the next token is a colon, interpret is as a type annotation and consume a type after it.
2443+
/// While type annotations aren’t allowed in shorthand closure parameters, we consume them to improve recovery.
2444+
func consumeOptionalTypeAnnotation() -> Bool {
2445+
if lookahead.consume(if: .colon) != nil {
2446+
return lookahead.canParseType()
2447+
} else {
2448+
return true
2449+
}
2450+
}
2451+
24562452
var parametersProgress = LoopProgressCondition()
2457-
while lookahead.consume(if: .comma) != nil && lookahead.hasProgressed(&parametersProgress) {
2453+
while consumeOptionalTypeAnnotation() && lookahead.consume(if: .comma) != nil && lookahead.hasProgressed(&parametersProgress) {
24582454
if lookahead.at(.identifier) || lookahead.at(.wildcard) {
24592455
lookahead.consumeAnyToken()
24602456
continue
24612457
}
24622458

24632459
return false
24642460
}
2461+
}
24652462

2466-
lookahead.consumeEffectsSpecifiers()
2463+
// Consume the ')', if it's there.
2464+
lookahead.consume(if: .rightParen)
24672465

2468-
// Parse the func-signature-result, if present.
2469-
if lookahead.consume(if: .arrow) != nil {
2470-
guard lookahead.canParseType() else {
2471-
return false
2472-
}
2466+
lookahead.consumeEffectsSpecifiers()
24732467

2474-
lookahead.consumeEffectsSpecifiers()
2468+
// Parse the func-signature-result, if present.
2469+
if lookahead.consume(if: .arrow) != nil {
2470+
guard lookahead.canParseType() else {
2471+
return false
24752472
}
2473+
2474+
lookahead.consumeEffectsSpecifiers()
24762475
}
24772476

24782477
// Parse the 'in' at the end.

Tests/SwiftParserTest/ExpressionTests.swift

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2633,4 +2633,84 @@ final class StatementExpressionTests: XCTestCase {
26332633
26342634
assertParse("_ = { (@Wrapper x) in }")
26352635
}
2636+
2637+
func testClosureWithMissingParentheses() {
2638+
assertParse(
2639+
"""
2640+
_ = { 1️⃣a: Int, b: Int 2️⃣in
2641+
}
2642+
""",
2643+
diagnostics: [
2644+
DiagnosticSpec(
2645+
locationMarker: "1️⃣",
2646+
message: "expected '(' to start parameter clause",
2647+
fixIts: ["insert '('"]
2648+
),
2649+
DiagnosticSpec(
2650+
locationMarker: "2️⃣",
2651+
message: "expected ')' to end parameter clause",
2652+
fixIts: ["insert ')'"]
2653+
),
2654+
],
2655+
fixedSource: """
2656+
_ = { (a: Int, b: Int) in
2657+
}
2658+
"""
2659+
)
2660+
}
2661+
2662+
func testClosureWithReturnArrowAndMissingParentheses() {
2663+
assertParse(
2664+
"""
2665+
_ = { 1️⃣a: Int, b: 2️⃣Int 3️⃣-> Int 4️⃣in
2666+
}
2667+
""",
2668+
diagnostics: [
2669+
DiagnosticSpec(
2670+
locationMarker: "1️⃣",
2671+
message: "expected '(' to start parameter clause",
2672+
fixIts: ["insert '('"]
2673+
),
2674+
DiagnosticSpec(
2675+
locationMarker: "2️⃣",
2676+
message: "expected '(' to start function type",
2677+
fixIts: ["insert '('"]
2678+
),
2679+
DiagnosticSpec(
2680+
locationMarker: "3️⃣",
2681+
message: "expected ')' in function type",
2682+
fixIts: ["insert ')'"]
2683+
),
2684+
DiagnosticSpec(
2685+
locationMarker: "4️⃣",
2686+
message: "expected ')' to end parameter clause",
2687+
fixIts: ["insert ')'"]
2688+
),
2689+
],
2690+
fixedSource: """
2691+
_ = { (a: Int, b: (Int) -> Int) in
2692+
}
2693+
"""
2694+
)
2695+
}
2696+
2697+
func testClosureWithMissingLeftParenthese() {
2698+
assertParse(
2699+
"""
2700+
_ = { 1️⃣a: Int, b: Int) in
2701+
}
2702+
""",
2703+
diagnostics: [
2704+
DiagnosticSpec(
2705+
locationMarker: "1️⃣",
2706+
message: "expected '(' to start parameter clause",
2707+
fixIts: ["insert '('"]
2708+
)
2709+
],
2710+
fixedSource: """
2711+
_ = { (a: Int, b: Int) in
2712+
}
2713+
"""
2714+
)
2715+
}
26362716
}

0 commit comments

Comments
 (0)