Skip to content

Commit b05dfb6

Browse files
authored
Merge pull request #2012 from kimdv/kimdv/1925-missing-parentheses-in-closure-signature-produces-bad-diagnostic
Add correct diagnostic for missing parentheses
2 parents 8d61458 + 7fb3e7f commit b05dfb6

File tree

2 files changed

+136
-27
lines changed

2 files changed

+136
-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()
@@ -2430,52 +2432,49 @@ extension Parser.Lookahead {
24302432
}
24312433

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

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

24662462
return false
24672463
}
2464+
}
24682465

2469-
lookahead.consumeEffectsSpecifiers()
2466+
// Consume the ')', if it's there.
2467+
lookahead.consume(if: .rightParen)
24702468

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

2477-
lookahead.consumeEffectsSpecifiers()
2471+
// Parse the func-signature-result, if present.
2472+
if lookahead.consume(if: .arrow) != nil {
2473+
guard lookahead.canParseType() else {
2474+
return false
24782475
}
2476+
2477+
lookahead.consumeEffectsSpecifiers()
24792478
}
24802479

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

Tests/SwiftParserTest/ExpressionTests.swift

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2632,5 +2632,115 @@ final class StatementExpressionTests: ParserTestCase {
26322632
assertParse("_ = { (@_noImplicitCopy _ x: Int) -> () in }")
26332633
26342634
assertParse("_ = { (@Wrapper x) in }")
2635+
2636+
assertParse(
2637+
"""
2638+
withInvalidOrderings { (comparisonPredicate: @escaping (Int, Int) -> Bool) in
2639+
}
2640+
"""
2641+
)
2642+
}
2643+
2644+
func testClosureWithMissingParentheses() {
2645+
assertParse(
2646+
"""
2647+
_ = { 1️⃣a: Int, b: Int 2️⃣in
2648+
}
2649+
""",
2650+
diagnostics: [
2651+
DiagnosticSpec(
2652+
locationMarker: "1️⃣",
2653+
message: "expected '(' to start parameter clause",
2654+
fixIts: ["insert '('"]
2655+
),
2656+
DiagnosticSpec(
2657+
locationMarker: "2️⃣",
2658+
message: "expected ')' to end parameter clause",
2659+
fixIts: ["insert ')'"]
2660+
),
2661+
],
2662+
fixedSource: """
2663+
_ = { (a: Int, b: Int) in
2664+
}
2665+
"""
2666+
)
2667+
}
2668+
2669+
func testClosureWithReturnArrowAndMissingParentheses() {
2670+
assertParse(
2671+
"""
2672+
_ = { 1️⃣a: Int, b: 2️⃣Int 3️⃣-> Int 4️⃣in
2673+
}
2674+
""",
2675+
diagnostics: [
2676+
DiagnosticSpec(
2677+
locationMarker: "1️⃣",
2678+
message: "expected '(' to start parameter clause",
2679+
fixIts: ["insert '('"]
2680+
),
2681+
DiagnosticSpec(
2682+
locationMarker: "2️⃣",
2683+
message: "expected '(' to start function type",
2684+
fixIts: ["insert '('"]
2685+
),
2686+
DiagnosticSpec(
2687+
locationMarker: "3️⃣",
2688+
message: "expected ')' in function type",
2689+
fixIts: ["insert ')'"]
2690+
),
2691+
DiagnosticSpec(
2692+
locationMarker: "4️⃣",
2693+
message: "expected ')' to end parameter clause",
2694+
fixIts: ["insert ')'"]
2695+
),
2696+
],
2697+
fixedSource: """
2698+
_ = { (a: Int, b: (Int) -> Int) in
2699+
}
2700+
"""
2701+
)
2702+
}
2703+
2704+
func testClosureWithMissingLeftParenthese() {
2705+
assertParse(
2706+
"""
2707+
_ = { 1️⃣a: Int, b: Int) in
2708+
}
2709+
""",
2710+
diagnostics: [
2711+
DiagnosticSpec(
2712+
locationMarker: "1️⃣",
2713+
message: "expected '(' to start parameter clause",
2714+
fixIts: ["insert '('"]
2715+
)
2716+
],
2717+
fixedSource: """
2718+
_ = { (a: Int, b: Int) in
2719+
}
2720+
"""
2721+
)
2722+
}
2723+
2724+
func testClosureWithDollarIdentifier() {
2725+
assertParse(
2726+
"""
2727+
let (ids, (actions, tracking)) = state.withCriticalRegion { ($0.valueObservers(for: keyPath), $0.didSet(keyPath: keyPath)) }
2728+
"""
2729+
)
2730+
2731+
assertParse(
2732+
"""
2733+
let (ids, (actions, tracking)) = state.withCriticalRegion { ($0.valueObservers(for: keyPath), $0.didSet(keyPath: keyPath)) }
2734+
"""
2735+
)
2736+
2737+
assertParse(
2738+
"""
2739+
state.withCriticalRegion { (1 + 2) }
2740+
for action in tracking {
2741+
action()
2742+
}
2743+
"""
2744+
)
26352745
}
26362746
}

0 commit comments

Comments
 (0)