diff --git a/Sources/SwiftParser/Nominals.swift b/Sources/SwiftParser/Nominals.swift index bbd15cf5618..eebdcd6fa0c 100644 --- a/Sources/SwiftParser/Nominals.swift +++ b/Sources/SwiftParser/Nominals.swift @@ -361,6 +361,11 @@ extension Parser { arena: self.arena ) ) + + // If this was a trailing comma, there are no more elements + if at(prefix: ">") { + break + } } while keepGoing != nil && self.hasProgressed(&loopProgress) } let rangle = self.expectWithoutRecovery(prefix: ">", as: .rightAngle) diff --git a/Sources/SwiftParser/Types.swift b/Sources/SwiftParser/Types.swift index 67c7604ce0f..58eacfa59b9 100644 --- a/Sources/SwiftParser/Types.swift +++ b/Sources/SwiftParser/Types.swift @@ -430,6 +430,11 @@ extension Parser { arena: self.arena ) ) + + // If this was a trailing comma, we're done parsing the list + if self.at(prefix: ">") { + break + } } while keepGoing != nil && self.hasProgressed(&loopProgress) } @@ -1052,7 +1057,8 @@ extension Parser.Lookahead { return false } // Parse the comma, if the list continues. - } while self.consume(if: .comma) != nil && self.hasProgressed(&loopProgress) + // This could be the trailing comma. + } while self.consume(if: .comma) != nil && !self.at(prefix: ">") && self.hasProgressed(&loopProgress) } guard self.consume(ifPrefix: ">", as: .rightAngle) != nil else { diff --git a/Tests/SwiftParserTest/DeclarationTests.swift b/Tests/SwiftParserTest/DeclarationTests.swift index 7ec097ec5bd..f55aa4c1a3d 100644 --- a/Tests/SwiftParserTest/DeclarationTests.swift +++ b/Tests/SwiftParserTest/DeclarationTests.swift @@ -3529,4 +3529,31 @@ final class DeclarationTests: ParserTestCase { ] ) } + + func testTrailingCommas() { + assertParse( + """ + protocol Baaz< + Foo, + Bar, + > { + associatedtype Foo + associatedtype Bar + } + """ + ) + + assertParse( + """ + struct Foo< + T1, + T2, + T3, + >: Baaz< + T1, + T2, + > {} + """ + ) + } } diff --git a/Tests/SwiftParserTest/ExpressionTests.swift b/Tests/SwiftParserTest/ExpressionTests.swift index e63d753d2da..3eb7d83add2 100644 --- a/Tests/SwiftParserTest/ExpressionTests.swift +++ b/Tests/SwiftParserTest/ExpressionTests.swift @@ -2125,6 +2125,26 @@ final class ExpressionTests: ParserTestCase { """ ) } + + func testTrailingCommasInTypeExpressions() { + assertParse( + """ + let _ = Foo2.self + """ + ) + + assertParse( + """ + let _ = Foo2() + """ + ) + + assertParse( + """ + let _ = ((Int, Bool, String,) -> Void).self + """ + ) + } } final class MemberExprTests: ParserTestCase { diff --git a/Tests/SwiftParserTest/TypeTests.swift b/Tests/SwiftParserTest/TypeTests.swift index 6e49d5fa75d..2d317551c14 100644 --- a/Tests/SwiftParserTest/TypeTests.swift +++ b/Tests/SwiftParserTest/TypeTests.swift @@ -734,6 +734,44 @@ final class TypeTests: ParserTestCase { fixedSource: "func foo(test: nonisolated(nonsendinghello) () async -> Void)" ) } + + func testTrailingCommas() { + assertParse( + """ + let foo: ( + bar: String, + quux: String, + ) + """ + ) + + assertParse( + """ + let closure: ( + String, + String, + ) -> ( + bar: String, + quux: String, + ) + """ + ) + + assertParse( + """ + struct Foo {} + + typealias Bar< + T1, + T2, + > = Foo< + T1, + T2, + Bool, + > + """ + ) + } } final class InlineArrayTypeTests: ParserTestCase {