Skip to content

Commit 99b5294

Browse files
committed
Add guard support to file scope.
1 parent a68f00a commit 99b5294

File tree

6 files changed

+212
-51
lines changed

6 files changed

+212
-51
lines changed

Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,17 @@ import Foundation
1515
@_spi(Experimental) public struct LookupConfig {
1616
/// Specifies behaviour of file scope.
1717
/// `memberBlockUpToLastDecl` by default.
18-
public var fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl
18+
public var fileScopeHandling: FileScopeHandlingConfig
1919

20-
public init(fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl) {
20+
/// Specifies scopes that introduce names to their parent and
21+
/// should be skipped during lookup in sequential scopes.
22+
public var ignoreChildrenToParentIntroductionsFrom: [IntroducingToParentScopeSyntax]
23+
24+
public init(
25+
fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl,
26+
ignoreChildrenToParentIntroductionsFrom: [IntroducingToParentScopeSyntax] = []
27+
) {
2128
self.fileScopeHandling = fileScopeHandling
29+
self.ignoreChildrenToParentIntroductionsFrom = ignoreChildrenToParentIntroductionsFrom
2230
}
2331
}

Sources/SwiftLexicalLookup/IntroducingToParentScopeSyntax.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ import SwiftSyntax
1616
func introducedToParent(
1717
for name: String?,
1818
at syntax: SyntaxProtocol,
19-
with configDict: LookupConfigDictionary
19+
with config: LookupConfig
2020
) -> [LookupResult]
2121
}

Sources/SwiftLexicalLookup/ScopeImplementations.swift

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

26-
@_spi(Experimental) extension SourceFileSyntax: ScopeSyntax {
26+
@_spi(Experimental) extension SourceFileSyntax: SequentialScopeSyntax {
2727
/// All names introduced in the file scope
2828
/// according to the default strategy: `memberBlockUpToLastDecl`.
2929
public var introducedNames: [LookupName] {
@@ -105,16 +105,58 @@ extension SyntaxProtocol {
105105
at syntax: SyntaxProtocol,
106106
with config: LookupConfig
107107
) -> [LookupResult] {
108-
let names = introducedNames(using: config.fileScopeHandling)
109-
.filter { introducedName in
110-
does(name: name, referTo: introducedName, at: syntax)
108+
switch config.fileScopeHandling {
109+
case .codeBlock:
110+
return sequentialLookup(
111+
in: statements,
112+
for: name,
113+
at: syntax,
114+
with: config,
115+
createResultsForThisScopeWith: { .fromFileScope(self, withNames: $0) }
116+
)
117+
case .memberBlock:
118+
let names = introducedNames(using: .memberBlock)
119+
.filter { lookupName in
120+
does(name: name, referTo: lookupName, at: syntax)
121+
}
122+
123+
return names.isEmpty ? [] : [.fromFileScope(self, withNames: names)]
124+
case .memberBlockUpToLastDecl:
125+
var members = [LookupName]()
126+
var sequentialItems = [CodeBlockItemSyntax]()
127+
var encounteredNonDeclaration = false
128+
129+
for codeBlockItem in statements {
130+
let item = codeBlockItem.item
131+
132+
if encounteredNonDeclaration {
133+
sequentialItems.append(codeBlockItem)
134+
} else {
135+
if item.is(DeclSyntax.self) || item.is(VariableDeclSyntax.self) {
136+
let foundNames = LookupName.getNames(from: item)
137+
138+
members.append(contentsOf: foundNames.filter { does(name: name, referTo: $0, at: syntax) })
139+
} else {
140+
encounteredNonDeclaration = true
141+
sequentialItems.append(codeBlockItem)
142+
}
143+
}
111144
}
112145

113-
return names.isEmpty ? [] : [.fromFileScope(self, withNames: names)]
146+
let sequentialNames = sequentialLookup(
147+
in: sequentialItems,
148+
for: name,
149+
at: syntax,
150+
with: config,
151+
createResultsForThisScopeWith: { .fromFileScope(self, withNames: $0) }
152+
)
153+
154+
return (members.isEmpty ? [] : [.fromFileScope(self, withNames: members)]) + sequentialNames
155+
}
114156
}
115157
}
116158

117-
@_spi(Experimental) extension CodeBlockSyntax: ScopeSyntax {
159+
@_spi(Experimental) extension CodeBlockSyntax: SequentialScopeSyntax {
118160
/// Names introduced in the code block scope
119161
/// accessible after their declaration.
120162
public var introducedNames: [LookupName] {
@@ -123,43 +165,14 @@ extension SyntaxProtocol {
123165
}
124166
}
125167

126-
public func lookup(
127-
for name: String?,
128-
at syntax: SyntaxProtocol,
129-
with configDict: LookupConfigDictionary
130-
) -> [LookupResult] {
131-
var result = [LookupResult]()
132-
var currentChunk = [LookupName]()
133-
134-
for codeBlockItem in statements {
135-
if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self)
136-
as? IntroducingToParentScopeSyntax
137-
{
138-
if !currentChunk.isEmpty {
139-
result.append(.fromScope(self, withNames: currentChunk))
140-
currentChunk = []
141-
}
142-
143-
result.append(contentsOf: introducingToParentScope.introducedToParent(for: name, at: syntax, with: configDict))
144-
} else {
145-
currentChunk.append(
146-
contentsOf:
147-
LookupName.getNames(
148-
from: codeBlockItem.item,
149-
accessibleAfter: codeBlockItem.endPosition
150-
).filter { introducedName in
151-
does(name: name, referTo: introducedName, at: syntax)
152-
}
153-
)
154-
}
155-
}
156-
157-
if !currentChunk.isEmpty {
158-
result.append(.fromScope(self, withNames: currentChunk))
159-
currentChunk = []
160-
}
161-
162-
return result.reversed() + lookupInParent(for: name, at: syntax, with: configDict)
168+
public func lookup(for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig) -> [LookupResult] {
169+
sequentialLookup(
170+
in: statements,
171+
for: name,
172+
at: syntax,
173+
with: config,
174+
createResultsForThisScopeWith: { .fromScope(self, withNames: $0) }
175+
)
163176
}
164177
}
165178

@@ -300,7 +313,7 @@ extension SyntaxProtocol {
300313
public func introducedToParent(
301314
for name: String?,
302315
at syntax: SwiftSyntax.SyntaxProtocol,
303-
with configDict: LookupConfigDictionary
316+
with config: LookupConfig
304317
) -> [LookupResult] {
305318
let names = conditions.flatMap { element in
306319
LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition)
@@ -332,9 +345,11 @@ extension SyntaxProtocol {
332345
with config: LookupConfig
333346
) -> [LookupResult] {
334347
if body.position <= syntax.position && body.endPosition >= syntax.position {
335-
lookupInParent(for: name, at: self, with: config)
348+
var newConfig = config
349+
newConfig.ignoreChildrenToParentIntroductionsFrom.append(self)
350+
return lookupInParent(for: name, at: syntax, with: newConfig)
336351
} else {
337-
defaultLookupImplementation(for: name, at: syntax, with: config)
352+
return defaultLookupImplementation(for: name, at: syntax, with: config)
338353
}
339354
}
340355
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
@_spi(Experimental) public protocol SequentialScopeSyntax: ScopeSyntax {
16+
func sequentialLookup(
17+
in codeBlockItems: any Collection<CodeBlockItemSyntax>,
18+
for name: String?,
19+
at syntax: SyntaxProtocol,
20+
with config: LookupConfig,
21+
createResultsForThisScopeWith getResults: ([LookupName]) -> (LookupResult)
22+
) -> [LookupResult]
23+
}
24+
25+
@_spi(Experimental) extension SequentialScopeSyntax {
26+
public func sequentialLookup(
27+
in codeBlockItems: any Collection<CodeBlockItemSyntax>,
28+
for name: String?,
29+
at syntax: SyntaxProtocol,
30+
with config: LookupConfig,
31+
createResultsForThisScopeWith getResults: ([LookupName]) -> (LookupResult)
32+
) -> [LookupResult] {
33+
var result = [LookupResult]()
34+
var currentChunk = [LookupName]()
35+
36+
for codeBlockItem in codeBlockItems {
37+
if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self)
38+
as? IntroducingToParentScopeSyntax
39+
{
40+
guard !config.ignoreChildrenToParentIntroductionsFrom.contains(where: { $0.id == introducingToParentScope.id })
41+
else {
42+
continue
43+
}
44+
45+
if !currentChunk.isEmpty {
46+
result.append(getResults(currentChunk))
47+
currentChunk = []
48+
}
49+
50+
result.append(contentsOf: introducingToParentScope.introducedToParent(for: name, at: syntax, with: config))
51+
} else {
52+
currentChunk.append(
53+
contentsOf:
54+
LookupName.getNames(
55+
from: codeBlockItem.item,
56+
accessibleAfter: codeBlockItem.endPosition
57+
).filter { introducedName in
58+
does(name: name, referTo: introducedName, at: syntax)
59+
}
60+
)
61+
}
62+
}
63+
64+
if !currentChunk.isEmpty {
65+
result.append(getResults(currentChunk))
66+
currentChunk = []
67+
}
68+
69+
return (result.isEmpty ? [] : result.reversed()) + lookupInParent(for: name, at: syntax, with: config)
70+
}
71+
}

Tests/SwiftLexicalLookupTest/Assertions.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,26 @@ enum ResultExpectation {
6666
expectedNames
6767
}
6868
}
69+
70+
var debugDescription: String {
71+
switch self {
72+
case .fromScope:
73+
"fromScope"
74+
case .fromFileScope:
75+
"fromFileScope"
76+
}
77+
}
78+
}
79+
80+
extension LookupResult {
81+
var debugDescription: String {
82+
switch self {
83+
case .fromScope:
84+
"fromScope"
85+
case .fromFileScope:
86+
"fromFileScope"
87+
}
88+
}
6989
}
7090

7191
/// `methodUnderTest` is called with the token at every position marker in the keys of `expected`.
@@ -175,7 +195,9 @@ func assertLexicalNameLookup(
175195
case (.fromFileScope, .fromFileScope):
176196
break
177197
default:
178-
XCTFail("For marker \(marker), result actual result kind \(actual) doesn't match expected \(expected)")
198+
XCTFail(
199+
"For marker \(marker), actual result kind \(actual.debugDescription) doesn't match expected \(expected.debugDescription)"
200+
)
179201
}
180202
}
181203

Tests/SwiftLexicalLookupTest/NameLookupTests.swift

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ final class testNameLookup: XCTestCase {
150150
"6️⃣": [
151151
.fromScope(ClosureExprSyntax.self, expectedNames: ["3️⃣"]),
152152
.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]),
153-
],
153+
]
154154
],
155155
expectedResultTypes: .all(
156156
ClosureCaptureSyntax.self,
@@ -567,4 +567,49 @@ final class testNameLookup: XCTestCase {
567567
config: LookupConfig(fileScopeHandling: .codeBlock)
568568
)
569569
}
570+
571+
func testGuardOnFileScope() {
572+
assertLexicalNameLookup(
573+
source: """
574+
let 1️⃣a = 0
575+
576+
class c {}
577+
578+
guard let 2️⃣a else { fatalError() }
579+
580+
3️⃣class a {}
581+
582+
let x = 4️⃣a
583+
""",
584+
references: [
585+
"4️⃣": [.fromFileScope(expectedNames: ["1️⃣"]),
586+
.fromFileScope(expectedNames: ["3️⃣"]),
587+
.fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"])],
588+
],
589+
expectedResultTypes: .all(IdentifierPatternSyntax.self, except: ["3️⃣": ClassDeclSyntax.self])
590+
)
591+
}
592+
593+
func testGuardOnFileScopeCodeBlock() {
594+
assertLexicalNameLookup(
595+
source: """
596+
let 1️⃣a = 0
597+
598+
class c {}
599+
600+
guard let 2️⃣a else { fatalError() }
601+
602+
3️⃣class a {}
603+
604+
let x = 4️⃣a
605+
""",
606+
references: [
607+
"4️⃣": [.fromFileScope(expectedNames: ["3️⃣"]),
608+
.fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]),
609+
.fromFileScope(expectedNames: ["1️⃣"]),],
610+
],
611+
expectedResultTypes: .all(IdentifierPatternSyntax.self, except: ["3️⃣": ClassDeclSyntax.self]),
612+
config: LookupConfig(fileScopeHandling: .codeBlock)
613+
)
614+
}
570615
}

0 commit comments

Comments
 (0)