Skip to content

Commit a537ed1

Browse files
authored
Merge pull request #1616 from ahoppen/container-name-of-accessors
Type-qualify accessors in call hierarchy
2 parents d9ea155 + d4ffe93 commit a537ed1

File tree

2 files changed

+126
-63
lines changed

2 files changed

+126
-63
lines changed

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 80 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,20 +1530,24 @@ extension SourceKitLSPServer {
15301530
)
15311531
}
15321532

1533-
/// Find all symbols in the workspace that include a string in their name.
1534-
/// - returns: An array of SymbolOccurrences that match the string.
1535-
func findWorkspaceSymbols(matching: String) throws -> [SymbolOccurrence] {
1533+
/// Handle a workspace/symbol request, returning the SymbolInformation.
1534+
/// - returns: An array with SymbolInformation for each matching symbol in the workspace.
1535+
func workspaceSymbols(_ req: WorkspaceSymbolsRequest) async throws -> [WorkspaceSymbolItem]? {
15361536
// Ignore short queries since they are:
15371537
// - noisy and slow, since they can match many symbols
15381538
// - normally unintentional, triggered when the user types slowly or if the editor doesn't
15391539
// debounce events while the user is typing
1540-
guard matching.count >= minWorkspaceSymbolPatternLength else {
1540+
guard req.query.count >= minWorkspaceSymbolPatternLength else {
15411541
return []
15421542
}
1543-
var symbolOccurrenceResults: [SymbolOccurrence] = []
1543+
var symbolsAndIndex: [(symbol: SymbolOccurrence, index: CheckedIndex)] = []
15441544
for workspace in workspaces {
1545-
workspace.index(checkedFor: .deletedFiles)?.forEachCanonicalSymbolOccurrence(
1546-
containing: matching,
1545+
guard let index = workspace.index(checkedFor: .deletedFiles) else {
1546+
continue
1547+
}
1548+
var symbolOccurrences: [SymbolOccurrence] = []
1549+
index.forEachCanonicalSymbolOccurrence(
1550+
containing: req.query,
15471551
anchorStart: false,
15481552
anchorEnd: false,
15491553
subsequence: true,
@@ -1555,19 +1559,36 @@ extension SourceKitLSPServer {
15551559
guard !symbol.location.isSystem && !symbol.roles.contains(.accessorOf) else {
15561560
return true
15571561
}
1558-
symbolOccurrenceResults.append(symbol)
1562+
symbolOccurrences.append(symbol)
15591563
return true
15601564
}
15611565
try Task.checkCancellation()
1566+
symbolsAndIndex += symbolOccurrences.map {
1567+
return ($0, index)
1568+
}
15621569
}
1563-
return symbolOccurrenceResults.sorted()
1564-
}
1570+
return symbolsAndIndex.sorted(by: { $0.symbol < $1.symbol }).map { symbolOccurrence, index in
1571+
let symbolPosition = Position(
1572+
line: symbolOccurrence.location.line - 1, // 1-based -> 0-based
1573+
// FIXME: we need to convert the utf8/utf16 column, which may require reading the file!
1574+
utf16index: symbolOccurrence.location.utf8Column - 1
1575+
)
15651576

1566-
/// Handle a workspace/symbol request, returning the SymbolInformation.
1567-
/// - returns: An array with SymbolInformation for each matching symbol in the workspace.
1568-
func workspaceSymbols(_ req: WorkspaceSymbolsRequest) async throws -> [WorkspaceSymbolItem]? {
1569-
let symbols = try findWorkspaceSymbols(matching: req.query).map(WorkspaceSymbolItem.init)
1570-
return symbols
1577+
let symbolLocation = Location(
1578+
uri: symbolOccurrence.location.documentUri,
1579+
range: Range(symbolPosition)
1580+
)
1581+
1582+
return WorkspaceSymbolItem.symbolInformation(
1583+
SymbolInformation(
1584+
name: symbolOccurrence.symbol.name,
1585+
kind: symbolOccurrence.symbol.kind.asLspSymbolKind(),
1586+
deprecated: nil,
1587+
location: symbolLocation,
1588+
containerName: index.containerName(of: symbolOccurrence)
1589+
)
1590+
)
1591+
}
15711592
}
15721593

15731594
/// Forwards a SymbolInfoRequest to the appropriate toolchain service for this document.
@@ -2087,7 +2108,7 @@ extension SourceKitLSPServer {
20872108
}
20882109
return self.indexToLSPCallHierarchyItem(
20892110
symbol: definition.symbol,
2090-
containerName: definition.containerName,
2111+
containerName: index.containerName(of: definition),
20912112
location: location
20922113
)
20932114
}.sorted(by: { Location(uri: $0.uri, range: $0.range) < Location(uri: $1.uri, range: $1.range) })
@@ -2163,6 +2184,12 @@ extension SourceKitLSPServer {
21632184
let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: caller.usr)
21642185
let definitionSymbolLocation = definition?.location
21652186
let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation2)
2187+
let containerName: String? =
2188+
if let definition {
2189+
index.containerName(of: definition)
2190+
} else {
2191+
nil
2192+
}
21662193

21672194
let locations = calls.compactMap { indexToLSPLocation2($0.location) }.sorted()
21682195
guard !locations.isEmpty else {
@@ -2172,7 +2199,7 @@ extension SourceKitLSPServer {
21722199
return CallHierarchyIncomingCall(
21732200
from: indexToLSPCallHierarchyItem2(
21742201
symbol: caller,
2175-
containerName: definition?.containerName,
2202+
containerName: containerName,
21762203
location: definitionLocation ?? locations.first!
21772204
),
21782205
fromRanges: locations.map(\.range)
@@ -2216,11 +2243,17 @@ extension SourceKitLSPServer {
22162243
let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: occurrence.symbol.usr)
22172244
let definitionSymbolLocation = definition?.location
22182245
let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation2)
2246+
let containerName: String? =
2247+
if let definition {
2248+
index.containerName(of: definition)
2249+
} else {
2250+
nil
2251+
}
22192252

22202253
return CallHierarchyOutgoingCall(
22212254
to: indexToLSPCallHierarchyItem2(
22222255
symbol: occurrence.symbol,
2223-
containerName: definition?.containerName,
2256+
containerName: containerName,
22242257
location: definitionLocation ?? location // Use occurrence location as fallback
22252258
),
22262259
fromRanges: [location.range]
@@ -2522,6 +2555,35 @@ fileprivate extension CheckedIndex {
25222555
}
25232556
return result
25242557
}
2558+
2559+
/// Get the name of the symbol that is a parent of this symbol, if one exists
2560+
func containerName(of symbol: SymbolOccurrence) -> String? {
2561+
// The container name of accessors is the container of the surrounding variable.
2562+
let accessorOf = symbol.relations.filter { $0.roles.contains(.accessorOf) }
2563+
if let primaryVariable = accessorOf.sorted().first {
2564+
if accessorOf.count > 1 {
2565+
logger.fault("Expected an occurrence to an accessor of at most one symbol, not multiple")
2566+
}
2567+
if let primaryVariable = primaryDefinitionOrDeclarationOccurrence(ofUSR: primaryVariable.symbol.usr) {
2568+
return containerName(of: primaryVariable)
2569+
}
2570+
}
2571+
2572+
let containers = symbol.relations.filter { $0.roles.contains(.childOf) }
2573+
if containers.count > 1 {
2574+
logger.fault("Expected an occurrence to a child of at most one symbol, not multiple")
2575+
}
2576+
return containers.filter {
2577+
switch $0.symbol.kind {
2578+
case .module, .namespace, .enum, .struct, .class, .protocol, .extension, .union:
2579+
return true
2580+
case .unknown, .namespaceAlias, .macro, .typealias, .function, .variable, .field, .enumConstant,
2581+
.instanceMethod, .classMethod, .staticMethod, .instanceProperty, .classProperty, .staticProperty, .constructor,
2582+
.destructor, .conversionFunction, .parameter, .using, .concept, .commentTag:
2583+
return false
2584+
}
2585+
}.sorted().first?.symbol.name
2586+
}
25252587
}
25262588

25272589
extension IndexSymbolKind {
@@ -2572,26 +2634,6 @@ extension IndexSymbolKind {
25722634
}
25732635
}
25742636

2575-
extension SymbolOccurrence {
2576-
/// Get the name of the symbol that is a parent of this symbol, if one exists
2577-
var containerName: String? {
2578-
let containers = relations.filter { $0.roles.contains(.childOf) }
2579-
if containers.count > 1 {
2580-
logger.fault("Expected an occurrence to a child of at most one symbol, not multiple")
2581-
}
2582-
return containers.filter {
2583-
switch $0.symbol.kind {
2584-
case .module, .namespace, .enum, .struct, .class, .protocol, .extension, .union:
2585-
return true
2586-
case .unknown, .namespaceAlias, .macro, .typealias, .function, .variable, .field, .enumConstant,
2587-
.instanceMethod, .classMethod, .staticMethod, .instanceProperty, .classProperty, .staticProperty, .constructor,
2588-
.destructor, .conversionFunction, .parameter, .using, .concept, .commentTag:
2589-
return false
2590-
}
2591-
}.sorted().first?.symbol.name
2592-
}
2593-
}
2594-
25952637
/// Simple struct for pending notifications/requests, including a cancellation handler.
25962638
/// For convenience the notifications/request handlers are type erased via wrapping.
25972639
fileprivate struct NotificationRequestOperation {
@@ -2637,31 +2679,6 @@ fileprivate func transitiveSubtypeClosure(ofUsrs usrs: [String], index: CheckedI
26372679
return result
26382680
}
26392681

2640-
extension WorkspaceSymbolItem {
2641-
init(_ symbolOccurrence: SymbolOccurrence) {
2642-
let symbolPosition = Position(
2643-
line: symbolOccurrence.location.line - 1, // 1-based -> 0-based
2644-
// FIXME: we need to convert the utf8/utf16 column, which may require reading the file!
2645-
utf16index: symbolOccurrence.location.utf8Column - 1
2646-
)
2647-
2648-
let symbolLocation = Location(
2649-
uri: symbolOccurrence.location.documentUri,
2650-
range: Range(symbolPosition)
2651-
)
2652-
2653-
self = .symbolInformation(
2654-
SymbolInformation(
2655-
name: symbolOccurrence.symbol.name,
2656-
kind: symbolOccurrence.symbol.kind.asLspSymbolKind(),
2657-
deprecated: nil,
2658-
location: symbolLocation,
2659-
containerName: symbolOccurrence.containerName
2660-
)
2661-
)
2662-
}
2663-
}
2664-
26652682
fileprivate extension RequestID {
26662683
/// Returns a numeric value for this request ID.
26672684
///

Tests/SourceKitLSPTests/CallHierarchyTests.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,4 +849,50 @@ final class CallHierarchyTests: XCTestCase {
849849
]
850850
)
851851
}
852+
853+
func testIncomingCallHierarchyFromComputedMember() async throws {
854+
try await SkipUnless.indexOnlyHasContainedByRelationsToIndexedDecls()
855+
let project = try await IndexedSingleSwiftFileTestProject(
856+
"""
857+
struct Foo {
858+
func 1️⃣foo() {}
859+
860+
var testVar: Int {
861+
2️⃣get {
862+
let myVar = 3️⃣foo()
863+
return 2
864+
}
865+
}
866+
}
867+
"""
868+
)
869+
let prepare = try await project.testClient.send(
870+
CallHierarchyPrepareRequest(
871+
textDocument: TextDocumentIdentifier(project.fileURI),
872+
position: project.positions["1️⃣"]
873+
)
874+
)
875+
let initialItem = try XCTUnwrap(prepare?.only)
876+
let calls = try await project.testClient.send(CallHierarchyIncomingCallsRequest(item: initialItem))
877+
XCTAssertEqual(
878+
calls,
879+
[
880+
CallHierarchyIncomingCall(
881+
from: CallHierarchyItem(
882+
name: "Foo.getter:testVar",
883+
kind: .method,
884+
tags: nil,
885+
uri: project.fileURI,
886+
range: Range(project.positions["2️⃣"]),
887+
selectionRange: Range(project.positions["2️⃣"]),
888+
data: .dictionary([
889+
"usr": .string("s:4test3FooV0A3VarSivg"),
890+
"uri": .string(project.fileURI.stringValue),
891+
])
892+
),
893+
fromRanges: [Range(project.positions["3️⃣"])]
894+
)
895+
]
896+
)
897+
}
852898
}

0 commit comments

Comments
 (0)