Skip to content

Commit 17e2db0

Browse files
authored
Merge pull request #1909 from ahoppen/cache-container-names
Cache container names in `CheckedIndex`
2 parents 991b893 + 5b9b531 commit 17e2db0

File tree

3 files changed

+133
-38
lines changed

3 files changed

+133
-38
lines changed

Sources/SemanticIndex/CheckedIndex.swift

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,32 @@ package final class CheckedIndex {
6060
private var checker: IndexOutOfDateChecker
6161
private let index: IndexStoreDB
6262

63+
/// Maps the USR of a symbol to its name and the name of all its containers, from outermost to innermost.
64+
///
65+
/// It is important that we cache this because we might find a lot of symbols in the same container for eg. workspace
66+
/// symbols (eg. consider many symbols in the same C++ namespace). If we didn't cache this value, then we would need
67+
/// to perform a `primaryDefinitionOrDeclarationOccurrence` lookup for all of these containers, which is expensive.
68+
///
69+
/// Since we don't expect `CheckedIndex` to be outlive a single request it is acceptable to cache these results
70+
/// without having any invalidation logic (similar to how we don't invalide results cached in
71+
/// `IndexOutOfDateChecker`).
72+
///
73+
/// ### Examples
74+
/// If we have
75+
/// ```swift
76+
/// struct Foo {}
77+
/// ``` then
78+
/// `containerNamesCache[<usr of Foo>]` will be `["Foo"]`.
79+
///
80+
/// If we have
81+
/// ```swift
82+
/// struct Bar {
83+
/// struct Foo {}
84+
/// }
85+
/// ```, then
86+
/// `containerNamesCache[<usr of Foo>]` will be `["Bar", "Foo"]`.
87+
private var containerNamesCache: [String: [String]] = [:]
88+
6389
fileprivate init(index: IndexStoreDB, checkLevel: IndexCheckLevel) {
6490
self.index = index
6591
self.checker = IndexOutOfDateChecker(checkLevel: checkLevel)
@@ -183,6 +209,84 @@ package final class CheckedIndex {
183209
}
184210
return result
185211
}
212+
213+
/// The names of all containers the symbol is contained in, from outermost to innermost.
214+
///
215+
/// ### Examples
216+
/// In the following, the container names of `test` are `["Foo"]`.
217+
/// ```swift
218+
/// struct Foo {
219+
/// func test() {}
220+
/// }
221+
/// ```
222+
///
223+
/// In the following, the container names of `test` are `["Bar", "Foo"]`.
224+
/// ```swift
225+
/// struct Bar {
226+
/// struct Foo {
227+
/// func test() {}
228+
/// }
229+
/// }
230+
/// ```
231+
package func containerNames(of symbol: SymbolOccurrence) -> [String] {
232+
// The container name of accessors is the container of the surrounding variable.
233+
let accessorOf = symbol.relations.filter { $0.roles.contains(.accessorOf) }
234+
if let primaryVariable = accessorOf.sorted().first {
235+
if accessorOf.count > 1 {
236+
logger.fault("Expected an occurrence to an accessor of at most one symbol, not multiple")
237+
}
238+
if let primaryVariable = primaryDefinitionOrDeclarationOccurrence(ofUSR: primaryVariable.symbol.usr) {
239+
return containerNames(of: primaryVariable)
240+
}
241+
}
242+
243+
let containers = symbol.relations.filter { $0.roles.contains(.childOf) }
244+
if containers.count > 1 {
245+
logger.fault("Expected an occurrence to a child of at most one symbol, not multiple")
246+
}
247+
let container = containers.filter {
248+
switch $0.symbol.kind {
249+
case .module, .namespace, .enum, .struct, .class, .protocol, .extension, .union:
250+
return true
251+
case .unknown, .namespaceAlias, .macro, .typealias, .function, .variable, .field, .enumConstant,
252+
.instanceMethod, .classMethod, .staticMethod, .instanceProperty, .classProperty, .staticProperty, .constructor,
253+
.destructor, .conversionFunction, .parameter, .using, .concept, .commentTag:
254+
return false
255+
}
256+
}.sorted().first
257+
258+
guard var containerSymbol = container?.symbol else {
259+
return []
260+
}
261+
if let cached = containerNamesCache[containerSymbol.usr] {
262+
return cached
263+
}
264+
265+
if containerSymbol.kind == .extension,
266+
let extendedSymbol = self.occurrences(relatedToUSR: containerSymbol.usr, roles: .extendedBy).first?.symbol
267+
{
268+
containerSymbol = extendedSymbol
269+
}
270+
let result: [String]
271+
272+
// Use `forEachSymbolOccurrence` instead of `primaryDefinitionOrDeclarationOccurrence` to get a symbol occurrence
273+
// for the container because it can be significantly faster: Eg. when searching for a C++ namespace (such as `llvm`),
274+
// it may be declared in many files. Finding the canonical definition means that we would need to scan through all
275+
// of these files. But we expect all all of these declarations to have the same parent container names and we don't
276+
// care about locations here.
277+
var containerDefinition: SymbolOccurrence?
278+
forEachSymbolOccurrence(byUSR: containerSymbol.usr, roles: [.definition, .declaration]) { occurrence in
279+
containerDefinition = occurrence
280+
return false // stop iteration
281+
}
282+
if let containerDefinition {
283+
result = self.containerNames(of: containerDefinition) + [containerSymbol.name]
284+
} else {
285+
result = [containerSymbol.name]
286+
}
287+
containerNamesCache[containerSymbol.usr] = result
288+
return result
289+
}
186290
}
187291

188292
/// A wrapper around `IndexStoreDB` that allows the retrieval of a `CheckedIndex` with a specified check level or the

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2379,48 +2379,11 @@ private let maxWorkspaceSymbolResults = 4096
23792379
package typealias Diagnostic = LanguageServerProtocol.Diagnostic
23802380

23812381
fileprivate extension CheckedIndex {
2382-
func containerNames(of symbol: SymbolOccurrence) -> [String] {
2383-
// The container name of accessors is the container of the surrounding variable.
2384-
let accessorOf = symbol.relations.filter { $0.roles.contains(.accessorOf) }
2385-
if let primaryVariable = accessorOf.sorted().first {
2386-
if accessorOf.count > 1 {
2387-
logger.fault("Expected an occurrence to an accessor of at most one symbol, not multiple")
2388-
}
2389-
if let primaryVariable = primaryDefinitionOrDeclarationOccurrence(ofUSR: primaryVariable.symbol.usr) {
2390-
return containerNames(of: primaryVariable)
2391-
}
2392-
}
2393-
2394-
let containers = symbol.relations.filter { $0.roles.contains(.childOf) }
2395-
if containers.count > 1 {
2396-
logger.fault("Expected an occurrence to a child of at most one symbol, not multiple")
2397-
}
2398-
let container = containers.filter {
2399-
switch $0.symbol.kind {
2400-
case .module, .namespace, .enum, .struct, .class, .protocol, .extension, .union:
2401-
return true
2402-
case .unknown, .namespaceAlias, .macro, .typealias, .function, .variable, .field, .enumConstant,
2403-
.instanceMethod, .classMethod, .staticMethod, .instanceProperty, .classProperty, .staticProperty, .constructor,
2404-
.destructor, .conversionFunction, .parameter, .using, .concept, .commentTag:
2405-
return false
2406-
}
2407-
}.sorted().first
2408-
2409-
if let container {
2410-
if let containerDefinition = primaryDefinitionOrDeclarationOccurrence(ofUSR: container.symbol.usr) {
2411-
return self.containerNames(of: containerDefinition) + [container.symbol.name]
2412-
}
2413-
return [container.symbol.name]
2414-
} else {
2415-
return []
2416-
}
2417-
}
2418-
24192382
/// Take the name of containers into account to form a fully-qualified name for the given symbol.
24202383
/// This means that we will form names of nested types and type-qualify methods.
24212384
func fullyQualifiedName(of symbolOccurrence: SymbolOccurrence) -> String {
24222385
let symbol = symbolOccurrence.symbol
2423-
let containerNames = containerNames(of: symbolOccurrence)
2386+
let containerNames = self.containerNames(of: symbolOccurrence)
24242387
guard let containerName = containerNames.last else {
24252388
// No containers, so nothing to do.
24262389
return symbol.name

Tests/SourceKitLSPTests/WorkspaceSymbolsTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,32 @@ class WorkspaceSymbolsTests: XCTestCase {
9595
]
9696
)
9797
}
98+
99+
func testContainerNameOfFunctionInExtension() async throws {
100+
let project = try await IndexedSingleSwiftFileTestProject(
101+
"""
102+
struct Foo {
103+
struct Bar {}
104+
}
105+
106+
extension Foo.Bar {
107+
func 1️⃣barMethod() {}
108+
}
109+
"""
110+
)
111+
let response = try await project.testClient.send(WorkspaceSymbolsRequest(query: "barMethod"))
112+
XCTAssertEqual(
113+
response,
114+
[
115+
.symbolInformation(
116+
SymbolInformation(
117+
name: "barMethod()",
118+
kind: .method,
119+
location: Location(uri: project.fileURI, range: Range(project.positions["1️⃣"])),
120+
containerName: "Foo.Bar"
121+
)
122+
)
123+
]
124+
)
125+
}
98126
}

0 commit comments

Comments
 (0)