Skip to content

Commit 4d80ff6

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 224900d commit 4d80ff6

File tree

2 files changed

+38
-37
lines changed

2 files changed

+38
-37
lines changed

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2128,27 +2128,44 @@ extension SourceKitLSPServer {
21282128
callableUsrs += index.occurrences(ofUSR: data.usr, roles: .overrideOf).flatMap { occurrence in
21292129
occurrence.relations.filter { $0.roles.contains(.overrideOf) }.map(\.symbol.usr)
21302130
}
2131+
// callOccurrences are all the places that any of the USRs in callableUsrs is called.
2132+
// We also load the `calledBy` roles to get the method that contains the reference to this call.
21312133
let callOccurrences = callableUsrs.flatMap { index.occurrences(ofUSR: $0, roles: .calledBy) }
2132-
let calls = callOccurrences.flatMap { occurrence -> [CallHierarchyIncomingCall] in
2133-
guard let location = indexToLSPLocation(occurrence.location) else {
2134-
return []
2134+
2135+
// Maps functions that call a USR in `callableUSRs` to all the called occurrences of `callableUSRs` within the
2136+
// function. If a function `foo` calls `bar` multiple times, `callersToCalls[foo]` will contain two call
2137+
// `SymbolOccurrence`s.
2138+
// This way, we can group multiple calls to `bar` within `foo` to a single item with multiple `fromRanges`.
2139+
var callersToCalls: [Symbol: [SymbolOccurrence]] = [:]
2140+
2141+
for call in callOccurrences {
2142+
// Callers are all `calledBy` relations of a call to a USR in `callableUsrs`, ie. all the functions that contain a
2143+
// call to a USR in callableUSRs. In practice, this should always be a single item.
2144+
let callers = call.relations.filter { $0.symbol.kind.isCallable }.map(\.symbol)
2145+
for caller in callers {
2146+
callersToCalls[caller, default: []].append(call)
21352147
}
2136-
return occurrence.relations.filter { $0.symbol.kind.isCallable }
2137-
.map { related in
2138-
// Resolve the caller's definition to find its location
2139-
let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: related.symbol.usr)
2140-
let definitionSymbolLocation = definition?.location
2141-
let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation)
2142-
2143-
return CallHierarchyIncomingCall(
2144-
from: indexToLSPCallHierarchyItem(
2145-
symbol: related.symbol,
2146-
containerName: definition?.containerName,
2147-
location: definitionLocation ?? location // Use occurrence location as fallback
2148-
),
2149-
fromRanges: [location.range]
2150-
)
2151-
}
2148+
}
2149+
2150+
let calls = callersToCalls.compactMap { (caller: Symbol, calls: [SymbolOccurrence]) -> CallHierarchyIncomingCall? in
2151+
// Resolve the caller's definition to find its location
2152+
let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: caller.usr)
2153+
let definitionSymbolLocation = definition?.location
2154+
let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation)
2155+
2156+
let locations = calls.compactMap { indexToLSPLocation($0.location) }.sorted()
2157+
guard !locations.isEmpty else {
2158+
return nil
2159+
}
2160+
2161+
return CallHierarchyIncomingCall(
2162+
from: indexToLSPCallHierarchyItem(
2163+
symbol: caller,
2164+
containerName: definition?.containerName,
2165+
location: definitionLocation ?? locations.first!
2166+
),
2167+
fromRanges: locations.map(\.range)
2168+
)
21522169
}
21532170
return calls.sorted(by: { $0.from.name < $1.from.name })
21542171
}

Tests/SourceKitLSPTests/CallHierarchyTests.swift

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -348,24 +348,8 @@ final class CallHierarchyTests: XCTestCase {
348348
"uri": .string(project.fileURI.stringValue),
349349
])
350350
),
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-
),
351+
fromRanges: [Range(project.positions["3️⃣"]), Range(project.positions["4️⃣"])]
352+
)
369353
]
370354
)
371355
}

0 commit comments

Comments
 (0)