Skip to content

Commit 8545bf9

Browse files
committed
[SwiftParser] Diagnose missing 'in' after closure signature
Teach SwiftSyntax to mirror the compiler's missing-'in' closure diagnostic: 'canParseClosureSignature' now only succeeds without 'in' when a top-level arrow was seen, 'ParseDiagnosticsGenerator' reports both the "unexpected tokens prior to 'in'" and "expected 'in' after the closure signature" diagnostics with the appropriate fix-it, the diagnostic strings are exposed for clients, the new closure-focused test suite verifies the scenarios the compiler added, and Recovery test 141 is updated to expect the extra diagnostic and rewritten source.
1 parent 8ea19b6 commit 8545bf9

File tree

5 files changed

+183
-5
lines changed

5 files changed

+183
-5
lines changed

Sources/SwiftParser/Expressions.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2567,6 +2567,7 @@ extension Parser.Lookahead {
25672567
mutating func canParseClosureSignature() -> Bool {
25682568
// Consume attributes.
25692569
var lookahead = self.lookahead()
2570+
var sawTopLevelArrowInLookahead = false
25702571
var attributesProgress = LoopProgressCondition()
25712572
while lookahead.consume(if: .atSign) != nil, lookahead.hasProgressed(&attributesProgress) {
25722573
guard lookahead.at(.identifier) else {
@@ -2630,15 +2631,21 @@ extension Parser.Lookahead {
26302631
return false
26312632
}
26322633

2634+
sawTopLevelArrowInLookahead = true
2635+
26332636
lookahead.consumeEffectsSpecifiers()
26342637
}
26352638

26362639
// Parse the 'in' at the end.
2637-
guard lookahead.at(.keyword(.in)) else {
2638-
return false
2640+
if lookahead.at(.keyword(.in)) {
2641+
// Okay, we have a closure signature.
2642+
return true
26392643
}
2640-
// Okay, we have a closure signature.
2641-
return true
2644+
2645+
// Even if 'in' is missing, the presence of a top-level '->' makes this look like a
2646+
// closure signature. There's no other valid syntax that could legally
2647+
// contain '->' at this position.
2648+
return sawTopLevelArrowInLookahead
26422649
}
26432650
}
26442651

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,49 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
744744
if shouldSkip(node) {
745745
return .skipChildren
746746
}
747+
if let unexpected = node.unexpectedBetweenReturnClauseAndInKeyword,
748+
let tokens = unexpected.onlyPresentTokens(satisfying: { _ in true }),
749+
!tokens.isEmpty
750+
{
751+
addDiagnostic(
752+
unexpected,
753+
.unexpectedTokensPriorToIn,
754+
handledNodes: [unexpected.id]
755+
)
756+
let baseInKeyword: TokenSyntax =
757+
node.inKeyword.isMissing
758+
? node.inKeyword
759+
: node.inKeyword.with(\.presence, .missing)
760+
let adjustedInKeyword =
761+
baseInKeyword
762+
.with(\.leadingTrivia, tokens.first?.leadingTrivia ?? baseInKeyword.leadingTrivia)
763+
.with(\.trailingTrivia, [])
764+
addDiagnostic(
765+
unexpected,
766+
.expectedInAfterClosureSignature,
767+
fixIts: [
768+
FixIt(
769+
message: InsertTokenFixIt(missingNodes: [Syntax(adjustedInKeyword)]),
770+
changes: [.makePresent(adjustedInKeyword)]
771+
)
772+
],
773+
handledNodes: [adjustedInKeyword.id]
774+
)
775+
}
776+
777+
if node.inKeyword.isMissing {
778+
addDiagnostic(
779+
node.inKeyword,
780+
.expectedInAfterClosureSignature,
781+
fixIts: [
782+
FixIt(
783+
message: InsertTokenFixIt(missingNodes: [Syntax(node.inKeyword)]),
784+
changes: [.makePresent(node.inKeyword)]
785+
)
786+
],
787+
handledNodes: [node.inKeyword.id]
788+
)
789+
}
747790
handleMisplacedEffectSpecifiers(effectSpecifiers: node.effectSpecifiers, output: node.returnClause)
748791
return .visitChildren
749792
}

Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ extension DiagnosticMessage where Self == StaticParserError {
140140
public static var expectedExpressionAfterTry: Self {
141141
.init("expected expression after 'try'")
142142
}
143+
public static var expectedInAfterClosureSignature: Self {
144+
.init("expected 'in' after the closure signature")
145+
}
143146
public static var expectedAssignmentInsteadOfComparisonOperator: Self {
144147
.init("expected '=' instead of '==' to assign default value for parameter")
145148
}
@@ -266,6 +269,9 @@ extension DiagnosticMessage where Self == StaticParserError {
266269
public static var unexpectedSemicolon: Self {
267270
.init("unexpected ';' separator")
268271
}
272+
public static var unexpectedTokensPriorToIn: Self {
273+
.init("unexpected tokens prior to 'in'")
274+
}
269275
public static var versionComparisonNotNeeded: Self {
270276
.init("version comparison not needed")
271277
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
@_spi(ExperimentalLanguageFeatures) @_spi(RawSyntax) import SwiftParser
2+
@_spi(ExperimentalLanguageFeatures) @_spi(RawSyntax) import SwiftSyntax
3+
import XCTest
4+
5+
final class ClosureMissingInTests: ParserTestCase {
6+
7+
func testMissingInAfterSignature() {
8+
assertParse(
9+
"""
10+
_ = { (x: Int) -> Int 1️⃣0 }
11+
""",
12+
diagnostics: [
13+
DiagnosticSpec(
14+
locationMarker: "1️⃣",
15+
message: "expected 'in' after the closure signature",
16+
fixIts: ["insert 'in'"]
17+
)
18+
],
19+
fixedSource: """
20+
_ = { (x: Int) -> Int in 0 }
21+
"""
22+
)
23+
}
24+
25+
func testArrayLiteralNotMisparsedAsSignature() {
26+
assertParse(
27+
"""
28+
_ = { [x, y] }
29+
"""
30+
)
31+
}
32+
33+
func testAsyncIsNotASignatureGate() {
34+
assertParse(
35+
"""
36+
_ = { async }
37+
"""
38+
)
39+
}
40+
41+
func testShorthandParamsWithReturnType() {
42+
assertParse(
43+
"""
44+
_ = { x, _ -> Int 1️⃣x }
45+
""",
46+
diagnostics: [
47+
DiagnosticSpec(
48+
locationMarker: "1️⃣",
49+
message: "expected 'in' after the closure signature",
50+
fixIts: ["insert 'in'"]
51+
)
52+
],
53+
fixedSource: """
54+
_ = { x, _ -> Int in x }
55+
"""
56+
)
57+
}
58+
59+
func testResyncTokensBeforeIn() {
60+
assertParse(
61+
"""
62+
_ = { () -> Int
63+
1️⃣2️⃣0
64+
in
65+
1
66+
}
67+
""",
68+
diagnostics: [
69+
DiagnosticSpec(
70+
locationMarker: "1️⃣",
71+
message: "unexpected tokens prior to 'in'"
72+
),
73+
DiagnosticSpec(
74+
locationMarker: "2️⃣",
75+
message: "expected 'in' after the closure signature",
76+
fixIts: ["insert 'in'"]
77+
),
78+
],
79+
fixedSource:
80+
"""
81+
_ = { () -> Int
82+
0
83+
in
84+
in
85+
1
86+
}
87+
"""
88+
)
89+
}
90+
91+
func testMissingInInFunctionArgument() {
92+
assertParse(
93+
"""
94+
test(make: { () -> [Int] 1️⃣
95+
return [3]
96+
}, consume: { _ in
97+
print("Test")
98+
})
99+
""",
100+
diagnostics: [
101+
DiagnosticSpec(
102+
locationMarker: "1️⃣",
103+
message: "expected 'in' after the closure signature",
104+
fixIts: ["insert 'in'"]
105+
)
106+
],
107+
fixedSource:
108+
"""
109+
test(make: { () -> [Int] in
110+
return [3]
111+
}, consume: { _ in
112+
print("Test")
113+
})
114+
"""
115+
)
116+
}
117+
}

Tests/SwiftParserTest/translated/RecoveryTests.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2445,6 +2445,11 @@ final class RecoveryTests: ParserTestCase {
24452445
locationMarker: "3️⃣",
24462446
message: "unexpected code '[' in function"
24472447
),
2448+
DiagnosticSpec(
2449+
locationMarker: "4️⃣",
2450+
message: "expected 'in' after the closure signature",
2451+
fixIts: ["insert 'in'"]
2452+
),
24482453
DiagnosticSpec(
24492454
locationMarker: "4️⃣",
24502455
message: "unexpected code ') -> Int {}' in closure"
@@ -2453,7 +2458,7 @@ final class RecoveryTests: ParserTestCase {
24532458
fixedSource: """
24542459
#if true
24552460
struct Foo19605164 {
2456-
func a(s: S) [{{g) -> Int {}
2461+
func a(s: S) [{{g in) -> Int {}
24572462
}}}
24582463
#endif
24592464
"""

0 commit comments

Comments
 (0)