Skip to content

Commit 74ddcc4

Browse files
committed
Extract walkParentTree helper method. Adjust query methods.
1 parent db7fd99 commit 74ddcc4

File tree

5 files changed

+121
-61
lines changed

5 files changed

+121
-61
lines changed

Sources/SwiftLexicalScopes/LexicalScopes.swift

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,30 +13,29 @@
1313
import Foundation
1414
import SwiftSyntax
1515

16-
public enum LexicalScopes {
16+
extension SyntaxProtocol {
1717
/// Given syntax node position, returns all available labeled statements.
18-
@_spi(Compiler) @_spi(Testing) public static func lookupLabeledStmts(at syntax: SyntaxProtocol) -> [LabeledStmtSyntax] {
19-
guard let scope = syntax.scope else { return [] }
20-
return scope.lookupLabeledStmts(at: syntax)
18+
@_spi(Compiler) @_spi(Testing) public func lookupLabeledStmts() -> [LabeledStmtSyntax] {
19+
guard let scope else { return [] }
20+
return scope.lookupLabeledStmts(at: self)
2121
}
2222

2323
/// Given syntax node position, returns the current switch case and it's fallthrough destination.
24-
@_spi(Compiler) @_spi(Testing) public static func lookupFallthroughSourceAndDest(
25-
at syntax: SyntaxProtocol
26-
) -> (source: SwitchCaseSyntax?, destination: SwitchCaseSyntax?) {
27-
guard let scope = syntax.scope else { return (nil, nil) }
28-
return scope.lookupFallthroughSourceAndDestination(at: syntax)
24+
@_spi(Compiler) @_spi(Testing) public func lookupFallthroughSourceAndDest()
25+
-> (source: SwitchCaseSyntax?, destination: SwitchCaseSyntax?) {
26+
guard let scope else { return (nil, nil) }
27+
return scope.lookupFallthroughSourceAndDestination(at: self)
2928
}
3029

3130
/// Given syntax node position, returns the closest ancestor catch node.
32-
@_spi(Compiler) @_spi(Testing) public static func lookupCatchNode(at syntax: SyntaxProtocol) -> Syntax? {
33-
guard let scope = syntax.scope else { return nil }
34-
return scope.lookupCatchNode(at: Syntax(syntax))
31+
@_spi(Compiler) @_spi(Testing) public func lookupCatchNode() -> Syntax? {
32+
guard let scope else { return nil }
33+
return scope.lookupCatchNode(at: Syntax(self))
3534
}
3635

3736
/// Given name and syntax node position, return referenced declaration.
38-
@_spi(Compiler) @_spi(Testing) public static func lookupDeclarationFor(name: String, at syntax: SyntaxProtocol) -> Syntax? {
39-
guard let scope = syntax.scope else { return nil }
40-
return scope.getDeclarationFor(name: name, at: syntax)
37+
@_spi(Compiler) @_spi(Testing) public func lookupDeclarationsFor(name: String) -> [Syntax] {
38+
guard let scope else { return [] }
39+
return scope.getDeclarationsFor(name: name, at: self)
4140
}
4241
}

Sources/SwiftLexicalScopes/Scope.swift

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ protocol Scope {
3434
var sourceSyntax: SyntaxProtocol { get }
3535

3636
/// Returns the declaration `name` refers to at a particular syntax node location.
37-
func getDeclarationFor(name: String, at syntax: SyntaxProtocol) -> Syntax?
37+
func getDeclarationsFor(name: String, at syntax: SyntaxProtocol) -> [Syntax]
3838
}
3939

4040
extension Scope {
@@ -46,43 +46,33 @@ extension Scope {
4646
return syntax?.scope
4747
}
4848
}
49-
49+
5050
// MARK: - lookupLabeledStmts
51-
51+
5252
/// Given syntax node position, returns all available labeled statements.
5353
func lookupLabeledStmts(at syntax: SyntaxProtocol) -> [LabeledStmtSyntax] {
54-
return lookupLabeledStmtsHelper(at: syntax.parent)
55-
}
56-
57-
/// Helper method to recursively collect labeled statements from the syntax node's parents.
58-
private func lookupLabeledStmtsHelper(at syntax: Syntax?) -> [LabeledStmtSyntax] {
59-
guard let syntax, !syntax.is(MemberBlockSyntax.self) else { return [] }
60-
if let labeledStmtSyntax = syntax.as(LabeledStmtSyntax.self) {
61-
return [labeledStmtSyntax] + lookupLabeledStmtsHelper(at: labeledStmtSyntax.parent)
62-
} else {
63-
return lookupLabeledStmtsHelper(at: syntax.parent)
64-
}
54+
return walkParentTreeUpToFunctionBoundary(at: syntax.parent, collect: LabeledStmtSyntax.self)
6555
}
66-
56+
6757
// MARK: - lookupFallthroughSourceAndDest
68-
58+
6959
/// Given syntax node position, returns the current switch case and it's fallthrough destination.
7060
func lookupFallthroughSourceAndDestination(at syntax: SyntaxProtocol) -> (SwitchCaseSyntax?, SwitchCaseSyntax?) {
71-
guard let originalSwitchCase = syntax.ancestorOrSelf(mapping: { $0.as(SwitchCaseSyntax.self) }) else {
61+
guard let originalSwitchCase = walkParentTreeUpToFunctionBoundary(at: Syntax(syntax), collect: SwitchCaseSyntax.self) else {
7262
return (nil, nil)
7363
}
74-
64+
7565
let nextSwitchCase = lookupNextSwitchCase(at: originalSwitchCase)
76-
66+
7767
return (originalSwitchCase, nextSwitchCase)
7868
}
79-
69+
8070
/// Given a switch case, returns the case that follows according to the parent.
8171
private func lookupNextSwitchCase(at switchCaseSyntax: SwitchCaseSyntax) -> SwitchCaseSyntax? {
8272
guard let switchCaseListSyntax = switchCaseSyntax.parent?.as(SwitchCaseListSyntax.self) else { return nil }
83-
73+
8474
var visitedOriginalCase = false
85-
75+
8676
for child in switchCaseListSyntax.children(viewMode: .sourceAccurate) {
8777
if let thisCase = child.as(SwitchCaseSyntax.self) {
8878
if thisCase.id == switchCaseSyntax.id {
@@ -92,21 +82,21 @@ extension Scope {
9282
}
9383
}
9484
}
95-
85+
9686
return nil
9787
}
98-
88+
9989
// MARK: - lookupCatchNode
100-
90+
10191
/// Given syntax node position, returns the closest ancestor catch node.
10292
func lookupCatchNode(at syntax: Syntax) -> Syntax? {
10393
return lookupCatchNodeHelper(at: syntax, traversedCatchClause: false)
10494
}
105-
95+
10696
/// Given syntax node location, finds where an error could be caught. If set to `true`, `traverseCatchClause`lookup will skip the next do statement.
10797
private func lookupCatchNodeHelper(at syntax: Syntax?, traversedCatchClause: Bool) -> Syntax? {
10898
guard let syntax else { return nil }
109-
99+
110100
switch syntax.as(SyntaxEnum.self) {
111101
case .doStmt:
112102
if traversedCatchClause {
@@ -122,14 +112,47 @@ extension Scope {
122112
} else {
123113
return lookupCatchNodeHelper(at: syntax.parent, traversedCatchClause: traversedCatchClause)
124114
}
125-
case .functionDecl(let functionDecl):
126-
if functionDecl.signature.effectSpecifiers?.throwsClause != nil {
127-
return syntax
128-
} else {
129-
return lookupCatchNodeHelper(at: syntax.parent, traversedCatchClause: traversedCatchClause)
130-
}
115+
case .functionDecl, .accessorDecl, .initializerDecl:
116+
return syntax
131117
default:
132118
return lookupCatchNodeHelper(at: syntax.parent, traversedCatchClause: traversedCatchClause)
133119
}
134120
}
121+
122+
/// Callect the first syntax node matching the collection type up to a function boundary.
123+
func walkParentTreeUpToFunctionBoundary<T: SyntaxProtocol>(at syntax: Syntax?,
124+
collect: T.Type) -> T? {
125+
walkParentTreeUpToFunctionBoundary(at: syntax, collect: collect, stopWithFirstMatch: true).first
126+
}
127+
128+
/// Callect syntax nodes matching the collection type up to a function boundary.
129+
func walkParentTreeUpToFunctionBoundary<T: SyntaxProtocol>(at syntax: Syntax?,
130+
collect: T.Type,
131+
stopWithFirstMatch: Bool = false) -> [T] {
132+
walkParentTree(upTo: [MemberBlockSyntax.self,
133+
FunctionDeclSyntax.self,
134+
InitializerDeclSyntax.self,
135+
ClosureExprSyntax.self],
136+
at: syntax,
137+
collect: collect,
138+
stopWithFirstMatch: stopWithFirstMatch
139+
)
140+
}
141+
142+
/// Callect syntax nodes matching the collection type up until encountering one of the specified syntax nodes.
143+
func walkParentTree<T: SyntaxProtocol>(upTo stopAt: [SyntaxProtocol.Type],
144+
at syntax: Syntax?,
145+
collect: T.Type,
146+
stopWithFirstMatch: Bool = false) -> [T] {
147+
guard let syntax, !stopAt.contains(where: { syntax.is($0) }) else { return [] }
148+
if let matchedSyntax = syntax.as(T.self) {
149+
if stopWithFirstMatch {
150+
return [matchedSyntax]
151+
} else {
152+
return [matchedSyntax] + walkParentTree(upTo: stopAt, at: syntax.parent, collect: collect, stopWithFirstMatch: stopWithFirstMatch)
153+
}
154+
} else {
155+
return walkParentTree(upTo: stopAt, at: syntax.parent, collect: collect, stopWithFirstMatch: stopWithFirstMatch)
156+
}
157+
}
135158
}

Sources/SwiftLexicalScopes/ScopeConcreteImplementations.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ class FileScope: Scope {
2222
self.sourceSyntax = syntax
2323
}
2424

25-
func getDeclarationFor(name: String, at syntax: SyntaxProtocol) -> Syntax? {
25+
func getDeclarationsFor(name: String, at syntax: SyntaxProtocol) -> [Syntax] {
2626
// TODO: Implement the method
27-
return nil
27+
return []
2828
}
2929
}

Tests/SwiftLexicalScopesTest/Assertions.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func assertLexicalScopeQuery(
8888
/// - expected: A dictionary of markers with reference location as keys and expected declaration as values.
8989
func assertLexicalNameLookup(
9090
source: String,
91-
references: [String: String?]
91+
references: [String: [String]]
9292
) {
9393
assertLexicalScopeQuery(
9494
source: source,
@@ -98,8 +98,8 @@ func assertLexicalNameLookup(
9898
XCTFail("Couldn't find a token at \(argument)")
9999
return []
100100
}
101-
return [LexicalScopes.lookupDeclarationFor(name: name, at: argument)]
101+
return argument.lookupDeclarationsFor(name: name)
102102
},
103-
expected: references.mapValues({ [$0] })
103+
expected: references
104104
)
105105
}

Tests/SwiftLexicalScopesTest/SimpleQueryTests.swift

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ final class testSimpleQueries: XCTestCase {
2828
}
2929
""",
3030
methodUnderTest: { argument in
31-
LexicalScopes.lookupLabeledStmts(at: argument)
31+
argument.lookupLabeledStmts()
3232
},
3333
expected: ["3️⃣": ["2️⃣", "1️⃣"], "4️⃣": ["1️⃣"]]
3434
)
@@ -42,7 +42,7 @@ final class testSimpleQueries: XCTestCase {
4242
}
4343
""",
4444
methodUnderTest: { argument in
45-
LexicalScopes.lookupLabeledStmts(at: argument)
45+
argument.lookupLabeledStmts()
4646
},
4747
expected: ["1️⃣": []]
4848
)
@@ -63,7 +63,45 @@ final class testSimpleQueries: XCTestCase {
6363
}
6464
""",
6565
methodUnderTest: { argument in
66-
LexicalScopes.lookupLabeledStmts(at: argument)
66+
argument.lookupLabeledStmts()
67+
},
68+
expected: ["3️⃣": ["2️⃣"], "4️⃣": ["1️⃣"]]
69+
)
70+
}
71+
72+
func testLabeledStmtLookupClosureNestedWithinLoop() {
73+
assertLexicalScopeQuery(
74+
source: """
75+
1️⃣one: while true {
76+
var a = {
77+
2️⃣two: while true {
78+
3️⃣break
79+
}
80+
}
81+
4️⃣break
82+
}
83+
""",
84+
methodUnderTest: { argument in
85+
argument.lookupLabeledStmts()
86+
},
87+
expected: ["3️⃣": ["2️⃣"], "4️⃣": ["1️⃣"]]
88+
)
89+
}
90+
91+
func testLabeledStmtLookupFunctionNestedWithinLoop() {
92+
assertLexicalScopeQuery(
93+
source: """
94+
1️⃣one: while true {
95+
func foo() {
96+
2️⃣two: while true {
97+
3️⃣break
98+
}
99+
}
100+
4️⃣break
101+
}
102+
""",
103+
methodUnderTest: { argument in
104+
argument.lookupLabeledStmts()
67105
},
68106
expected: ["3️⃣": ["2️⃣"], "4️⃣": ["1️⃣"]]
69107
)
@@ -86,7 +124,7 @@ final class testSimpleQueries: XCTestCase {
86124
}
87125
""",
88126
methodUnderTest: { argument in
89-
let result = LexicalScopes.lookupFallthroughSourceAndDest(at: argument)
127+
let result = argument.lookupFallthroughSourceAndDest()
90128
return [result.source, result.destination]
91129
},
92130
expected: ["2️⃣": ["1️⃣", "3️⃣"], "4️⃣": ["3️⃣", "5️⃣"], "6️⃣": ["5️⃣", nil], "7️⃣": [nil, nil]]
@@ -105,14 +143,14 @@ final class testSimpleQueries: XCTestCase {
105143
}
106144
}
107145
108-
func bar() {
146+
8️⃣func bar() {
109147
throw 7️⃣f()
110148
}
111149
""",
112150
methodUnderTest: { argument in
113-
[LexicalScopes.lookupCatchNode(at: argument)]
151+
return [argument.lookupCatchNode()]
114152
},
115-
expected: ["3️⃣": ["2️⃣"], "5️⃣": ["4️⃣"], "6️⃣": ["1️⃣"], "7️⃣": [nil]]
153+
expected: ["3️⃣": ["2️⃣"], "5️⃣": ["4️⃣"], "6️⃣": ["1️⃣"], "7️⃣": ["8️⃣"]]
116154
)
117155
}
118156

@@ -133,7 +171,7 @@ final class testSimpleQueries: XCTestCase {
133171
}
134172
""",
135173
methodUnderTest: { argument in
136-
[LexicalScopes.lookupCatchNode(at: argument)]
174+
[argument.lookupCatchNode()]
137175
},
138176
expected: ["4️⃣": ["3️⃣"], "5️⃣": ["2️⃣"], "7️⃣": ["6️⃣"], "8️⃣": ["1️⃣"]]
139177
)

0 commit comments

Comments
 (0)