Skip to content

Commit f7b21d2

Browse files
committed
Show unapplied function references in call hierarchy
With swiftlang/swift#72930 we can use the `containedBy` instead of `calledBy` relation for the call hierarchy, which allows us to show unapplied function references in the call hierarchy as well. rdar://123769825
1 parent b9af0cf commit f7b21d2

File tree

3 files changed

+131
-5
lines changed

3 files changed

+131
-5
lines changed

Sources/SKTestSupport/SkipUnless.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,34 @@ public enum SkipUnless {
232232
}
233233
}
234234

235+
/// Checks whether the index contains a fix that prevents it from adding relations to non-indexed locals
236+
/// (https://github.com/apple/swift/pull/72930).
237+
public static func indexOnlyHasContainedByRelationsToIndexedDecls(
238+
file: StaticString = #file,
239+
line: UInt = #line
240+
) async throws {
241+
return try await skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(6, 0), file: file, line: line) {
242+
let project = try await IndexedSingleSwiftFileTestProject(
243+
"""
244+
func foo() {}
245+
246+
func 1️⃣testFunc(x: String) {
247+
let myVar = foo
248+
}
249+
"""
250+
)
251+
let prepare = try await project.testClient.send(
252+
CallHierarchyPrepareRequest(
253+
textDocument: TextDocumentIdentifier(project.fileURI),
254+
position: project.positions["1️⃣"]
255+
)
256+
)
257+
let initialItem = try XCTUnwrap(prepare?.only)
258+
let calls = try await project.testClient.send(CallHierarchyOutgoingCallsRequest(item: initialItem))
259+
return calls != []
260+
}
261+
}
262+
235263
public static func longTestsEnabled() throws {
236264
if let value = ProcessInfo.processInfo.environment["SKIP_LONG_TESTS"], value == "1" || value == "YES" {
237265
throw XCTSkip("Long tests disabled using the `SKIP_LONG_TESTS` environment variable")

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2134,16 +2134,14 @@ extension SourceKitLSPServer {
21342134
return []
21352135
}
21362136
var callableUsrs = [data.usr]
2137-
// Calls to the accessors of a property count as calls to the property
2138-
callableUsrs += index.occurrences(relatedToUSR: data.usr, roles: .accessorOf).map(\.symbol.usr)
21392137
// Also show calls to the functions that this method overrides. This includes overridden class methods and
21402138
// satisfied protocol requirements.
21412139
callableUsrs += index.occurrences(ofUSR: data.usr, roles: .overrideOf).flatMap { occurrence in
21422140
occurrence.relations.filter { $0.roles.contains(.overrideOf) }.map(\.symbol.usr)
21432141
}
21442142
// callOccurrences are all the places that any of the USRs in callableUsrs is called.
21452143
// We also load the `calledBy` roles to get the method that contains the reference to this call.
2146-
let callOccurrences = callableUsrs.flatMap { index.occurrences(ofUSR: $0, roles: .calledBy) }
2144+
let callOccurrences = callableUsrs.flatMap { index.occurrences(ofUSR: $0, roles: .containedBy) }
21472145

21482146
// Maps functions that call a USR in `callableUSRs` to all the called occurrences of `callableUSRs` within the
21492147
// function. If a function `foo` calls `bar` multiple times, `callersToCalls[foo]` will contain two call
@@ -2154,7 +2152,7 @@ extension SourceKitLSPServer {
21542152
for call in callOccurrences {
21552153
// Callers are all `calledBy` relations of a call to a USR in `callableUsrs`, ie. all the functions that contain a
21562154
// call to a USR in callableUSRs. In practice, this should always be a single item.
2157-
let callers = call.relations.filter { $0.roles.contains(.calledBy) }.map(\.symbol)
2155+
let callers = call.relations.filter { $0.roles.contains(.containedBy) }.map(\.symbol)
21582156
for caller in callers {
21592157
callersToCalls[caller, default: []].append(call)
21602158
}
@@ -2190,8 +2188,11 @@ extension SourceKitLSPServer {
21902188
return []
21912189
}
21922190
let callableUsrs = [data.usr] + index.occurrences(relatedToUSR: data.usr, roles: .accessorOf).map(\.symbol.usr)
2193-
let callOccurrences = callableUsrs.flatMap { index.occurrences(relatedToUSR: $0, roles: .calledBy) }
2191+
let callOccurrences = callableUsrs.flatMap { index.occurrences(relatedToUSR: $0, roles: .containedBy) }
21942192
let calls = callOccurrences.compactMap { occurrence -> CallHierarchyOutgoingCall? in
2193+
guard occurrence.symbol.kind.isCallable else {
2194+
return nil
2195+
}
21952196
guard let location = indexToLSPLocation(occurrence.location) else {
21962197
return nil
21972198
}
@@ -2494,6 +2495,17 @@ extension IndexSymbolKind {
24942495
return .null
24952496
}
24962497
}
2498+
2499+
var isCallable: Bool {
2500+
switch self {
2501+
case .function, .instanceMethod, .classMethod, .staticMethod, .constructor, .destructor, .conversionFunction:
2502+
return true
2503+
case .unknown, .module, .namespace, .namespaceAlias, .macro, .enum, .struct, .protocol, .extension, .union,
2504+
.typealias, .field, .enumConstant, .parameter, .using, .concept, .commentTag, .variable, .instanceProperty,
2505+
.class, .staticProperty, .classProperty:
2506+
return false
2507+
}
2508+
}
24972509
}
24982510

24992511
extension SymbolOccurrence {

Tests/SourceKitLSPTests/CallHierarchyTests.swift

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ final class CallHierarchyTests: XCTestCase {
229229
}
230230

231231
func testIncomingCallHierarchyShowsSurroundingFunctionCall() async throws {
232+
try await SkipUnless.indexOnlyHasContainedByRelationsToIndexedDecls()
232233
// We used to show `myVar` as the caller here
233234
let project = try await IndexedSingleSwiftFileTestProject(
234235
"""
@@ -271,6 +272,7 @@ final class CallHierarchyTests: XCTestCase {
271272
}
272273

273274
func testIncomingCallHierarchyFromComputedProperty() async throws {
275+
try await SkipUnless.indexOnlyHasContainedByRelationsToIndexedDecls()
274276
let project = try await IndexedSingleSwiftFileTestProject(
275277
"""
276278
func 1️⃣foo() {}
@@ -606,4 +608,88 @@ final class CallHierarchyTests: XCTestCase {
606608
]
607609
)
608610
}
611+
612+
func testUnappliedFunctionReferenceInIncomingCallHierarchy() async throws {
613+
try await SkipUnless.indexOnlyHasContainedByRelationsToIndexedDecls()
614+
let project = try await IndexedSingleSwiftFileTestProject(
615+
"""
616+
func 1️⃣foo() {}
617+
618+
func 2️⃣testFunc(x: String) {
619+
let myVar = 3️⃣foo
620+
}
621+
"""
622+
)
623+
let prepare = try await project.testClient.send(
624+
CallHierarchyPrepareRequest(
625+
textDocument: TextDocumentIdentifier(project.fileURI),
626+
position: project.positions["1️⃣"]
627+
)
628+
)
629+
let initialItem = try XCTUnwrap(prepare?.only)
630+
let calls = try await project.testClient.send(CallHierarchyIncomingCallsRequest(item: initialItem))
631+
XCTAssertEqual(
632+
calls,
633+
[
634+
CallHierarchyIncomingCall(
635+
from: CallHierarchyItem(
636+
name: "testFunc(x:)",
637+
kind: .function,
638+
tags: nil,
639+
detail: nil,
640+
uri: project.fileURI,
641+
range: Range(project.positions["2️⃣"]),
642+
selectionRange: Range(project.positions["2️⃣"]),
643+
data: .dictionary([
644+
"usr": .string("s:4test0A4Func1xySS_tF"),
645+
"uri": .string(project.fileURI.stringValue),
646+
])
647+
),
648+
fromRanges: [Range(project.positions["3️⃣"])]
649+
)
650+
]
651+
)
652+
}
653+
654+
func testUnappliedFunctionReferenceInOutgoingCallHierarchy() async throws {
655+
try await SkipUnless.indexOnlyHasContainedByRelationsToIndexedDecls()
656+
let project = try await IndexedSingleSwiftFileTestProject(
657+
"""
658+
func 1️⃣foo() {}
659+
660+
func 2️⃣testFunc(x: String) {
661+
let myVar = 3️⃣foo
662+
}
663+
"""
664+
)
665+
let prepare = try await project.testClient.send(
666+
CallHierarchyPrepareRequest(
667+
textDocument: TextDocumentIdentifier(project.fileURI),
668+
position: project.positions["2️⃣"]
669+
)
670+
)
671+
let initialItem = try XCTUnwrap(prepare?.only)
672+
let calls = try await project.testClient.send(CallHierarchyOutgoingCallsRequest(item: initialItem))
673+
XCTAssertEqual(
674+
calls,
675+
[
676+
CallHierarchyOutgoingCall(
677+
to: CallHierarchyItem(
678+
name: "foo()",
679+
kind: .function,
680+
tags: nil,
681+
detail: nil,
682+
uri: project.fileURI,
683+
range: Range(project.positions["1️⃣"]),
684+
selectionRange: Range(project.positions["1️⃣"]),
685+
data: .dictionary([
686+
"usr": .string("s:4test3fooyyF"),
687+
"uri": .string(project.fileURI.stringValue),
688+
])
689+
),
690+
fromRanges: [Range(project.positions["3️⃣"])]
691+
)
692+
]
693+
)
694+
}
609695
}

0 commit comments

Comments
 (0)