Skip to content

Commit 0dc27c9

Browse files
committed
Add documentation. Move lookupFallthroughSourceAndDest to FallThroughStmtSyntax.
1 parent 8dddaa3 commit 0dc27c9

File tree

3 files changed

+132
-79
lines changed

3 files changed

+132
-79
lines changed

Sources/SwiftLexicalLookup/SimpleLookupQueries.swift

Lines changed: 121 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,48 @@ import Foundation
1414
import SwiftSyntax
1515

1616
extension SyntaxProtocol {
17-
/// Given syntax node position, returns all available labeled statements.
18-
@_spi(Compiler) @_spi(Testing) public func lookupLabeledStmts() -> [LabeledStmtSyntax] {
17+
/// Returns all labeled statements available at a particular syntax node.
18+
///
19+
/// - Returns: Available labeled statements at a particular syntax node in the exact order they appear in the source code, starting with the innermost statement.
20+
///
21+
/// Example usage:
22+
/// ```swift
23+
/// one: while cond1 {
24+
/// func foo() {
25+
/// two: while cond2 {
26+
/// three: while cond3 {
27+
/// break // 1
28+
/// }
29+
/// break // 2
30+
/// }
31+
/// }
32+
/// break // 3
33+
/// }
34+
/// ```
35+
/// When calling this function at the first `break`, it returns `three` and `two` in this exact order. For the second `break`, it returns only `two`. The results don't include `one`, which is unavailable at both locations due to the encapsulating function body. For `break` numbered 3, the result is `one`, as it's outside the function body and within the labeled statement. The function returns an empty array when there are no available labeled statements.
36+
///
37+
@_spi(Experimental) public func lookupLabeledStmts() -> [LabeledStmtSyntax] {
1938
return lookupLabeledStmts(at: self)
2039
}
2140

22-
/// Given syntax node position, returns the current switch case and it's fallthrough destination.
23-
@_spi(Compiler) @_spi(Testing) public func lookupFallthroughSourceAndDest()
24-
-> (source: SwitchCaseSyntax?, destination: SwitchCaseSyntax?)
25-
{
26-
return lookupFallthroughSourceAndDestination(at: self)
27-
}
28-
29-
/// Given syntax node position, returns the closest ancestor catch node.
30-
@_spi(Compiler) @_spi(Testing) public func lookupCatchNode() -> Syntax? {
41+
/// Returns the catch node responsible for handling an error thrown at a particular syntax node.
42+
///
43+
/// - Returns: The catch node responsible for handling an error thrown at the lookup source node. This could be a `do` statement, `try?`, `try!`, `init`, `deinit`, accessors, closures, or function declarations.
44+
///
45+
/// Example usage:
46+
/// ```swift
47+
/// func x() {
48+
/// do {
49+
/// try foo()
50+
/// try? bar()
51+
/// } catch {
52+
/// throw error
53+
/// }
54+
/// }
55+
/// ```
56+
/// When calling this function on `foo`, it returns the `do` statement. Calling the function on `bar` results in `try?`. When used on `error`, the function returns the function declaration `x`. The function returns `nil` when there's no available catch node.
57+
///
58+
@_spi(Experimental) public func lookupCatchNode() -> Syntax? {
3159
return lookupCatchNodeHelper(at: Syntax(self), traversedCatchClause: false)
3260
}
3361

@@ -41,45 +69,6 @@ extension SyntaxProtocol {
4169
)
4270
}
4371

44-
// MARK: - lookupFallthroughSourceAndDest
45-
46-
/// Given syntax node position, returns the current switch case and it's fallthrough destination.
47-
private func lookupFallthroughSourceAndDestination(at syntax: SyntaxProtocol)
48-
-> (SwitchCaseSyntax?, SwitchCaseSyntax?)
49-
{
50-
guard
51-
let originalSwitchCase = walkParentTreeUpToFunctionBoundary(
52-
at: Syntax(syntax),
53-
collect: SwitchCaseSyntax.self
54-
)
55-
else {
56-
return (nil, nil)
57-
}
58-
59-
let nextSwitchCase = lookupNextSwitchCase(at: originalSwitchCase)
60-
61-
return (originalSwitchCase, nextSwitchCase)
62-
}
63-
64-
/// Given a switch case, returns the case that follows according to the parent.
65-
private func lookupNextSwitchCase(at switchCaseSyntax: SwitchCaseSyntax) -> SwitchCaseSyntax? {
66-
guard let switchCaseListSyntax = switchCaseSyntax.parent?.as(SwitchCaseListSyntax.self) else { return nil }
67-
68-
var visitedOriginalCase = false
69-
70-
for child in switchCaseListSyntax.children(viewMode: .sourceAccurate) {
71-
if let thisCase = child.as(SwitchCaseSyntax.self) {
72-
if thisCase.id == switchCaseSyntax.id {
73-
visitedOriginalCase = true
74-
} else if visitedOriginalCase {
75-
return thisCase
76-
}
77-
}
78-
}
79-
80-
return nil
81-
}
82-
8372
// MARK: - lookupCatchNode
8473

8574
/// Given syntax node location, finds where an error could be caught. If set to `true`, `traverseCatchClause`lookup will skip the next do statement.
@@ -116,15 +105,15 @@ extension SyntaxProtocol {
116105
// MARK: - walkParentTree helper methods
117106

118107
/// Callect the first syntax node matching the collection type up to a function boundary.
119-
private func walkParentTreeUpToFunctionBoundary<T: SyntaxProtocol>(
108+
fileprivate func walkParentTreeUpToFunctionBoundary<T: SyntaxProtocol>(
120109
at syntax: Syntax?,
121110
collect: T.Type
122111
) -> T? {
123112
walkParentTreeUpToFunctionBoundary(at: syntax, collect: collect, stopWithFirstMatch: true).first
124113
}
125114

126115
/// Callect syntax nodes matching the collection type up to a function boundary.
127-
private func walkParentTreeUpToFunctionBoundary<T: SyntaxProtocol>(
116+
fileprivate func walkParentTreeUpToFunctionBoundary<T: SyntaxProtocol>(
128117
at syntax: Syntax?,
129118
collect: T.Type,
130119
stopWithFirstMatch: Bool = false
@@ -138,7 +127,6 @@ extension SyntaxProtocol {
138127
AccessorDeclSyntax.self,
139128
ClosureExprSyntax.self,
140129
],
141-
at: syntax,
142130
collect: collect,
143131
stopWithFirstMatch: stopWithFirstMatch
144132
)
@@ -147,30 +135,91 @@ extension SyntaxProtocol {
147135
/// Callect syntax nodes matching the collection type up until encountering one of the specified syntax nodes.
148136
private func walkParentTree<T: SyntaxProtocol>(
149137
upTo stopAt: [SyntaxProtocol.Type],
150-
at syntax: Syntax?,
151138
collect: T.Type,
152139
stopWithFirstMatch: Bool = false
153140
) -> [T] {
154-
guard let syntax, !stopAt.contains(where: { syntax.is($0) }) else { return [] }
155-
if let matchedSyntax = syntax.as(T.self) {
156-
if stopWithFirstMatch {
157-
return [matchedSyntax]
158-
} else {
159-
return [matchedSyntax]
160-
+ walkParentTree(
161-
upTo: stopAt,
162-
at: syntax.parent,
163-
collect: collect,
164-
stopWithFirstMatch: stopWithFirstMatch
165-
)
141+
var matches: [T] = []
142+
var nextSyntax: Syntax? = Syntax(self)
143+
while let currentSyntax = nextSyntax {
144+
if stopAt.contains(where: { currentSyntax.is($0) }) {
145+
break
166146
}
167-
} else {
168-
return walkParentTree(
169-
upTo: stopAt,
170-
at: syntax.parent,
171-
collect: collect,
172-
stopWithFirstMatch: stopWithFirstMatch
147+
148+
if let matchedSyntax = currentSyntax.as(T.self) {
149+
matches.append(matchedSyntax)
150+
if stopWithFirstMatch {
151+
break
152+
}
153+
}
154+
155+
nextSyntax = currentSyntax.parent
156+
}
157+
158+
return matches
159+
}
160+
}
161+
162+
extension FallThroughStmtSyntax {
163+
/// Returns the source and destination of a `fallthrough`.
164+
///
165+
/// - Returns: `source` as the switch case that encapsulates the `fallthrough` keyword and `destination` as the switch case that the `fallthrough` directs to.
166+
///
167+
/// Example usage:
168+
/// ```swift
169+
/// switch value {
170+
/// case 2:
171+
/// doSomething()
172+
/// fallthrough
173+
/// case 1:
174+
/// doSomethingElse()
175+
/// default:
176+
/// break
177+
/// }
178+
/// ```
179+
/// When calling this function at the `fallthrough`, it returns `case 2` and `case 1` in this exact order. The `nil` results handle ill-formed code: there's no `source` if the `fallthrough` is outside of a case. There's no `destination` if there is no case or `default` after the source case.
180+
///
181+
@_spi(Experimental) public func lookupFallthroughSourceAndDest()
182+
-> (source: SwitchCaseSyntax?, destination: SwitchCaseSyntax?)
183+
{
184+
return lookupFallthroughSourceAndDestination(at: self)
185+
}
186+
187+
// MARK: - lookupFallthroughSourceAndDest
188+
189+
/// Given syntax node position, returns the current switch case and it's fallthrough destination.
190+
private func lookupFallthroughSourceAndDestination(at syntax: SyntaxProtocol)
191+
-> (SwitchCaseSyntax?, SwitchCaseSyntax?)
192+
{
193+
guard
194+
let originalSwitchCase = walkParentTreeUpToFunctionBoundary(
195+
at: Syntax(syntax),
196+
collect: SwitchCaseSyntax.self
173197
)
198+
else {
199+
return (nil, nil)
200+
}
201+
202+
let nextSwitchCase = lookupNextSwitchCase(at: originalSwitchCase)
203+
204+
return (originalSwitchCase, nextSwitchCase)
205+
}
206+
207+
/// Given a switch case, returns the case that follows according to the parent.
208+
private func lookupNextSwitchCase(at switchCaseSyntax: SwitchCaseSyntax) -> SwitchCaseSyntax? {
209+
guard let switchCaseListSyntax = switchCaseSyntax.parent?.as(SwitchCaseListSyntax.self) else { return nil }
210+
211+
var visitedOriginalCase = false
212+
213+
for child in switchCaseListSyntax.children(viewMode: .sourceAccurate) {
214+
if let thisCase = child.as(SwitchCaseSyntax.self) {
215+
if thisCase.id == switchCaseSyntax.id {
216+
visitedOriginalCase = true
217+
} else if visitedOriginalCase {
218+
return thisCase
219+
}
220+
}
174221
}
222+
223+
return nil
175224
}
176225
}

Tests/SwiftLexicalLookupTest/Assertions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import Foundation
14-
@_spi(Testing) import SwiftLexicalLookup
14+
@_spi(Experimental) import SwiftLexicalLookup
1515
import SwiftParser
1616
import SwiftSyntax
1717
import XCTest

Tests/SwiftLexicalLookupTest/SimpleQueryTests.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import Foundation
14-
@_spi(Testing) import SwiftLexicalLookup
14+
@_spi(Experimental) import SwiftLexicalLookup
15+
import SwiftSyntax
1516
import XCTest
1617

1718
final class testSimpleQueries: XCTestCase {
@@ -111,20 +112,23 @@ final class testSimpleQueries: XCTestCase {
111112
assertLexicalScopeQuery(
112113
source: """
113114
func foo() {
114-
7️⃣print(0)
115+
7️⃣fallthrough
115116
}
116117
117118
switch a {
118119
1️⃣case 1:
119-
2️⃣print(1)
120+
2️⃣fallthrough
120121
3️⃣case 2:
121-
4️⃣print(2)
122+
4️⃣fallthrough
122123
5️⃣default:
123-
6️⃣print(3)
124+
6️⃣fallthrough
124125
}
125126
""",
126127
methodUnderTest: { argument in
127-
let result = argument.lookupFallthroughSourceAndDest()
128+
guard let fallthroughStmt = argument.ancestorOrSelf(mapping: { $0.as(FallThroughStmtSyntax.self) }) else {
129+
return []
130+
}
131+
let result = fallthroughStmt.lookupFallthroughSourceAndDest()
128132
return [result.source, result.destination]
129133
},
130134
expected: ["2️⃣": ["1️⃣", "3️⃣"], "4️⃣": ["3️⃣", "5️⃣"], "6️⃣": ["5️⃣", nil], "7️⃣": [nil, nil]]

0 commit comments

Comments
 (0)