Skip to content

Commit 9f2fab0

Browse files
committed
Add SwiftLexicalScopes library
1 parent 9a49bb7 commit 9f2fab0

File tree

6 files changed

+462
-0
lines changed

6 files changed

+462
-0
lines changed

Package.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ let package = Package(
2828
.library(name: "SwiftSyntaxMacros", targets: ["SwiftSyntaxMacros"]),
2929
.library(name: "SwiftSyntaxMacroExpansion", targets: ["SwiftSyntaxMacroExpansion"]),
3030
.library(name: "SwiftSyntaxMacrosTestSupport", targets: ["SwiftSyntaxMacrosTestSupport"]),
31+
.library(name: "SwiftLexicalScopes", targets: ["SwiftLexicalScopes"]),
3132
.library(
3233
name: "SwiftSyntaxMacrosGenericTestSupport",
3334
targets: ["SwiftSyntaxMacrosGenericTestSupport"]
@@ -243,6 +244,18 @@ let package = Package(
243244
]
244245
),
245246

247+
// MARK: SwiftLexicalScopes
248+
249+
.target(
250+
name: "SwiftLexicalScopes",
251+
dependencies: ["SwiftSyntax"]
252+
),
253+
254+
.testTarget(
255+
name: "SwiftLexicalScopesTest",
256+
dependencies: ["_SwiftSyntaxTestSupport", "SwiftLexicalScopes"]
257+
),
258+
246259
// MARK: SwiftSyntaxMacrosGenericTestSupport
247260

248261
.target(
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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+
public struct LexicalScopes {
17+
18+
/// Given syntax node position, returns all available labeled statements.
19+
public static func lookupLabeledStmts(at syntax: SyntaxProtocol)
20+
-> [LabeledStmtSyntax]
21+
{
22+
guard let scope = syntax.scope else { return [] }
23+
return scope.lookupLabeledStmts(at: syntax)
24+
}
25+
26+
/// Given syntax node position, returns the current switch case and it's fallthrough destination.
27+
public static func lookupFallthroughSourceAndDest(at syntax: SyntaxProtocol) -> (
28+
SwitchCaseSyntax?, SwitchCaseSyntax?
29+
) {
30+
guard let scope = syntax.scope else { return (nil, nil) }
31+
return scope.lookupFallthroughSourceAndDest(at: syntax)
32+
}
33+
34+
/// Given syntax node position, returns the closest ancestor catch node.
35+
public static func lookupCatchNode(at syntax: SyntaxProtocol) -> Syntax? {
36+
guard let scope = syntax.scope else { return nil }
37+
return scope.lookupCatchNode(at: Syntax(syntax))
38+
}
39+
40+
/// Given name and syntax node position, return referenced declaration.
41+
public static func lookupDeclarationFor(name: String, at syntax: SyntaxProtocol) -> Syntax? {
42+
guard let scope = syntax.scope else { return nil }
43+
return scope.getDeclarationFor(name: name, at: syntax)
44+
}
45+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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+
/// Scope at the syntax node. Could be inherited from parent or introduced at the node.
18+
var scope: Scope? {
19+
switch self.syntaxNodeType {
20+
case is SourceFileSyntax.Type:
21+
FileScope(syntax: self)
22+
default:
23+
parent?.scope
24+
}
25+
}
26+
}
27+
28+
protocol Scope {
29+
var parent: Scope? { get }
30+
31+
var sourceSyntax: SyntaxProtocol { get }
32+
33+
func getDeclarationFor(name: String, at syntax: SyntaxProtocol) -> Syntax?
34+
}
35+
36+
extension Scope {
37+
var parent: Scope? {
38+
getParentScope(forSyntax: sourceSyntax)
39+
}
40+
41+
private func getParentScope(forSyntax syntax: SyntaxProtocol?) -> Scope? {
42+
if let lookedUpScope = syntax?.scope, lookedUpScope.sourceSyntax.id == syntax?.id {
43+
return getParentScope(forSyntax: sourceSyntax.parent)
44+
} else {
45+
return syntax?.scope
46+
}
47+
}
48+
49+
// MARK: - lookupLabeledStmts
50+
51+
func lookupLabeledStmts(at syntax: SyntaxProtocol) -> [LabeledStmtSyntax] {
52+
var result = [LabeledStmtSyntax]()
53+
lookupLabeledStmtsHelper(at: syntax.parent, accumulator: &result)
54+
return result
55+
}
56+
57+
private func lookupLabeledStmtsHelper(at syntax: Syntax?, accumulator: inout [LabeledStmtSyntax]) {
58+
guard let syntax, !syntax.is(MemberBlockSyntax.self) else { return }
59+
if let labeledStmtSyntax = syntax.as(LabeledStmtSyntax.self) {
60+
accumulator.append(labeledStmtSyntax)
61+
lookupLabeledStmtsHelper(at: labeledStmtSyntax.parent, accumulator: &accumulator)
62+
} else {
63+
lookupLabeledStmtsHelper(at: syntax.parent, accumulator: &accumulator)
64+
}
65+
}
66+
67+
// MARK: - lookupFallthroughSourceAndDest
68+
69+
func lookupFallthroughSourceAndDest(at syntax: SyntaxProtocol) -> (SwitchCaseSyntax?, SwitchCaseSyntax?) {
70+
guard let originalSwitchCase = lookupClosestSwitchCaseSyntaxAncestor(at: syntax) else { return (nil, nil) }
71+
72+
let nextSwitchCase = lookupNextSwitchCase(at: originalSwitchCase)
73+
74+
return (originalSwitchCase, nextSwitchCase)
75+
}
76+
77+
private func lookupClosestSwitchCaseSyntaxAncestor(at syntax: SyntaxProtocol?) -> SwitchCaseSyntax? {
78+
guard let syntax else { return nil }
79+
80+
if let switchCaseSyntax = syntax.as(SwitchCaseSyntax.self) {
81+
return switchCaseSyntax
82+
} else {
83+
return lookupClosestSwitchCaseSyntaxAncestor(at: syntax.parent)
84+
}
85+
}
86+
87+
private func lookupNextSwitchCase(at switchCaseSyntax: SwitchCaseSyntax) -> SwitchCaseSyntax? {
88+
guard let switchCaseListSyntax = switchCaseSyntax.parent?.as(SwitchCaseListSyntax.self) else { return nil }
89+
90+
var visitedOriginalCase = false
91+
92+
for child in switchCaseListSyntax.children(viewMode: .sourceAccurate) {
93+
if let thisCase = child.as(SwitchCaseSyntax.self) {
94+
if thisCase.id == switchCaseSyntax.id {
95+
visitedOriginalCase = true
96+
} else if visitedOriginalCase {
97+
return thisCase
98+
}
99+
}
100+
}
101+
102+
return nil
103+
}
104+
105+
// MARK: - lookupCatchNode
106+
107+
func lookupCatchNode(at syntax: Syntax) -> Syntax? {
108+
return lookupCatchNodeHelper(at: syntax, traversedCatchClause: false)
109+
}
110+
111+
private func lookupCatchNodeHelper(at syntax: Syntax?, traversedCatchClause: Bool) -> Syntax? {
112+
guard let syntax else { return nil }
113+
114+
switch syntax.syntaxNodeType {
115+
case is DoStmtSyntax.Type:
116+
if traversedCatchClause {
117+
return lookupCatchNodeHelper(at: syntax.parent, traversedCatchClause: false)
118+
} else {
119+
return syntax
120+
}
121+
case is CatchClauseSyntax.Type:
122+
return lookupCatchNodeHelper(at: syntax.parent, traversedCatchClause: true)
123+
case is TryExprSyntax.Type:
124+
if syntax.as(TryExprSyntax.self)!.questionOrExclamationMark != nil {
125+
return syntax
126+
} else {
127+
return lookupCatchNodeHelper(at: syntax.parent, traversedCatchClause: traversedCatchClause)
128+
}
129+
case is FunctionDeclSyntax.Type:
130+
if syntax.as(FunctionDeclSyntax.self)!.signature.effectSpecifiers?.throwsClause != nil {
131+
return syntax
132+
} else {
133+
return lookupCatchNodeHelper(at: syntax.parent, traversedCatchClause: traversedCatchClause)
134+
}
135+
default:
136+
return lookupCatchNodeHelper(at: syntax.parent, traversedCatchClause: traversedCatchClause)
137+
}
138+
}
139+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
class FileScope: Scope {
17+
var parent: Scope? = nil
18+
19+
var sourceSyntax: SyntaxProtocol
20+
21+
init(syntax: SyntaxProtocol) {
22+
self.sourceSyntax = syntax
23+
}
24+
25+
func getDeclarationFor(name: String, at syntax: SyntaxProtocol) -> Syntax? {
26+
// TODO: Implement the method
27+
return nil
28+
}
29+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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 SwiftLexicalScopes
15+
import SwiftParser
16+
import SwiftSyntax
17+
import XCTest
18+
import _SwiftSyntaxTestSupport
19+
20+
/// Parse `source` and check if the method passed as `methodUnderTest` produces the same results as indicated in `expected`.
21+
///
22+
/// The `methodUnderTest` provides test inputs taken from the `expected` dictionary. The closure should return result produced by the tested method as an array with the same ordering.
23+
///
24+
/// - Parameters:
25+
/// - methodUnderTest: Closure with the tested method. Provides test argument from `expected` to the tested function. Should return method result as an array.
26+
/// - expected: A dictionary with parameter markers as keys and expected results as marker arrays ordered as returned by the test method.
27+
func assertLexicalScopeQuery(
28+
source: String,
29+
methodUnderTest: (SyntaxProtocol) -> ([SyntaxProtocol?]),
30+
expected: [String: [String?]]
31+
) {
32+
// Extract markers
33+
let (markerDict, textWithoutMarkers) = extractMarkers(source)
34+
35+
// Parse the test source
36+
var parser = Parser(textWithoutMarkers)
37+
let sourceFileSyntax = SourceFileSyntax.parse(from: &parser)
38+
39+
// Iterate through the expected results
40+
for (marker, expectedMarkers) in expected {
41+
// Extract a test argument
42+
guard let position = markerDict[marker],
43+
let testArgument = sourceFileSyntax.token(at: AbsolutePosition(utf8Offset: position))
44+
else {
45+
XCTFail("Could not find token at location \(marker)")
46+
continue
47+
}
48+
49+
// Execute the tested method
50+
let result = methodUnderTest(testArgument)
51+
52+
// Extract the expected results for the test argument
53+
let expectedValues: [SyntaxProtocol?] = expectedMarkers.map { expectedMarker in
54+
guard let expectedMarker else { return nil }
55+
56+
guard let expectedPosition = markerDict[expectedMarker],
57+
let expectedToken = sourceFileSyntax.token(at: AbsolutePosition(utf8Offset: expectedPosition))
58+
else {
59+
XCTFail("Could not find token at location \(marker)")
60+
return nil
61+
}
62+
63+
return expectedToken
64+
}
65+
66+
// Compare number of actual results to the number of expected results
67+
if result.count != expectedValues.count {
68+
XCTFail(
69+
"For marker \(marker), actual number of elements: \(result.count) doesn't match the expected: \(expectedValues.count)"
70+
)
71+
}
72+
73+
// Assert validity of the output
74+
for (actual, expected) in zip(result, expectedValues) {
75+
if actual == nil && expected == nil { continue }
76+
77+
XCTAssert(
78+
actual?.firstToken(viewMode: .sourceAccurate)?.id == expected?.id,
79+
"For marker \(marker), actual result: \(actual?.firstToken(viewMode: .sourceAccurate) ?? "nil") doesn't match expected value: \(expected?.firstToken(viewMode: .sourceAccurate) ?? "nil")"
80+
)
81+
}
82+
}
83+
}
84+
85+
/// Parse `source` and check if the lexical name lookup matches results passed as `expected`.
86+
///
87+
/// - Parameters:
88+
/// - expected: A dictionary of markers with reference location as keys and expected declaration as values.
89+
func assertLexicalNameLookup(
90+
source: String,
91+
references: [String: String?]
92+
) {
93+
assertLexicalScopeQuery(
94+
source: source,
95+
methodUnderTest: { argument in
96+
// Extract reference name and use it for lookup
97+
guard let name = argument.firstToken(viewMode: .sourceAccurate)?.text else {
98+
XCTFail("Couldn't find a token at \(argument)")
99+
return []
100+
}
101+
return [LexicalScopes.lookupDeclarationFor(name: name, at: argument)]
102+
},
103+
expected: references.mapValues({ [$0] })
104+
)
105+
}

0 commit comments

Comments
 (0)