Skip to content

Commit

Permalink
Parse method and initializer keypaths.
Browse files Browse the repository at this point in the history
  • Loading branch information
amritpan committed Feb 3, 2025
1 parent d8eaf70 commit f49e95c
Show file tree
Hide file tree
Showing 2 changed files with 359 additions and 2 deletions.
74 changes: 72 additions & 2 deletions Sources/SwiftParser/Expressions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -1128,7 +1168,6 @@ extension Parser {
// No more postfix expressions.
break
}

return RawKeyPathExprSyntax(
unexpectedBeforeBackslash,
backslash: backslash,
Expand Down Expand Up @@ -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
Expand Down
287 changes: 287 additions & 0 deletions Tests/SwiftParserTest/ExpressionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Lens<Point>>.obj.x
"""#,
experimentalFeatures: .keypathWithMethodMembers
)

assertParse(
#"""
_ = \Lens<Point>.y
"""#,
experimentalFeatures: .keypathWithMethodMembers
)

assertParse(
#"""
_ = f(\String?.!.count)
"""#,
experimentalFeatures: .keypathWithMethodMembers
)
}

func testKeyPathSubscript() {
assertParse(
#"\Foo.Type.[2]"#,
Expand Down

0 comments on commit f49e95c

Please sign in to comment.