Skip to content

Commit 1cb4a9f

Browse files
committed
Add file and guard scopes. Add closure capture and if case support. Add a way to customize lookup behavior through LookupConfig/
1 parent 85be4f1 commit 1cb4a9f

File tree

9 files changed

+368
-21
lines changed

9 files changed

+368
-21
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
/// Specifies how names should be introduced at the file scope.
16+
@_spi(Experimental) public enum FileScopeNameIntroductionStrategy: LookupConfig {
17+
/// Default behavior. Names introduced sequentially like in member block
18+
/// scope up to the first non-declaration after and including which,
19+
/// the declarations are treated like in code block scope.
20+
case memberBlockUpToLastDecl
21+
/// File scope behaves like member block scope.
22+
case memberBlock
23+
/// File scope behaves like code block scope.
24+
case codeBlock
25+
}

Sources/SwiftLexicalLookup/IdentifiableSyntax.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,19 @@ public protocol IdentifiableSyntax: SyntaxProtocol {
2020
extension IdentifierPatternSyntax: IdentifiableSyntax {}
2121

2222
extension ClosureParameterSyntax: IdentifiableSyntax {
23-
@_spi(Experimental) public var identifier: SwiftSyntax.TokenSyntax {
23+
@_spi(Experimental) public var identifier: TokenSyntax {
2424
secondName ?? firstName
2525
}
2626
}
2727

2828
extension ClosureShorthandParameterSyntax: IdentifiableSyntax {
29-
@_spi(Experimental) public var identifier: SwiftSyntax.TokenSyntax {
29+
@_spi(Experimental) public var identifier: TokenSyntax {
3030
name
3131
}
3232
}
33+
34+
extension ClosureCaptureSyntax: IdentifiableSyntax {
35+
@_spi(Experimental) public var identifier: TokenSyntax {
36+
expression.as(DeclReferenceExprSyntax.self)!.baseName
37+
}
38+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
15+
/// Used to customize lookup behavior.
16+
@_spi(Experimental) public protocol LookupConfig {}

Sources/SwiftLexicalLookup/LookupName.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ import SwiftSyntax
8585
getNames(from: patternExpr.pattern, accessibleAfter: accessibleAfter)
8686
case .optionalBindingCondition(let optionalBinding):
8787
getNames(from: optionalBinding.pattern, accessibleAfter: accessibleAfter)
88+
case .matchingPatternCondition(let matchingPatternCondition):
89+
getNames(from: matchingPatternCondition.pattern, accessibleAfter: accessibleAfter)
90+
case .functionCallExpr(let functionCallExpr):
91+
functionCallExpr.arguments.flatMap { argument in
92+
getNames(from: argument.expression, accessibleAfter: accessibleAfter)
93+
}
94+
case .guardStmt(let guardStmt):
95+
guardStmt.conditions.flatMap { cond in
96+
getNames(from: cond.condition, accessibleAfter: cond.endPosition)
97+
}
8898
default:
8999
if let namedDecl = Syntax(syntax).asProtocol(SyntaxProtocol.self) as? NamedDeclSyntax {
90100
handle(namedDecl: namedDecl, accessibleAfter: accessibleAfter)

Sources/SwiftLexicalLookup/LookupResult.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@ import SwiftSyntax
1616
@_spi(Experimental) public enum LookupResult {
1717
/// Scope and the names that matched lookup.
1818
case fromScope(ScopeSyntax, withNames: [LookupName])
19+
/// File scope, names that matched lookup and name introduction
20+
/// strategy used for the lookup.
21+
case fromFileScope(SourceFileSyntax, withNames: [LookupName], nameIntroductionStrategy: FileScopeNameIntroductionStrategy)
1922

2023
/// Associated scope.
2124
@_spi(Experimental) public var scope: ScopeSyntax? {
2225
switch self {
2326
case .fromScope(let scopeSyntax, _):
2427
scopeSyntax
28+
case .fromFileScope(let fileScopeSyntax, withNames: _, nameIntroductionStrategy: _):
29+
fileScopeSyntax
2530
}
2631
}
2732

@@ -30,6 +35,8 @@ import SwiftSyntax
3035
switch self {
3136
case .fromScope(_, let names):
3237
names
38+
case .fromFileScope(_, withNames: let names, nameIntroductionStrategy: _):
39+
names
3340
}
3441
}
3542
}

Sources/SwiftLexicalLookup/ScopeImplementations.swift

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,50 @@ extension SyntaxProtocol {
2525

2626
@_spi(Experimental) extension SourceFileSyntax: ScopeSyntax {
2727
public var introducedNames: [LookupName] {
28-
[]
28+
introducedNames(using: .memberBlockUpToLastDecl)
29+
}
30+
31+
public func introducedNames(using nameIntroductionStrategy: FileScopeNameIntroductionStrategy) -> [LookupName] {
32+
switch nameIntroductionStrategy {
33+
case .memberBlockUpToLastDecl:
34+
var encounteredNonDeclaration = false
35+
36+
return statements.flatMap { codeBlockItem in
37+
let item = codeBlockItem.item
38+
39+
if encounteredNonDeclaration {
40+
return LookupName.getNames(from: item, accessibleAfter: codeBlockItem.endPosition)
41+
} else {
42+
if item.is(DeclSyntax.self) || item.is(VariableDeclSyntax.self) {
43+
return LookupName.getNames(from: item)
44+
} else {
45+
encounteredNonDeclaration = true
46+
return LookupName.getNames(from: item, accessibleAfter: codeBlockItem.endPosition)
47+
}
48+
}
49+
}
50+
case .codeBlock:
51+
return statements.flatMap { codeBlockItem in
52+
LookupName.getNames(from: codeBlockItem.item, accessibleAfter: codeBlockItem.endPosition)
53+
}
54+
case .memberBlock:
55+
return statements.flatMap { codeBlockItem in
56+
LookupName.getNames(from: codeBlockItem.item)
57+
}
58+
}
59+
}
60+
61+
public func lookup(for name: String?, at syntax: SyntaxProtocol, with config: [LookupConfig]) -> [LookupResult] {
62+
let nameIntroductionStrategy = config.first {
63+
$0 is FileScopeNameIntroductionStrategy
64+
} as? FileScopeNameIntroductionStrategy ?? .memberBlockUpToLastDecl
65+
66+
let names = introducedNames(using: nameIntroductionStrategy)
67+
.filter { introducedName in
68+
introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!))
69+
}
70+
71+
return [.fromFileScope(self, withNames: names, nameIntroductionStrategy: nameIntroductionStrategy)]
2972
}
3073
}
3174

@@ -45,7 +88,17 @@ extension SyntaxProtocol {
4588

4689
@_spi(Experimental) extension ClosureExprSyntax: ScopeSyntax {
4790
public var introducedNames: [LookupName] {
48-
signature?.parameterClause?.children(viewMode: .sourceAccurate).flatMap { parameter in
91+
let captureNames = signature?.capture?.children(viewMode: .sourceAccurate).flatMap { child in
92+
if let captureList = child.as(ClosureCaptureListSyntax.self) {
93+
captureList.children(viewMode: .sourceAccurate).flatMap { capture in
94+
LookupName.getNames(from: capture)
95+
}
96+
} else {
97+
LookupName.getNames(from: child)
98+
}
99+
} ?? []
100+
101+
let parameterNames = signature?.parameterClause?.children(viewMode: .sourceAccurate).flatMap { parameter in
49102
if let parameterList = parameter.as(ClosureParameterListSyntax.self) {
50103
parameterList.children(viewMode: .sourceAccurate).flatMap { parameter in
51104
LookupName.getNames(from: parameter)
@@ -54,6 +107,8 @@ extension SyntaxProtocol {
54107
LookupName.getNames(from: parameter)
55108
}
56109
} ?? []
110+
111+
return captureNames + parameterNames
57112
}
58113
}
59114

@@ -91,11 +146,11 @@ extension SyntaxProtocol {
91146
}
92147
}
93148

94-
public func lookup(for name: String?, at syntax: SyntaxProtocol) -> [LookupResult] {
149+
public func lookup(for name: String?, at syntax: SyntaxProtocol, with config: [LookupConfig]) -> [LookupResult] {
95150
if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position {
96-
parentScope?.lookup(for: name, at: syntax) ?? []
151+
lookupInParent(for: name, at: syntax, with: config)
97152
} else {
98-
defaultLookupImplementation(for: name, at: syntax)
153+
defaultLookupImplementation(for: name, at: syntax, with: config)
99154
}
100155
}
101156
}
@@ -107,3 +162,17 @@ extension SyntaxProtocol {
107162
}
108163
}
109164
}
165+
166+
@_spi(Experimental) extension GuardStmtSyntax: ScopeSyntax {
167+
public var introducedNames: [LookupName] {
168+
[]
169+
}
170+
171+
public func lookup(for name: String?, at syntax: SyntaxProtocol, with config: [LookupConfig]) -> [LookupResult] {
172+
if body.position <= syntax.position && body.endPosition >= syntax.position {
173+
lookupInParent(for: name, at: self, with: config)
174+
} else {
175+
defaultLookupImplementation(for: name, at: syntax, with: config)
176+
}
177+
}
178+
}

Sources/SwiftLexicalLookup/ScopeSyntax.swift

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import SwiftSyntax
1414

1515
extension SyntaxProtocol {
1616
/// Returns all names that `for` refers to at this syntax node.
17+
/// Optional configuration can be passed as `with` to customize the lookup behavior.
1718
///
1819
/// - Returns: An array of `LookupResult` for name `for` at this syntax node,
1920
/// ordered by visibility. If set to `nil`, returns all available names ordered by visibility.
@@ -42,8 +43,8 @@ extension SyntaxProtocol {
4243
/// declaration, followed by the first function name, and then the second function name,
4344
/// in this exact order. The constant declaration within the function body is omitted
4445
/// due to the ordering rules that prioritize visibility within the function body.
45-
@_spi(Experimental) public func lookup(for name: String?) -> [LookupResult] {
46-
scope?.lookup(for: name, at: self) ?? []
46+
@_spi(Experimental) public func lookup(for name: String?, with config: [LookupConfig] = []) -> [LookupResult] {
47+
scope?.lookup(for: name, at: self, with: config) ?? []
4748
}
4849
}
4950

@@ -54,7 +55,7 @@ extension SyntaxProtocol {
5455
var introducedNames: [LookupName] { get }
5556
/// Finds all declarations `name` refers to. `at` specifies the node lookup was triggered with.
5657
/// If `name` set to `nil`, returns all available names at the given node.
57-
func lookup(for name: String?, at syntax: SyntaxProtocol) -> [LookupResult]
58+
func lookup(for name: String?, at syntax: SyntaxProtocol, with config: [LookupConfig]) -> [LookupResult]
5859
}
5960

6061
@_spi(Experimental) extension ScopeSyntax {
@@ -65,16 +66,17 @@ extension SyntaxProtocol {
6566
/// Returns `LookupResult` of all names introduced in this scope that `name`
6667
/// refers to and is accessible at given syntax node then passes lookup to the parent.
6768
/// If `name` set to `nil`, returns all available names at the given node.
68-
public func lookup(for name: String?, at syntax: SyntaxProtocol) -> [LookupResult] {
69-
defaultLookupImplementation(for: name, at: syntax)
69+
public func lookup(for name: String?, at syntax: SyntaxProtocol, with config: [LookupConfig]) -> [LookupResult] {
70+
defaultLookupImplementation(for: name, at: syntax, with: config)
7071
}
7172

7273
/// Returns `LookupResult` of all names introduced in this scope that `name`
7374
/// refers to and is accessible at given syntax node then passes lookup to the parent.
7475
/// If `name` set to `nil`, returns all available names at the given node.
75-
public func defaultLookupImplementation(
76+
func defaultLookupImplementation(
7677
for name: String?,
77-
at syntax: SyntaxProtocol
78+
at syntax: SyntaxProtocol,
79+
with config: [LookupConfig]
7880
) -> [LookupResult] {
7981
let filteredNames =
8082
introducedNames
@@ -83,9 +85,18 @@ extension SyntaxProtocol {
8385
}
8486

8587
if filteredNames.isEmpty {
86-
return parentScope?.lookup(for: name, at: syntax) ?? []
88+
return lookupInParent(for: name, at: syntax, with: config)
8789
} else {
88-
return [.fromScope(self, withNames: filteredNames)] + (parentScope?.lookup(for: name, at: syntax) ?? [])
90+
return [.fromScope(self, withNames: filteredNames)] + lookupInParent(for: name, at: syntax, with: config)
8991
}
9092
}
93+
94+
/// Looks up in parent scope.
95+
func lookupInParent(
96+
for name: String?,
97+
at syntax: SyntaxProtocol,
98+
with config: [LookupConfig]
99+
) -> [LookupResult] {
100+
parentScope?.lookup(for: name, at: syntax, with: config) ?? []
101+
}
91102
}

Tests/SwiftLexicalLookupTest/Assertions.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,14 @@ enum MarkerExpectation {
5656
/// Used to define
5757
enum ResultExpectation {
5858
case fromScope(ScopeSyntax.Type, expectedNames: [String])
59+
case fromFileScope(expectedNames: [String], nameIntroductionStrategy: FileScopeNameIntroductionStrategy)
5960

6061
var expectedNames: [String] {
6162
switch self {
6263
case .fromScope(_, let expectedNames):
6364
expectedNames
65+
case .fromFileScope(expectedNames: let expectedNames, nameIntroductionStrategy: _):
66+
expectedNames
6467
}
6568
}
6669
}
@@ -149,12 +152,13 @@ func assertLexicalNameLookup(
149152
source: String,
150153
references: [String: [ResultExpectation]],
151154
expectedResultTypes: MarkerExpectation = .none,
152-
useNilAsTheParameter: Bool = false
155+
useNilAsTheParameter: Bool = false,
156+
config: [LookupConfig] = []
153157
) {
154158
assertLexicalScopeQuery(
155159
source: source,
156160
methodUnderTest: { marker, argument in
157-
let result = argument.lookup(for: useNilAsTheParameter ? nil : argument.text)
161+
let result = argument.lookup(for: useNilAsTheParameter ? nil : argument.text, with: config)
158162

159163
guard let expectedValues = references[marker] else {
160164
XCTFail("For marker \(marker), couldn't find result expectation")
@@ -168,6 +172,13 @@ func assertLexicalNameLookup(
168172
scope.syntaxNodeType == expectedType,
169173
"For marker \(marker), scope result type of \(scope.syntaxNodeType) doesn't match expected \(expectedType)"
170174
)
175+
case (.fromFileScope(_, withNames: _, nameIntroductionStrategy: let nameIntroductionStrategy), .fromFileScope(expectedNames: _, nameIntroductionStrategy: let expectedNameIntroductionStrategy)):
176+
XCTAssert(
177+
nameIntroductionStrategy == expectedNameIntroductionStrategy,
178+
"For marker \(marker), actual file scope name introduction strategy \(nameIntroductionStrategy) doesn't match expected \(expectedNameIntroductionStrategy)"
179+
)
180+
default:
181+
XCTFail("For marker \(marker), result actual result kind \(actual) doesn't match expected \(expected)")
171182
}
172183
}
173184

0 commit comments

Comments
 (0)