Skip to content

Commit 8356e1c

Browse files
committed
Add correct diagnostic for missing parentheses
1 parent 83348e7 commit 8356e1c

File tree

2 files changed

+96
-27
lines changed

2 files changed

+96
-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: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2689,4 +2689,74 @@ final class StatementExpressionTests: XCTestCase {
26892689
26902690
assertParse("_ = { (@Wrapper x) in }")
26912691
}
2692+
2693+
func testClosureWithMissingParentheses() {
2694+
assertParse(
2695+
"""
2696+
_ = { 1️⃣a: Int, b: Int 2️⃣in
2697+
}
2698+
""",
2699+
diagnostics: [
2700+
DiagnosticSpec(
2701+
locationMarker: "1️⃣",
2702+
message: "expected '(' to start parameter clause",
2703+
fixIts: ["insert '('"]
2704+
),
2705+
DiagnosticSpec(
2706+
locationMarker: "2️⃣",
2707+
message: "expected ')' to end parameter clause",
2708+
fixIts: ["insert ')'"]
2709+
),
2710+
],
2711+
fixedSource: """
2712+
_ = { (a: Int, b: Int) in
2713+
}
2714+
"""
2715+
)
2716+
}
2717+
2718+
func testClosureWithReturnArrowAndMissingParentheses() {
2719+
assertParse(
2720+
"""
2721+
_ = { 1️⃣a: Int, b: Int 2️⃣ -> Int in
2722+
}
2723+
""",
2724+
diagnostics: [
2725+
DiagnosticSpec(
2726+
locationMarker: "1️⃣",
2727+
message: "expected '(' to start parameter clause",
2728+
fixIts: ["insert '('"]
2729+
),
2730+
DiagnosticSpec(
2731+
locationMarker: "2️⃣",
2732+
message: "expected ')' to end parameter clause",
2733+
fixIts: ["insert ')'"]
2734+
),
2735+
],
2736+
fixedSource: """
2737+
_ = { (a: Int, b: Int) in
2738+
}
2739+
"""
2740+
)
2741+
}
2742+
2743+
func testClosureWithMissingLeftParenthese() {
2744+
assertParse(
2745+
"""
2746+
_ = { 1️⃣a: Int, b: Int) in
2747+
}
2748+
""",
2749+
diagnostics: [
2750+
DiagnosticSpec(
2751+
locationMarker: "1️⃣",
2752+
message: "expected '(' to start parameter clause",
2753+
fixIts: ["insert '('"]
2754+
)
2755+
],
2756+
fixedSource: """
2757+
_ = { (a: Int, b: Int) in
2758+
}
2759+
"""
2760+
)
2761+
}
26922762
}

0 commit comments

Comments
 (0)