Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions Sources/SwiftParser/Expressions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2567,6 +2567,7 @@ extension Parser.Lookahead {
mutating func canParseClosureSignature() -> Bool {
// Consume attributes.
var lookahead = self.lookahead()
var sawTopLevelArrowInLookahead = false
var attributesProgress = LoopProgressCondition()
while lookahead.consume(if: .atSign) != nil, lookahead.hasProgressed(&attributesProgress) {
guard lookahead.at(.identifier) else {
Expand Down Expand Up @@ -2630,15 +2631,21 @@ extension Parser.Lookahead {
return false
}

sawTopLevelArrowInLookahead = true

lookahead.consumeEffectsSpecifiers()
}

// Parse the 'in' at the end.
guard lookahead.at(.keyword(.in)) else {
return false
if lookahead.at(.keyword(.in)) {
// Okay, we have a closure signature.
return true
}
// Okay, we have a closure signature.
return true

// Even if 'in' is missing, the presence of a top-level '->' makes this look like a
// closure signature. There's no other valid syntax that could legally
// contain '->' at this position.
return sawTopLevelArrowInLookahead
}
}

Expand Down
43 changes: 43 additions & 0 deletions Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,49 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
if shouldSkip(node) {
return .skipChildren
}
if let unexpected = node.unexpectedBetweenReturnClauseAndInKeyword,
let tokens = unexpected.onlyPresentTokens(satisfying: { _ in true }),
!tokens.isEmpty
{
addDiagnostic(
unexpected,
.unexpectedTokensPriorToIn,
handledNodes: [unexpected.id]
)
let baseInKeyword: TokenSyntax =
node.inKeyword.isMissing
? node.inKeyword
: node.inKeyword.with(\.presence, .missing)
let adjustedInKeyword =
baseInKeyword
.with(\.leadingTrivia, tokens.first?.leadingTrivia ?? baseInKeyword.leadingTrivia)
.with(\.trailingTrivia, [])
addDiagnostic(
unexpected,
.expectedInAfterClosureSignature,
fixIts: [
FixIt(
message: InsertTokenFixIt(missingNodes: [Syntax(adjustedInKeyword)]),
changes: [.makePresent(adjustedInKeyword)]
)
],
handledNodes: [adjustedInKeyword.id]
)
Comment on lines +756 to +774
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the intent of this block?
Also, is unexpectedTokensPriorToIn really needed? UnexpectedNodesSyntax should be automatically handled. I think it should diagnose it like unexpected code in closure signature or something.

Copy link
Author

@kwikysw kwikysw Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the block synthesizing the custom diagnostic for the resync case and reporting the fix-it. This is indeed redundant, only difference being phrasing from the custom diagnostic message in ParserDiagnosticMessages ("unexpected ... in closure signature" instead of after closure signature).
However, if we remove it, we'll still have the generic diagnostic from the UnexpectedNodes visitor, but no fix it.
Should we keep it ?

}

if node.inKeyword.isMissing {
addDiagnostic(
node.inKeyword,
.expectedInAfterClosureSignature,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought missing tokens are also automatically handled (ParseDiagnosticsGenerator.handleMissingToken(_:)). What would happen without this change?

Copy link
Author

@kwikysw kwikysw Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct, this one is actually redundant and still gives the diagnostic, just with a different wording. I'll remove it

fixIts: [
FixIt(
message: InsertTokenFixIt(missingNodes: [Syntax(node.inKeyword)]),
changes: [.makePresent(node.inKeyword)]
)
],
handledNodes: [node.inKeyword.id]
)
}
handleMisplacedEffectSpecifiers(effectSpecifiers: node.effectSpecifiers, output: node.returnClause)
return .visitChildren
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ extension DiagnosticMessage where Self == StaticParserError {
public static var expectedExpressionAfterTry: Self {
.init("expected expression after 'try'")
}
public static var expectedInAfterClosureSignature: Self {
.init("expected 'in' after the closure signature")
}
public static var expectedAssignmentInsteadOfComparisonOperator: Self {
.init("expected '=' instead of '==' to assign default value for parameter")
}
Expand Down Expand Up @@ -266,6 +269,9 @@ extension DiagnosticMessage where Self == StaticParserError {
public static var unexpectedSemicolon: Self {
.init("unexpected ';' separator")
}
public static var unexpectedTokensPriorToIn: Self {
.init("unexpected tokens prior to 'in'")
}
public static var versionComparisonNotNeeded: Self {
.init("version comparison not needed")
}
Expand Down
117 changes: 117 additions & 0 deletions Tests/SwiftParserTest/ClosureMissingInTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
@_spi(ExperimentalLanguageFeatures) @_spi(RawSyntax) import SwiftParser
@_spi(ExperimentalLanguageFeatures) @_spi(RawSyntax) import SwiftSyntax
import XCTest

final class ClosureMissingInTests: ParserTestCase {

func testMissingInAfterSignature() {
assertParse(
"""
_ = { (x: Int) -> Int 1️⃣0 }
""",
diagnostics: [
DiagnosticSpec(
locationMarker: "1️⃣",
message: "expected 'in' after the closure signature",
fixIts: ["insert 'in'"]
)
],
fixedSource: """
_ = { (x: Int) -> Int in 0 }
"""
)
}

func testArrayLiteralNotMisparsedAsSignature() {
assertParse(
"""
_ = { [x, y] }
"""
)
}

func testAsyncIsNotASignatureGate() {
assertParse(
"""
_ = { async }
"""
)
}

func testShorthandParamsWithReturnType() {
assertParse(
"""
_ = { x, _ -> Int 1️⃣x }
""",
diagnostics: [
DiagnosticSpec(
locationMarker: "1️⃣",
message: "expected 'in' after the closure signature",
fixIts: ["insert 'in'"]
)
],
fixedSource: """
_ = { x, _ -> Int in x }
"""
)
}

func testResyncTokensBeforeIn() {
assertParse(
"""
_ = { () -> Int
1️⃣2️⃣0
in
1
}
""",
diagnostics: [
DiagnosticSpec(
locationMarker: "1️⃣",
message: "unexpected tokens prior to 'in'"
),
DiagnosticSpec(
locationMarker: "2️⃣",
message: "expected 'in' after the closure signature",
fixIts: ["insert 'in'"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look right. in is duplicated in the fixedSource.

Copy link
Author

@kwikysw kwikysw Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's because of the resync fix it that adds an 'in', but doesn't remove the previous one. I'll update it depending on whether we should keep the fix it block in our new block or rely on the UnexpectedNodes diagnostic.
But thinking about it this would be pretty cumbersome and tricky to implement

),
],
fixedSource:
"""
_ = { () -> Int
0
in
in
1
}
"""
)
}

func testMissingInInFunctionArgument() {
assertParse(
"""
test(make: { () -> [Int] 1️⃣
return [3]
}, consume: { _ in
print("Test")
})
""",
diagnostics: [
DiagnosticSpec(
locationMarker: "1️⃣",
message: "expected 'in' after the closure signature",
fixIts: ["insert 'in'"]
)
],
fixedSource:
"""
test(make: { () -> [Int] in
return [3]
}, consume: { _ in
print("Test")
})
"""
)
}
}
7 changes: 6 additions & 1 deletion Tests/SwiftParserTest/translated/RecoveryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2445,6 +2445,11 @@ final class RecoveryTests: ParserTestCase {
locationMarker: "3️⃣",
message: "unexpected code '[' in function"
),
DiagnosticSpec(
locationMarker: "4️⃣",
message: "expected 'in' after the closure signature",
fixIts: ["insert 'in'"]
),
DiagnosticSpec(
locationMarker: "4️⃣",
message: "unexpected code ') -> Int {}' in closure"
Expand All @@ -2453,7 +2458,7 @@ final class RecoveryTests: ParserTestCase {
fixedSource: """
#if true
struct Foo19605164 {
func a(s: S) [{{g) -> Int {}
func a(s: S) [{{g in) -> Int {}
}}}
#endif
"""
Expand Down