diff --git a/Sources/SwiftParser/Expressions.swift b/Sources/SwiftParser/Expressions.swift index b768e586462..41f45d22602 100644 --- a/Sources/SwiftParser/Expressions.swift +++ b/Sources/SwiftParser/Expressions.swift @@ -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 { @@ -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 } } diff --git a/Tests/SwiftParserTest/ClosureMissingInTests.swift b/Tests/SwiftParserTest/ClosureMissingInTests.swift new file mode 100644 index 00000000000..7775bc04147 --- /dev/null +++ b/Tests/SwiftParserTest/ClosureMissingInTests.swift @@ -0,0 +1,115 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@_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' in 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' in closure signature", + fixIts: ["insert 'in'"] + ) + ], + fixedSource: """ + _ = { x, _ -> Int in x } + """ + ) + } + + func testResyncTokensBeforeIn() { + assertParse( + """ + _ = { () -> Int + 1️⃣0 + in + 1 + } + """, + diagnostics: [ + DiagnosticSpec( + locationMarker: "1️⃣", + message: "unexpected code '0' in closure signature" + ) + ] + ) + } + + func testMissingInInFunctionArgument() { + assertParse( + """ + test(make: { () -> [Int] 1️⃣ + return [3] + }, consume: { _ in + print("Test") + }) + """, + diagnostics: [ + DiagnosticSpec( + locationMarker: "1️⃣", + message: "expected 'in' in closure signature", + fixIts: ["insert 'in'"] + ) + ], + fixedSource: + """ + test(make: { () -> [Int] in + return [3] + }, consume: { _ in + print("Test") + }) + """ + ) + } +} diff --git a/Tests/SwiftParserTest/translated/RecoveryTests.swift b/Tests/SwiftParserTest/translated/RecoveryTests.swift index dcc01fdcfef..4bb52f7411c 100644 --- a/Tests/SwiftParserTest/translated/RecoveryTests.swift +++ b/Tests/SwiftParserTest/translated/RecoveryTests.swift @@ -2445,6 +2445,11 @@ final class RecoveryTests: ParserTestCase { locationMarker: "3️⃣", message: "unexpected code '[' in function" ), + DiagnosticSpec( + locationMarker: "4️⃣", + message: "expected 'in' in closure signature", + fixIts: ["insert 'in'"] + ), DiagnosticSpec( locationMarker: "4️⃣", message: "unexpected code ') -> Int {}' in closure" @@ -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 """