Skip to content

Commit 1e3488b

Browse files
committed
Don’t repeat a function in incomingCalls if it contains multiple calls to the same function
Eg. if we have the following, and we get the call hierarchy of `foo`, we only want to show `bar` once, with multiple `fromRanges` instead of having two entries for `bar` in the call hierarchy. ```swift func foo() {} func bar() { foo() foo() } ```
1 parent e99892b commit 1e3488b

File tree

2 files changed

+72
-51
lines changed

2 files changed

+72
-51
lines changed

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2126,27 +2126,44 @@ extension SourceKitLSPServer {
21262126
callableUsrs += index.occurrences(ofUSR: data.usr, roles: .overrideOf).flatMap { occurrence in
21272127
occurrence.relations.filter { $0.roles.contains(.overrideOf) }.map(\.symbol.usr)
21282128
}
2129+
// callOccurrences are all the places that any of the USRs in callableUsrs is called.
2130+
// We also load the `calledBy` roles to get the method that contains the reference to this call.
21292131
let callOccurrences = callableUsrs.flatMap { index.occurrences(ofUSR: $0, roles: .calledBy) }
2130-
let calls = callOccurrences.flatMap { occurrence -> [CallHierarchyIncomingCall] in
2131-
guard let location = indexToLSPLocation(occurrence.location) else {
2132-
return []
2132+
2133+
// Maps functions that call a USR in `callableUSRs` to all the called occurrences of `callableUSRs` within the
2134+
// function. If a function `foo` calls `bar` multiple times, `callersToCalls[foo]` will contain two call
2135+
// `SymbolOccurrence`s.
2136+
// This way, we can group multiple calls to `bar` within `foo` to a single item with multiple `fromRanges`.
2137+
var callersToCalls: [Symbol: [SymbolOccurrence]] = [:]
2138+
2139+
for call in callOccurrences {
2140+
// Callers are all `calledBy` relations of a call to a USR in `callableUsrs`, ie. all the functions that contain a
2141+
// call to a USR in callableUSRs. In practice, this should always be a single item.
2142+
let callers = call.relations.filter { $0.roles.contains(.calledBy) }.map(\.symbol)
2143+
for caller in callers {
2144+
callersToCalls[caller, default: []].append(call)
21332145
}
2134-
return occurrence.relations.filter { $0.symbol.kind.isCallable }
2135-
.map { related in
2136-
// Resolve the caller's definition to find its location
2137-
let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: related.symbol.usr)
2138-
let definitionSymbolLocation = definition?.location
2139-
let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation)
2140-
2141-
return CallHierarchyIncomingCall(
2142-
from: indexToLSPCallHierarchyItem(
2143-
symbol: related.symbol,
2144-
containerName: definition?.containerName,
2145-
location: definitionLocation ?? location // Use occurrence location as fallback
2146-
),
2147-
fromRanges: [location.range]
2148-
)
2149-
}
2146+
}
2147+
2148+
let calls = callersToCalls.compactMap { (caller: Symbol, calls: [SymbolOccurrence]) -> CallHierarchyIncomingCall? in
2149+
// Resolve the caller's definition to find its location
2150+
let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: caller.usr)
2151+
let definitionSymbolLocation = definition?.location
2152+
let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation)
2153+
2154+
let locations = calls.compactMap { indexToLSPLocation($0.location) }.sorted()
2155+
guard !locations.isEmpty else {
2156+
return nil
2157+
}
2158+
2159+
return CallHierarchyIncomingCall(
2160+
from: indexToLSPCallHierarchyItem(
2161+
symbol: caller,
2162+
containerName: definition?.containerName,
2163+
location: definitionLocation ?? locations.first!
2164+
),
2165+
fromRanges: locations.map(\.range)
2166+
)
21502167
}
21512168
return calls.sorted(by: { $0.from.name < $1.from.name })
21522169
}
@@ -2455,17 +2472,6 @@ extension IndexSymbolKind {
24552472
return .null
24562473
}
24572474
}
2458-
2459-
var isCallable: Bool {
2460-
switch self {
2461-
case .function, .instanceMethod, .classMethod, .staticMethod, .constructor, .destructor, .conversionFunction:
2462-
return true
2463-
case .unknown, .module, .namespace, .namespaceAlias, .macro, .enum, .struct, .protocol, .extension, .union,
2464-
.typealias, .field, .enumConstant, .parameter, .using, .concept, .commentTag, .variable, .instanceProperty,
2465-
.class, .staticProperty, .classProperty:
2466-
return false
2467-
}
2468-
}
24692475
}
24702476

24712477
extension SymbolOccurrence {

Tests/SourceKitLSPTests/CallHierarchyTests.swift

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,15 @@ final class CallHierarchyTests: XCTestCase {
275275
"""
276276
func 1️⃣foo() {}
277277
278-
var testVar: Int 2️⃣{
279-
let myVar = 3️⃣foo()
280-
return 2
278+
var testVar: Int {
279+
2️⃣get {
280+
let myVar = 3️⃣foo()
281+
return 2
282+
}
283+
}
284+
285+
func 4️⃣testFunc() {
286+
_ = 5️⃣testVar
281287
}
282288
"""
283289
)
@@ -310,6 +316,31 @@ final class CallHierarchyTests: XCTestCase {
310316
)
311317
]
312318
)
319+
320+
let testVarItem = try XCTUnwrap(calls?.first?.from)
321+
322+
let callsToTestVar = try await project.testClient.send(CallHierarchyIncomingCallsRequest(item: testVarItem))
323+
XCTAssertEqual(
324+
callsToTestVar,
325+
[
326+
CallHierarchyIncomingCall(
327+
from: CallHierarchyItem(
328+
name: "testFunc()",
329+
kind: .function,
330+
tags: nil,
331+
detail: nil,
332+
uri: project.fileURI,
333+
range: Range(project.positions["4️⃣"]),
334+
selectionRange: Range(project.positions["4️⃣"]),
335+
data: .dictionary([
336+
"usr": .string("s:4test0A4FuncyyF"),
337+
"uri": .string(project.fileURI.stringValue),
338+
])
339+
),
340+
fromRanges: [Range(project.positions["5️⃣"])]
341+
)
342+
]
343+
)
313344
}
314345

315346
func testIncomingCallHierarchyShowsAccessToVariables() async throws {
@@ -348,24 +379,8 @@ final class CallHierarchyTests: XCTestCase {
348379
"uri": .string(project.fileURI.stringValue),
349380
])
350381
),
351-
fromRanges: [Range(project.positions["3️⃣"])]
352-
),
353-
CallHierarchyIncomingCall(
354-
from: CallHierarchyItem(
355-
name: "testFunc()",
356-
kind: .function,
357-
tags: nil,
358-
detail: nil,
359-
uri: project.fileURI,
360-
range: Range(project.positions["2️⃣"]),
361-
selectionRange: Range(project.positions["2️⃣"]),
362-
data: .dictionary([
363-
"usr": .string("s:4test0A4FuncyyF"),
364-
"uri": .string(project.fileURI.stringValue),
365-
])
366-
),
367-
fromRanges: [Range(project.positions["4️⃣"])]
368-
),
382+
fromRanges: [Range(project.positions["3️⃣"]), Range(project.positions["4️⃣"])]
383+
)
369384
]
370385
)
371386
}

0 commit comments

Comments
 (0)