Skip to content

Commit 9cc77ac

Browse files
authored
Merge pull request #2696 from MAJKFL/main
[SwiftLexicalScopes][GSoC] Add simple scope queries and initial name lookup API structure
2 parents 050f057 + 67e2608 commit 9cc77ac

File tree

9 files changed

+527
-27
lines changed

9 files changed

+527
-27
lines changed

.spi.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ builder:
1212
- SwiftCompilerPlugin
1313
- SwiftDiagnostics
1414
- SwiftIDEUtils
15+
- SwiftLexicalLookup
1516
- SwiftOperators
1617
- SwiftParser
1718
- SwiftParserDiagnostics

Package.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ let package = Package(
1717
.library(name: "SwiftCompilerPlugin", targets: ["SwiftCompilerPlugin"]),
1818
.library(name: "SwiftDiagnostics", targets: ["SwiftDiagnostics"]),
1919
.library(name: "SwiftIDEUtils", targets: ["SwiftIDEUtils"]),
20+
.library(name: "SwiftLexicalLookup", targets: ["SwiftLexicalLookup"]),
2021
.library(name: "SwiftOperators", targets: ["SwiftOperators"]),
2122
.library(name: "SwiftParser", targets: ["SwiftParser"]),
2223
.library(name: "SwiftParserDiagnostics", targets: ["SwiftParserDiagnostics"]),
@@ -137,6 +138,18 @@ let package = Package(
137138
dependencies: ["_SwiftSyntaxTestSupport", "SwiftIDEUtils", "SwiftParser", "SwiftSyntax"]
138139
),
139140

141+
// MARK: SwiftLexicalLookup
142+
143+
.target(
144+
name: "SwiftLexicalLookup",
145+
dependencies: ["SwiftSyntax"]
146+
),
147+
148+
.testTarget(
149+
name: "SwiftLexicalLookupTest",
150+
dependencies: ["_SwiftSyntaxTestSupport", "SwiftLexicalLookup"]
151+
),
152+
140153
// MARK: SwiftLibraryPluginProvider
141154

142155
.target(

Release Notes/601.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
- Description: Allows retrieving the represented literal value when valid.
77
- Issue: https://github.com/apple/swift-syntax/issues/405
88
- Pull Request: https://github.com/apple/swift-syntax/pull/2605
9+
10+
- `SyntaxProtocol` now has a method `ancestorOrSelf`.
11+
- Description: Returns the node or the first ancestor that satisfies `condition`.
12+
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/2696
913

1014
## API Behavior Changes
1115

Sources/SwiftBasicFormat/BasicFormat.swift

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -676,17 +676,3 @@ fileprivate extension TokenSyntax {
676676
}
677677
}
678678
}
679-
680-
fileprivate extension SyntaxProtocol {
681-
/// Returns this node or the first ancestor that satisfies `condition`.
682-
func ancestorOrSelf<T>(mapping map: (Syntax) -> T?) -> T? {
683-
var walk: Syntax? = Syntax(self)
684-
while let unwrappedParent = walk {
685-
if let mapped = map(unwrappedParent) {
686-
return mapped
687-
}
688-
walk = unwrappedParent.parent
689-
}
690-
return nil
691-
}
692-
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
import SwiftSyntax
15+
16+
extension SyntaxProtocol {
17+
/// Returns all labeled statements available at a particular syntax node.
18+
///
19+
/// - Returns: Available labeled statements at a particular syntax node
20+
/// in the exact order they appear in the source code, starting with the innermost statement.
21+
///
22+
/// Example usage:
23+
/// ```swift
24+
/// one: while cond1 {
25+
/// func foo() {
26+
/// two: while cond2 {
27+
/// three: while cond3 {
28+
/// break // 1
29+
/// }
30+
/// break // 2
31+
/// }
32+
/// }
33+
/// break // 3
34+
/// }
35+
/// ```
36+
/// When calling this function at the first `break`, it returns `three` and `two` in this exact order.
37+
/// For the second `break`, it returns only `two`.
38+
/// The results don't include `one`, which is unavailable at both locations due to the encapsulating function body.
39+
/// For `break` numbered 3, the result is `one`, as it's outside the function body and within the labeled statement.
40+
/// The function returns an empty array when there are no available labeled statements.
41+
@_spi(Experimental) public func lookupLabeledStmts() -> [LabeledStmtSyntax] {
42+
collectNodesOfTypeUpToFunctionBoundary(LabeledStmtSyntax.self)
43+
}
44+
45+
/// Returns the catch node responsible for handling an error thrown at a particular syntax node.
46+
///
47+
/// - Returns: The catch node responsible for handling an error thrown at the lookup source node.
48+
/// This could be a `do` statement, `try?`, `try!`, `init`, `deinit`, accessors, closures, or function declarations.
49+
///
50+
/// Example usage:
51+
/// ```swift
52+
/// func x() {
53+
/// do {
54+
/// try foo()
55+
/// try? bar()
56+
/// } catch {
57+
/// throw error
58+
/// }
59+
/// }
60+
/// ```
61+
/// When calling this function on `foo`, it returns the `do` statement.
62+
/// Calling the function on `bar` results in `try?`.
63+
/// When used on `error`, the function returns the function declaration `x`.
64+
/// The function returns `nil` when there's no available catch node.
65+
@_spi(Experimental) public func lookupCatchNode() -> Syntax? {
66+
lookupCatchNodeHelper(traversedCatchClause: false)
67+
}
68+
69+
// MARK: - lookupCatchNode
70+
71+
/// Given syntax node location, finds where an error could be caught. If `traverseCatchClause` is set to `true` lookup will skip the next do statement.
72+
private func lookupCatchNodeHelper(traversedCatchClause: Bool) -> Syntax? {
73+
guard let parent else { return nil }
74+
75+
switch parent.as(SyntaxEnum.self) {
76+
case .doStmt:
77+
if traversedCatchClause {
78+
return parent.lookupCatchNodeHelper(traversedCatchClause: false)
79+
} else {
80+
return parent
81+
}
82+
case .catchClause:
83+
return parent.lookupCatchNodeHelper(traversedCatchClause: true)
84+
case .tryExpr(let tryExpr):
85+
if tryExpr.questionOrExclamationMark != nil {
86+
return parent
87+
} else {
88+
return parent.lookupCatchNodeHelper(traversedCatchClause: traversedCatchClause)
89+
}
90+
case .functionDecl, .accessorDecl, .initializerDecl, .deinitializerDecl, .closureExpr:
91+
return parent
92+
case .exprList(let exprList):
93+
if let tryExpr = exprList.first?.as(TryExprSyntax.self), tryExpr.questionOrExclamationMark != nil {
94+
return Syntax(tryExpr)
95+
}
96+
return parent.lookupCatchNodeHelper(traversedCatchClause: traversedCatchClause)
97+
default:
98+
return parent.lookupCatchNodeHelper(traversedCatchClause: traversedCatchClause)
99+
}
100+
}
101+
102+
// MARK: - walkParentTree helper methods
103+
104+
/// Returns the innermost node of the specified type up to a function boundary.
105+
fileprivate func innermostNodeOfTypeUpToFunctionBoundary<T: SyntaxProtocol>(
106+
_ type: T.Type
107+
) -> T? {
108+
collectNodesOfTypeUpToFunctionBoundary(type, stopWithFirstMatch: true).first
109+
}
110+
111+
/// Collect syntax nodes matching the collection type up until encountering one of the specified syntax nodes. The nodes in the array are inside out, with the innermost node being the first.
112+
fileprivate func collectNodesOfTypeUpToFunctionBoundary<T: SyntaxProtocol>(
113+
_ type: T.Type,
114+
stopWithFirstMatch: Bool = false
115+
) -> [T] {
116+
collectNodes(
117+
ofType: type,
118+
upTo: [
119+
MemberBlockSyntax.self,
120+
FunctionDeclSyntax.self,
121+
InitializerDeclSyntax.self,
122+
DeinitializerDeclSyntax.self,
123+
AccessorDeclSyntax.self,
124+
ClosureExprSyntax.self,
125+
SubscriptDeclSyntax.self,
126+
],
127+
stopWithFirstMatch: stopWithFirstMatch
128+
)
129+
}
130+
131+
/// Callect syntax nodes matching the collection type up until encountering one of the specified syntax nodes.
132+
private func collectNodes<T: SyntaxProtocol>(
133+
ofType type: T.Type,
134+
upTo stopAt: [SyntaxProtocol.Type],
135+
stopWithFirstMatch: Bool = false
136+
) -> [T] {
137+
var matches: [T] = []
138+
var nextSyntax: Syntax? = Syntax(self)
139+
while let currentSyntax = nextSyntax {
140+
if stopAt.contains(where: { currentSyntax.is($0) }) {
141+
break
142+
}
143+
144+
if let matchedSyntax = currentSyntax.as(T.self) {
145+
matches.append(matchedSyntax)
146+
if stopWithFirstMatch {
147+
break
148+
}
149+
}
150+
151+
nextSyntax = currentSyntax.parent
152+
}
153+
154+
return matches
155+
}
156+
}
157+
158+
extension FallThroughStmtSyntax {
159+
/// Returns the source and destination of a `fallthrough`.
160+
///
161+
/// - Returns: `source` as the switch case that encapsulates the `fallthrough` keyword and
162+
/// `destination` as the switch case that the `fallthrough` directs to.
163+
///
164+
/// Example usage:
165+
/// ```swift
166+
/// switch value {
167+
/// case 2:
168+
/// doSomething()
169+
/// fallthrough
170+
/// case 1:
171+
/// doSomethingElse()
172+
/// default:
173+
/// break
174+
/// }
175+
/// ```
176+
/// When calling this function at the `fallthrough`, it returns `case 2` and `case 1` in this exact order.
177+
/// The `nil` results handle ill-formed code: there's no `source` if the `fallthrough` is outside of a case.
178+
/// There's no `destination` if there is no case or `default` after the source case.
179+
@_spi(Experimental) public func lookupFallthroughSourceAndDestintation()
180+
-> (source: SwitchCaseSyntax?, destination: SwitchCaseSyntax?)
181+
{
182+
guard
183+
let originalSwitchCase = innermostNodeOfTypeUpToFunctionBoundary(
184+
SwitchCaseSyntax.self
185+
)
186+
else {
187+
return (nil, nil)
188+
}
189+
190+
let nextSwitchCase = lookupNextSwitchCase(at: originalSwitchCase)
191+
192+
return (originalSwitchCase, nextSwitchCase)
193+
}
194+
195+
/// Given a switch case, returns the case that follows according to the parent.
196+
private func lookupNextSwitchCase(at switchCaseSyntax: SwitchCaseSyntax) -> SwitchCaseSyntax? {
197+
guard let switchCaseListSyntax = switchCaseSyntax.parent?.as(SwitchCaseListSyntax.self) else { return nil }
198+
199+
var visitedOriginalCase = false
200+
201+
for child in switchCaseListSyntax.children(viewMode: .sourceAccurate) {
202+
if let thisCase = child.as(SwitchCaseSyntax.self) {
203+
if thisCase.id == switchCaseSyntax.id {
204+
visitedOriginalCase = true
205+
} else if visitedOriginalCase {
206+
return thisCase
207+
}
208+
}
209+
}
210+
211+
return nil
212+
}
213+
}

Sources/SwiftParserDiagnostics/SyntaxExtensions.swift

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -119,18 +119,6 @@ extension SyntaxProtocol {
119119
}
120120
}
121121

122-
/// Returns this node or the first ancestor that satisfies `condition`.
123-
func ancestorOrSelf<T>(mapping map: (Syntax) -> T?) -> T? {
124-
var walk: Syntax? = Syntax(self)
125-
while let unwrappedParent = walk {
126-
if let mapped = map(unwrappedParent) {
127-
return mapped
128-
}
129-
walk = unwrappedParent.parent
130-
}
131-
return nil
132-
}
133-
134122
/// Returns `true` if the next token's leading trivia should be made leading trivia
135123
/// of this mode, when it is switched from being missing to present.
136124
var shouldBeInsertedAfterNextTokenTrivia: Bool {

Sources/SwiftSyntax/SyntaxProtocol.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ extension SyntaxProtocol {
186186
}
187187
}
188188

189-
// MARK: Children / parent
189+
// MARK: Children / parent / ancestor
190190

191191
extension SyntaxProtocol {
192192
/// A sequence over the children of this node.
@@ -238,6 +238,18 @@ extension SyntaxProtocol {
238238
public var previousToken: TokenSyntax? {
239239
return self.previousToken(viewMode: .sourceAccurate)
240240
}
241+
242+
/// Returns this node or the first ancestor that satisfies `condition`.
243+
public func ancestorOrSelf<T>(mapping map: (Syntax) -> T?) -> T? {
244+
var walk: Syntax? = Syntax(self)
245+
while let unwrappedParent = walk {
246+
if let mapped = map(unwrappedParent) {
247+
return mapped
248+
}
249+
walk = unwrappedParent.parent
250+
}
251+
return nil
252+
}
241253
}
242254

243255
// MARK: Accessing tokens

0 commit comments

Comments
 (0)