diff --git a/Sources/SwiftParser/Expressions.swift b/Sources/SwiftParser/Expressions.swift index bf1c9fba52d..c8d46055cbe 100644 --- a/Sources/SwiftParser/Expressions.swift +++ b/Sources/SwiftParser/Expressions.swift @@ -1103,8 +1103,48 @@ extension Parser { continue } - // Check for a .name or .1 suffix. + // Check for a .name, .1, .name(), .name("Kiwi"), .name(fruit:), + // .name(_:), .name(fruit: "Kiwi) suffix. if self.at(.period) { + // Parse as a keypath method if fully applied. + if self.experimentalFeatures.contains(.keypathWithMethodMembers) + && self.withLookahead({ $0.isAppliedKeyPathMethod() }) + { + let (unexpectedPeriod, period, declName, _) = parseDottedExpressionSuffix( + previousNode: components.last?.raw ?? rootType?.raw ?? backslash.raw + ) + let leftParen = self.consumeAnyToken() + var args: [RawLabeledExprSyntax] = [] + if !self.at(.rightParen) { + args = self.parseArgumentListElements( + pattern: pattern, + allowTrailingComma: true + ) + } + let (unexpectedBeforeRParen, rightParen) = self.expect(.rightParen) + components.append( + RawKeyPathComponentSyntax( + unexpectedPeriod, + period: period, + component: .method( + RawKeyPathMethodComponentSyntax( + declName: declName, + leftParen: leftParen, + arguments: RawLabeledExprListSyntax( + elements: args, + arena: self.arena + ), + unexpectedBeforeRParen, + rightParen: rightParen, + arena: self.arena + ) + ), + arena: self.arena + ) + ) + continue + } + // Else, parse as a property. let (unexpectedPeriod, period, declName, generics) = parseDottedExpressionSuffix( previousNode: components.last?.raw ?? rootType?.raw ?? backslash.raw ) @@ -1128,7 +1168,6 @@ extension Parser { // No more postfix expressions. break } - return RawKeyPathExprSyntax( unexpectedBeforeBackslash, backslash: backslash, @@ -2017,6 +2056,37 @@ extension Parser { } extension Parser.Lookahead { + /// Check if the keypath method is applied, and not partially applied which should be parsed as a key path property. + mutating func isAppliedKeyPathMethod() -> Bool { + var lookahead = self.lookahead() + var hasLParen = false, hasRParen = false + + while true { + let token = lookahead.peek().rawTokenKind + if token == .endOfFile { + break + } + if token == .leftParen { + hasLParen = true + } + if token == .colon { + lookahead.consumeAnyToken() + // If there's a colon followed by a right parenthesis, it is + // a partial application and should be parsed as a property. + if lookahead.peek().rawTokenKind == .rightParen { + return false + } + } + if token == .rightParen { + hasRParen = true + } + lookahead.consumeAnyToken() + } + // If parentheses exist with no partial application pattern, + // parse as a key path method. + return hasLParen && hasRParen ? true : false + } + mutating func atStartOfLabelledTrailingClosure() -> Bool { // Fast path: the next two tokens must be a label and a colon. // But 'default:' is ambiguous with switch cases and we disallow it diff --git a/Tests/SwiftParserTest/ExpressionTests.swift b/Tests/SwiftParserTest/ExpressionTests.swift index 1038026eb26..93376cdb23f 100644 --- a/Tests/SwiftParserTest/ExpressionTests.swift +++ b/Tests/SwiftParserTest/ExpressionTests.swift @@ -256,6 +256,293 @@ final class ExpressionTests: ParserTestCase { ) } + func testKeyPathMethodAndInitializers() { + assertParse( + #"\Foo.method()"#, + substructure: KeyPathExprSyntax( + root: TypeSyntax("Foo"), + components: KeyPathComponentListSyntax([ + KeyPathComponentSyntax( + period: .periodToken(), + component: KeyPathComponentSyntax.Component( + KeyPathMethodComponentSyntax( + declName: DeclReferenceExprSyntax(baseName: .identifier("method")), + leftParen: .leftParenToken(), + arguments: LabeledExprListSyntax([]), + rightParen: .rightParenToken() + ) + ) + ) + ]) + ), + experimentalFeatures: .keypathWithMethodMembers + ) + + assertParse( + #"\Foo.method(10)"#, + substructure: KeyPathExprSyntax( + root: TypeSyntax("Foo"), + components: KeyPathComponentListSyntax([ + KeyPathComponentSyntax( + period: .periodToken(), + component: .init( + KeyPathMethodComponentSyntax( + declName: DeclReferenceExprSyntax(baseName: .identifier("method")), + leftParen: .leftParenToken(), + arguments: LabeledExprListSyntax([ + LabeledExprSyntax( + label: nil, + colon: nil, + expression: ExprSyntax("10") + ) + ]), + rightParen: .rightParenToken() + ) + ) + ) + ]) + ), + experimentalFeatures: .keypathWithMethodMembers + ) + + assertParse( + #"\Foo.method(arg: 10)"#, + substructure: KeyPathExprSyntax( + root: TypeSyntax("Foo"), + components: KeyPathComponentListSyntax([ + KeyPathComponentSyntax( + period: .periodToken(), + component: .init( + KeyPathMethodComponentSyntax( + declName: DeclReferenceExprSyntax(baseName: .identifier("method")), + leftParen: .leftParenToken(), + arguments: LabeledExprListSyntax([ + LabeledExprSyntax( + label: .identifier("arg"), + colon: .colonToken(), + expression: ExprSyntax("10") + ) + ]), + rightParen: .rightParenToken() + ) + ) + ) + ]) + ), + experimentalFeatures: .keypathWithMethodMembers + ) + + assertParse( + #"\Foo.method(_:)"#, + substructure: KeyPathExprSyntax( + root: TypeSyntax("Foo"), + components: KeyPathComponentListSyntax([ + KeyPathComponentSyntax( + period: .periodToken(), + component: .init( + KeyPathPropertyComponentSyntax( + declName: DeclReferenceExprSyntax( + baseName: .identifier("method"), + argumentNames: DeclNameArgumentsSyntax( + leftParen: .leftParenToken(), + arguments: [ + DeclNameArgumentSyntax(name: .wildcardToken(), colon: .colonToken()) + ], + rightParen: .rightParenToken() + ) + ) + ) + ) + ) + ]) + ), + experimentalFeatures: .keypathWithMethodMembers + ) + + assertParse( + #"\Foo.method(arg:)"#, + substructure: KeyPathExprSyntax( + root: TypeSyntax("Foo"), + components: KeyPathComponentListSyntax([ + KeyPathComponentSyntax( + period: .periodToken(), + component: .init( + KeyPathPropertyComponentSyntax( + declName: DeclReferenceExprSyntax( + baseName: .identifier("method"), + argumentNames: DeclNameArgumentsSyntax( + leftParen: .leftParenToken(), + arguments: [ + DeclNameArgumentSyntax(name: .identifier("arg"), colon: .colonToken()) + ], + rightParen: .rightParenToken() + ) + ) + ) + ) + ) + ]) + ), + experimentalFeatures: .keypathWithMethodMembers + ) + + assertParse( + #"\Foo.method().anotherMethod(arg: 10)"#, + substructure: KeyPathExprSyntax( + root: TypeSyntax("Foo"), + components: KeyPathComponentListSyntax([ + KeyPathComponentSyntax( + period: .periodToken(), + component: .init( + KeyPathMethodComponentSyntax( + declName: DeclReferenceExprSyntax(baseName: .identifier("method")), + leftParen: .leftParenToken(), + arguments: LabeledExprListSyntax([]), + rightParen: .rightParenToken() + ) + ) + ), + KeyPathComponentSyntax( + period: .periodToken(), + component: .init( + KeyPathMethodComponentSyntax( + declName: DeclReferenceExprSyntax(baseName: .identifier("anotherMethod")), + leftParen: .leftParenToken(), + arguments: LabeledExprListSyntax([ + LabeledExprSyntax( + label: .identifier("arg"), + colon: .colonToken(), + expression: ExprSyntax("10") + ) + ]), + rightParen: .rightParenToken() + ) + ) + ), + ]) + ), + experimentalFeatures: .keypathWithMethodMembers + ) + + assertParse( + #"\Foo.Type.init()"#, + substructure: KeyPathExprSyntax( + root: TypeSyntax( + MetatypeTypeSyntax(baseType: TypeSyntax("Foo"), metatypeSpecifier: .keyword(.Type)) + ), + components: KeyPathComponentListSyntax([ + KeyPathComponentSyntax( + period: .periodToken(), + component: KeyPathComponentSyntax.Component( + KeyPathMethodComponentSyntax( + declName: DeclReferenceExprSyntax(baseName: .keyword(.init("init")!)), + leftParen: .leftParenToken(), + arguments: LabeledExprListSyntax([]), + rightParen: .rightParenToken() + ) + ) + ) + ]) + ), + experimentalFeatures: .keypathWithMethodMembers + ) + + assertParse( + #""" + \Foo.method1️⃣(2️⃣ + """#, + diagnostics: [ + DiagnosticSpec( + locationMarker: "1️⃣", + message: "consecutive statements on a line must be separated by newline or ';'", + fixIts: ["insert newline", "insert ';'"] + ), + DiagnosticSpec( + locationMarker: "2️⃣", + message: "expected value and ')' to end tuple", + fixIts: ["insert value and ')'"] + ), + ], + fixedSource: #""" + \Foo.method + (<#expression#>) + """#, + experimentalFeatures: .keypathWithMethodMembers + ) + + assertParse( + #"\Foo.1️⃣()"#, + diagnostics: [ + DiagnosticSpec(message: "expected identifier in key path method component", fixIts: ["insert identifier"]) + ], + fixedSource: #"\Foo.<#identifier#>()"#, + experimentalFeatures: .keypathWithMethodMembers + ) + + assertParse( + #""" + S()[keyPath: \.i] = 1 + """#, + experimentalFeatures: .keypathWithMethodMembers + ) + + assertParse( + #""" + public let keyPath2FromLibB = \AStruct.Type.property + """#, + experimentalFeatures: .keypathWithMethodMembers + ) + + assertParse( + #""" + public let keyPath9FromLibB = \AStruct.Type.init(val: 2025) + """#, + experimentalFeatures: .keypathWithMethodMembers + ) + + assertParse( + #""" + _ = ([S]()).map(\.i) + """#, + experimentalFeatures: .keypathWithMethodMembers + ) + + assertParse( + #""" + let some = Some(keyPath: \Demo.here) + """#, + experimentalFeatures: .keypathWithMethodMembers + ) + + assertParse( + #""" + _ = ([S.Type]()).map(\.init) + """#, + experimentalFeatures: .keypathWithMethodMembers + ) + + assertParse( + #""" + \Lens>.obj.x + """#, + experimentalFeatures: .keypathWithMethodMembers + ) + + assertParse( + #""" + _ = \Lens.y + """#, + experimentalFeatures: .keypathWithMethodMembers + ) + + assertParse( + #""" + _ = f(\String?.!.count) + """#, + experimentalFeatures: .keypathWithMethodMembers + ) + } + func testKeyPathSubscript() { assertParse( #"\Foo.Type.[2]"#,