Skip to content

Commit 6c40203

Browse files
committed
Add new result structure using LookupResult enum array.
1 parent 3450029 commit 6c40203

File tree

7 files changed

+293
-72
lines changed

7 files changed

+293
-72
lines changed

Sources/SwiftLexicalLookup/LookupName.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import SwiftSyntax
3939
syntax.name.text
4040
}
4141
}
42-
42+
4343
/// Point, after which the name is available in scope.
4444
/// If set to `nil`, the name is available at any point in scope.
4545
var accessibleAfter: AbsolutePosition? {
@@ -97,14 +97,15 @@ import SwiftSyntax
9797
}
9898

9999
/// Extracts name introduced by `IdentifiableSyntax` node.
100-
private static func handle(identifiable: IdentifiableSyntax, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] {
100+
private static func handle(identifiable: IdentifiableSyntax, accessibleAfter: AbsolutePosition? = nil) -> [LookupName]
101+
{
101102
if identifiable.identifier.text != "_" {
102103
return [.identifier(identifiable, accessibleAfter: accessibleAfter)]
103104
} else {
104105
return []
105106
}
106107
}
107-
108+
108109
/// Extracts name introduced by `NamedDeclSyntax` node.
109110
private static func handle(namedDecl: NamedDeclSyntax, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] {
110111
[.declaration(namedDecl, accessibleAfter: accessibleAfter)]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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 SwiftSyntax
14+
15+
/// Represents resul
16+
@_spi(Experimental) public enum LookupResult {
17+
/// Scope and the names that matched lookup.
18+
case fromScope(ScopeSyntax, withNames: [LookupName])
19+
20+
/// Associated scope.
21+
@_spi(Experimental) public var scope: ScopeSyntax? {
22+
switch self {
23+
case .fromScope(let scopeSyntax, _):
24+
scopeSyntax
25+
}
26+
}
27+
28+
/// Names that matched lookup.
29+
@_spi(Experimental) public var names: [LookupName] {
30+
switch self {
31+
case .fromScope(_, let names):
32+
names
33+
}
34+
}
35+
}

Sources/SwiftLexicalLookup/ScopeImplementations.swift

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,28 +23,28 @@ extension SyntaxProtocol {
2323
}
2424
}
2525

26-
extension SourceFileSyntax: ScopeSyntax {
27-
var introducedNames: [LookupName] {
26+
@_spi(Experimental) extension SourceFileSyntax: ScopeSyntax {
27+
public var introducedNames: [LookupName] {
2828
[]
2929
}
3030
}
3131

32-
extension CodeBlockSyntax: ScopeSyntax {
33-
var introducedNames: [LookupName] {
32+
@_spi(Experimental) extension CodeBlockSyntax: ScopeSyntax {
33+
public var introducedNames: [LookupName] {
3434
statements.flatMap { codeBlockItem in
35-
LookupName.getNames(from: codeBlockItem.item, accessibleAfter: codeBlockItem.item.endPosition)
35+
LookupName.getNames(from: codeBlockItem.item, accessibleAfter: codeBlockItem.endPosition)
3636
}
3737
}
3838
}
3939

40-
extension ForStmtSyntax: ScopeSyntax {
41-
var introducedNames: [LookupName] {
40+
@_spi(Experimental) extension ForStmtSyntax: ScopeSyntax {
41+
public var introducedNames: [LookupName] {
4242
LookupName.getNames(from: pattern)
4343
}
4444
}
4545

46-
extension ClosureExprSyntax: ScopeSyntax {
47-
var introducedNames: [LookupName] {
46+
@_spi(Experimental) extension ClosureExprSyntax: ScopeSyntax {
47+
public var introducedNames: [LookupName] {
4848
signature?.parameterClause?.children(viewMode: .sourceAccurate).flatMap { parameter in
4949
if let parameterList = parameter.as(ClosureParameterListSyntax.self) {
5050
parameterList.children(viewMode: .sourceAccurate).flatMap { parameter in
@@ -57,16 +57,16 @@ extension ClosureExprSyntax: ScopeSyntax {
5757
}
5858
}
5959

60-
extension WhileStmtSyntax: ScopeSyntax {
61-
var introducedNames: [LookupName] {
60+
@_spi(Experimental) extension WhileStmtSyntax: ScopeSyntax {
61+
public var introducedNames: [LookupName] {
6262
conditions.flatMap { element in
6363
LookupName.getNames(from: element.condition)
6464
}
6565
}
6666
}
6767

68-
extension IfExprSyntax: ScopeSyntax {
69-
var parentScope: ScopeSyntax? {
68+
@_spi(Experimental) extension IfExprSyntax: ScopeSyntax {
69+
public var parentScope: ScopeSyntax? {
7070
getParent(for: self.parent, previousIfElse: self.elseKeyword == nil)
7171
}
7272

@@ -85,13 +85,13 @@ extension IfExprSyntax: ScopeSyntax {
8585
}
8686
}
8787

88-
var introducedNames: [LookupName] {
88+
public var introducedNames: [LookupName] {
8989
conditions.flatMap { element in
90-
LookupName.getNames(from: element.condition, accessibleAfter: element.condition.endPosition)
90+
LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition)
9191
}
9292
}
9393

94-
func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] {
94+
public func lookup(for name: String?, at syntax: SyntaxProtocol) -> [LookupResult] {
9595
if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position {
9696
parentScope?.lookup(for: name, at: syntax) ?? []
9797
} else {
@@ -100,8 +100,8 @@ extension IfExprSyntax: ScopeSyntax {
100100
}
101101
}
102102

103-
extension MemberBlockSyntax: ScopeSyntax {
104-
var introducedNames: [LookupName] {
103+
@_spi(Experimental) extension MemberBlockSyntax: ScopeSyntax {
104+
public var introducedNames: [LookupName] {
105105
members.flatMap { member in
106106
LookupName.getNames(from: member.decl)
107107
}

Sources/SwiftLexicalLookup/ScopeSyntax.swift

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import SwiftSyntax
1515
extension SyntaxProtocol {
1616
/// Returns all names that `for` refers to at this syntax node.
1717
///
18-
/// - Returns: An array of names referred to by `for` at this syntax node,
19-
/// ordered by visibility. The order is from the innermost to the outermost
20-
/// scope, and within each scope, names are ordered by their introduction
18+
/// - Returns: An array of `LookupResult` for name `for` at this syntax node,
19+
/// ordered by visibility. If set to `nil`, returns all available names ordered by visibility.
20+
/// The order is from the innermost to the outermost scope,
21+
/// and within each result, names are ordered by their introduction
2122
/// in the source code.
2223
///
2324
/// Example usage:
@@ -41,40 +42,50 @@ extension SyntaxProtocol {
4142
/// declaration, followed by the first function name, and then the second function name,
4243
/// in this exact order. The constant declaration within the function body is omitted
4344
/// due to the ordering rules that prioritize visibility within the function body.
44-
@_spi(Experimental) public func lookup(for name: String) -> [LookupName] {
45+
@_spi(Experimental) public func lookup(for name: String?) -> [LookupResult] {
4546
scope?.lookup(for: name, at: self) ?? []
4647
}
4748
}
4849

49-
protocol ScopeSyntax: SyntaxProtocol {
50+
@_spi(Experimental) public protocol ScopeSyntax: SyntaxProtocol {
5051
/// Parent of this scope, or `nil` if it is the root.
5152
var parentScope: ScopeSyntax? { get }
5253
/// Names found in this scope. Ordered from first to last introduced.
5354
var introducedNames: [LookupName] { get }
5455
/// Finds all declarations `name` refers to. `at` specifies the node lookup was triggered with.
55-
func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName]
56+
/// If `name` set to `nil`, returns all available names at the given node.
57+
func lookup(for name: String?, at syntax: SyntaxProtocol) -> [LookupResult]
5658
}
5759

58-
extension ScopeSyntax {
59-
var parentScope: ScopeSyntax? {
60+
@_spi(Experimental) extension ScopeSyntax {
61+
public var parentScope: ScopeSyntax? {
6062
self.parent?.scope
6163
}
62-
63-
/// Returns all names introduced in this scope that `name` refers to and
64-
/// is accessible at given syntax node then passes lookup to the parent.
65-
func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] {
64+
65+
/// Returns `LookupResult` of all names introduced in this scope that `name`
66+
/// refers to and is accessible at given syntax node then passes lookup to the parent.
67+
/// If `name` set to `nil`, returns all available names at the given node.
68+
public func lookup(for name: String?, at syntax: SyntaxProtocol) -> [LookupResult] {
6669
defaultLookupImplementation(for: name, at: syntax)
6770
}
6871

69-
/// Returns all names introduced in this scope that `name` refers to and
70-
/// is accessible at given syntax node then passes lookup to the parent.
71-
func defaultLookupImplementation(
72-
for name: String,
72+
/// Returns `LookupResult` of all names introduced in this scope that `name`
73+
/// refers to and is accessible at given syntax node then passes lookup to the parent.
74+
/// If `name` set to `nil`, returns all available names at the given node.
75+
public func defaultLookupImplementation(
76+
for name: String?,
7377
at syntax: SyntaxProtocol
74-
) -> [LookupName] {
75-
introducedNames
78+
) -> [LookupResult] {
79+
let filteredNames =
80+
introducedNames
7681
.filter { introducedName in
77-
introducedName.isAccessible(at: syntax) && introducedName.refersTo(name)
78-
} + (parentScope?.lookup(for: name, at: syntax) ?? [])
82+
introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!))
83+
}
84+
85+
if filteredNames.isEmpty {
86+
return parentScope?.lookup(for: name, at: syntax) ?? []
87+
} else {
88+
return [.fromScope(self, withNames: filteredNames)] + (parentScope?.lookup(for: name, at: syntax) ?? [])
89+
}
7990
}
8091
}

Tests/SwiftLexicalLookupTest/Assertions.swift

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,31 @@ enum MarkerExpectation {
5353
}
5454
}
5555

56+
/// Used to define
57+
enum ResultExpectation {
58+
case fromScope(ScopeSyntax.Type, expectedNames: [String])
59+
60+
var expectedNames: [String] {
61+
switch self {
62+
case .fromScope(_, let expectedNames):
63+
expectedNames
64+
}
65+
}
66+
67+
static func == (lhs: ResultExpectation, rhs: LookupResult) -> Bool {
68+
switch (lhs, rhs) {
69+
case (.fromScope, .fromScope):
70+
return true
71+
}
72+
}
73+
}
74+
5675
/// `methodUnderTest` is called with the token at every position marker in the keys of `expected`.
5776
/// It then asserts that the positions of the syntax nodes returned by `methodUnderTest` are the values in `expected`.
5877
/// It also checks whether result types match rules specified in `expectedResultTypes`.
5978
func assertLexicalScopeQuery(
6079
source: String,
61-
methodUnderTest: (TokenSyntax) -> ([SyntaxProtocol?]),
80+
methodUnderTest: (String, TokenSyntax) -> ([SyntaxProtocol?]),
6281
expected: [String: [String?]],
6382
expectedResultTypes: MarkerExpectation = .none
6483
) {
@@ -80,7 +99,7 @@ func assertLexicalScopeQuery(
8099
}
81100

82101
// Execute the tested method
83-
let result = methodUnderTest(testArgument)
102+
let result = methodUnderTest(marker, testArgument)
84103

85104
// Extract the expected results for the test argument
86105
let expectedPositions: [AbsolutePosition?] = expectedMarkers.map { expectedMarker in
@@ -135,17 +154,48 @@ func assertLexicalScopeQuery(
135154
/// It also checks whether result types match rules specified in `expectedResultTypes`.
136155
func assertLexicalNameLookup(
137156
source: String,
138-
references: [String: [String]],
139-
expectedResultTypes: MarkerExpectation = .none
157+
references: [String: [ResultExpectation]],
158+
expectedResultTypes: MarkerExpectation = .none,
159+
useNilAsTheParameter: Bool = false
140160
) {
141161
assertLexicalScopeQuery(
142162
source: source,
143-
methodUnderTest: { argument in
144-
return argument.lookup(for: argument.text).map { lookUpResult in
145-
lookUpResult.syntax
163+
methodUnderTest: { marker, argument in
164+
let result = argument.lookup(for: useNilAsTheParameter ? nil : argument.text)
165+
166+
guard let expectedValues = references[marker] else {
167+
XCTFail("For marker \(marker), couldn't find expectation")
168+
return []
169+
}
170+
171+
for (actual, expected) in zip(result, expectedValues) {
172+
XCTAssert(
173+
expected == actual,
174+
"For marker \(marker), expected actual result \(actual) doesn't match expected \(expected)"
175+
)
176+
177+
switch actual {
178+
case .fromScope(let scope, withNames: _):
179+
if case .fromScope(let expectedType, expectedNames: _) = expected {
180+
XCTAssert(
181+
scope.syntaxNodeType == expectedType,
182+
"For marker \(marker), scope result type of \(scope.syntaxNodeType) doesn't match expected \(expectedType)"
183+
)
184+
}
185+
}
186+
}
187+
188+
return result.flatMap { lookUpResult in
189+
lookUpResult.names.map { lookupName in
190+
lookupName.syntax
191+
}
192+
}
193+
},
194+
expected: references.mapValues { expectations in
195+
expectations.flatMap { expectation in
196+
expectation.expectedNames
146197
}
147198
},
148-
expected: references,
149199
expectedResultTypes: expectedResultTypes
150200
)
151201
}

0 commit comments

Comments
 (0)