Skip to content

Commit 8ed842b

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 8ed842b

File tree

3 files changed

+211
-13
lines changed

3 files changed

+211
-13
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: 166 additions & 8 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
"""
@@ -255,7 +256,6 @@ final class CallHierarchyTests: XCTestCase {
255256
name: "testFunc(x:)",
256257
kind: .function,
257258
tags: nil,
258-
detail: nil,
259259
uri: project.fileURI,
260260
range: Range(project.positions["2️⃣"]),
261261
selectionRange: Range(project.positions["2️⃣"]),
@@ -271,6 +271,7 @@ final class CallHierarchyTests: XCTestCase {
271271
}
272272

273273
func testIncomingCallHierarchyFromComputedProperty() async throws {
274+
try await SkipUnless.indexOnlyHasContainedByRelationsToIndexedDecls()
274275
let project = try await IndexedSingleSwiftFileTestProject(
275276
"""
276277
func 1️⃣foo() {}
@@ -303,7 +304,6 @@ final class CallHierarchyTests: XCTestCase {
303304
name: "getter:testVar",
304305
kind: .function,
305306
tags: nil,
306-
detail: nil,
307307
uri: project.fileURI,
308308
range: Range(project.positions["2️⃣"]),
309309
selectionRange: Range(project.positions["2️⃣"]),
@@ -328,7 +328,6 @@ final class CallHierarchyTests: XCTestCase {
328328
name: "testFunc()",
329329
kind: .function,
330330
tags: nil,
331-
detail: nil,
332331
uri: project.fileURI,
333332
range: Range(project.positions["4️⃣"]),
334333
selectionRange: Range(project.positions["4️⃣"]),
@@ -370,7 +369,6 @@ final class CallHierarchyTests: XCTestCase {
370369
name: "testFunc()",
371370
kind: .function,
372371
tags: nil,
373-
detail: nil,
374372
uri: project.fileURI,
375373
range: Range(project.positions["2️⃣"]),
376374
selectionRange: Range(project.positions["2️⃣"]),
@@ -411,7 +409,6 @@ final class CallHierarchyTests: XCTestCase {
411409
name: "getter:foo",
412410
kind: .function,
413411
tags: nil,
414-
detail: nil,
415412
uri: project.fileURI,
416413
range: Range(project.positions["1️⃣"]),
417414
selectionRange: Range(project.positions["1️⃣"]),
@@ -451,7 +448,6 @@ final class CallHierarchyTests: XCTestCase {
451448
name: "testFunc()",
452449
kind: .function,
453450
tags: nil,
454-
detail: nil,
455451
uri: project.fileURI,
456452
range: Range(project.positions["1️⃣"]),
457453
selectionRange: Range(project.positions["1️⃣"]),
@@ -500,7 +496,6 @@ final class CallHierarchyTests: XCTestCase {
500496
name: "test(proto:)",
501497
kind: .function,
502498
tags: nil,
503-
detail: nil,
504499
uri: project.fileURI,
505500
range: Range(project.positions["2️⃣"]),
506501
selectionRange: Range(project.positions["2️⃣"]),
@@ -549,7 +544,6 @@ final class CallHierarchyTests: XCTestCase {
549544
name: "test(base:)",
550545
kind: .function,
551546
tags: nil,
552-
detail: nil,
553547
uri: project.fileURI,
554548
range: Range(project.positions["2️⃣"]),
555549
selectionRange: Range(project.positions["2️⃣"]),
@@ -606,4 +600,168 @@ final class CallHierarchyTests: XCTestCase {
606600
]
607601
)
608602
}
603+
604+
func testUnappliedFunctionReferenceInIncomingCallHierarchy() async throws {
605+
try await SkipUnless.indexOnlyHasContainedByRelationsToIndexedDecls()
606+
let project = try await IndexedSingleSwiftFileTestProject(
607+
"""
608+
func 1️⃣foo() {}
609+
610+
func 2️⃣testFunc(x: String) {
611+
let myVar = 3️⃣foo
612+
}
613+
"""
614+
)
615+
let prepare = try await project.testClient.send(
616+
CallHierarchyPrepareRequest(
617+
textDocument: TextDocumentIdentifier(project.fileURI),
618+
position: project.positions["1️⃣"]
619+
)
620+
)
621+
let initialItem = try XCTUnwrap(prepare?.only)
622+
let calls = try await project.testClient.send(CallHierarchyIncomingCallsRequest(item: initialItem))
623+
XCTAssertEqual(
624+
calls,
625+
[
626+
CallHierarchyIncomingCall(
627+
from: CallHierarchyItem(
628+
name: "testFunc(x:)",
629+
kind: .function,
630+
tags: nil,
631+
uri: project.fileURI,
632+
range: Range(project.positions["2️⃣"]),
633+
selectionRange: Range(project.positions["2️⃣"]),
634+
data: .dictionary([
635+
"usr": .string("s:4test0A4Func1xySS_tF"),
636+
"uri": .string(project.fileURI.stringValue),
637+
])
638+
),
639+
fromRanges: [Range(project.positions["3️⃣"])]
640+
)
641+
]
642+
)
643+
}
644+
645+
func testUnappliedFunctionReferenceInOutgoingCallHierarchy() async throws {
646+
try await SkipUnless.indexOnlyHasContainedByRelationsToIndexedDecls()
647+
let project = try await IndexedSingleSwiftFileTestProject(
648+
"""
649+
func 1️⃣foo() {}
650+
651+
func 2️⃣testFunc(x: String) {
652+
let myVar = 3️⃣foo
653+
}
654+
"""
655+
)
656+
let prepare = try await project.testClient.send(
657+
CallHierarchyPrepareRequest(
658+
textDocument: TextDocumentIdentifier(project.fileURI),
659+
position: project.positions["2️⃣"]
660+
)
661+
)
662+
let initialItem = try XCTUnwrap(prepare?.only)
663+
let calls = try await project.testClient.send(CallHierarchyOutgoingCallsRequest(item: initialItem))
664+
XCTAssertEqual(
665+
calls,
666+
[
667+
CallHierarchyOutgoingCall(
668+
to: CallHierarchyItem(
669+
name: "foo()",
670+
kind: .function,
671+
tags: nil,
672+
uri: project.fileURI,
673+
range: Range(project.positions["1️⃣"]),
674+
selectionRange: Range(project.positions["1️⃣"]),
675+
data: .dictionary([
676+
"usr": .string("s:4test3fooyyF"),
677+
"uri": .string(project.fileURI.stringValue),
678+
])
679+
),
680+
fromRanges: [Range(project.positions["3️⃣"])]
681+
)
682+
]
683+
)
684+
}
685+
686+
func testIncomingCallHierarchyForPropertyInitializedWithClosure() async throws {
687+
try await SkipUnless.indexOnlyHasContainedByRelationsToIndexedDecls()
688+
let project = try await IndexedSingleSwiftFileTestProject(
689+
"""
690+
func 1️⃣foo() -> Int {}
691+
692+
let 2️⃣myVar: Int = {
693+
3️⃣foo()
694+
}()
695+
"""
696+
)
697+
let prepare = try await project.testClient.send(
698+
CallHierarchyPrepareRequest(
699+
textDocument: TextDocumentIdentifier(project.fileURI),
700+
position: project.positions["1️⃣"]
701+
)
702+
)
703+
let initialItem = try XCTUnwrap(prepare?.only)
704+
let calls = try await project.testClient.send(CallHierarchyIncomingCallsRequest(item: initialItem))
705+
XCTAssertEqual(
706+
calls,
707+
[
708+
CallHierarchyIncomingCall(
709+
from: CallHierarchyItem(
710+
name: "myVar",
711+
kind: .variable,
712+
tags: nil,
713+
uri: project.fileURI,
714+
range: Range(project.positions["2️⃣"]),
715+
selectionRange: Range(project.positions["2️⃣"]),
716+
data: .dictionary([
717+
"usr": .string("s:4test5myVarSivp"),
718+
"uri": .string(project.fileURI.stringValue),
719+
])
720+
),
721+
fromRanges: [Range(project.positions["3️⃣"])]
722+
)
723+
]
724+
)
725+
}
726+
727+
func testOutgoingCallHierarchyForPropertyInitializedWithClosure() async throws {
728+
try await SkipUnless.indexOnlyHasContainedByRelationsToIndexedDecls()
729+
let project = try await IndexedSingleSwiftFileTestProject(
730+
"""
731+
func 1️⃣foo() -> Int {}
732+
733+
let 2️⃣myVar: Int = {
734+
3️⃣foo()
735+
}()
736+
"""
737+
)
738+
let prepare = try await project.testClient.send(
739+
CallHierarchyPrepareRequest(
740+
textDocument: TextDocumentIdentifier(project.fileURI),
741+
position: project.positions["2️⃣"]
742+
)
743+
)
744+
let initialItem = try XCTUnwrap(prepare?.only)
745+
let calls = try await project.testClient.send(CallHierarchyOutgoingCallsRequest(item: initialItem))
746+
XCTAssertEqual(
747+
calls,
748+
[
749+
CallHierarchyOutgoingCall(
750+
to: CallHierarchyItem(
751+
name: "foo()",
752+
kind: .function,
753+
tags: nil,
754+
uri: project.fileURI,
755+
range: Range(project.positions["1️⃣"]),
756+
selectionRange: Range(project.positions["1️⃣"]),
757+
data: .dictionary([
758+
"usr": .string("s:4test3fooSiyF"),
759+
"uri": .string(project.fileURI.stringValue),
760+
])
761+
),
762+
fromRanges: [Range(project.positions["3️⃣"])]
763+
)
764+
]
765+
)
766+
}
609767
}

0 commit comments

Comments
 (0)