Skip to content

Commit 84fdea9

Browse files
committed
Filter overrides in DefinitionRequest by receiver types
1 parent 1ffa825 commit 84fdea9

File tree

6 files changed

+117
-3
lines changed

6 files changed

+117
-3
lines changed

Sources/LanguageServerProtocol/Requests/SymbolInfoRequest.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,19 +87,46 @@ public struct SymbolDetails: ResponseType, Hashable {
8787
/// Optional because `clangd` does not return whether a symbol is dynamic.
8888
public var isDynamic: Bool?
8989

90+
/// If the symbol is dynamic, the USRs of the types that might be called.
91+
///
92+
/// This is relevant in the following cases
93+
/// ```swift
94+
/// class A {
95+
/// func doThing() {}
96+
/// }
97+
/// class B: A {}
98+
/// class C: B {
99+
/// override func doThing() {}
100+
/// }
101+
/// class D: A {
102+
/// override func doThing() {}
103+
/// }
104+
/// func test(value: B) {
105+
/// value.doThing()
106+
/// }
107+
/// ```
108+
///
109+
/// The USR of the called function in `value.doThing` is `A.doThing` (or its
110+
/// mangled form) but it can never call `D.doThing`. In this case, the
111+
/// receiver USR would be `B`, indicating that only overrides of subtypes in
112+
/// `B` may be called dynamically.
113+
public var receiverUsrs: [String]?
114+
90115
public init(
91116
name: String?,
92117
containerName: String?,
93118
usr: String?,
94119
bestLocalDeclaration: Location?,
95120
kind: SymbolKind?,
96-
isDynamic: Bool
121+
isDynamic: Bool?,
122+
receiverUsrs: [String]?
97123
) {
98124
self.name = name
99125
self.containerName = containerName
100126
self.usr = usr
101127
self.bestLocalDeclaration = bestLocalDeclaration
102128
self.kind = kind
103129
self.isDynamic = isDynamic
130+
self.receiverUsrs = receiverUsrs
104131
}
105132
}

Sources/SourceKitD/SKDResponseArray.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,27 @@ public final class SKDResponseArray {
5555
return true
5656
}
5757

58+
public func map<T>(_ transform: (SKDResponseDictionary) -> T) -> [T] {
59+
var result: [T] = []
60+
result.reserveCapacity(self.count)
61+
self.forEach { _, element in
62+
result.append(transform(element))
63+
return true
64+
}
65+
return result
66+
}
67+
68+
public func compactMap<T>(_ transform: (SKDResponseDictionary) -> T?) -> [T] {
69+
var result: [T] = []
70+
self.forEach { _, element in
71+
if let transformed = transform(element) {
72+
result.append(transformed)
73+
}
74+
return true
75+
}
76+
return result
77+
}
78+
5879
/// Attempt to access the item at `index` as a string.
5980
public subscript(index: Int) -> String? {
6081
if let cstr = sourcekitd.api.variant_array_get_string(array, index) {

Sources/SourceKitD/sourcekitd_uids.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public struct sourcekitd_keys {
5757
public let num_bytes_to_erase: sourcekitd_uid_t
5858
public let offset: sourcekitd_uid_t
5959
public let ranges: sourcekitd_uid_t
60+
public let receivers: sourcekitd_uid_t
6061
public let refactor_actions: sourcekitd_uid_t
6162
public let request: sourcekitd_uid_t
6263
public let results: sourcekitd_uid_t
@@ -136,6 +137,7 @@ public struct sourcekitd_keys {
136137
num_bytes_to_erase = api.uid_get_from_cstr("key.num_bytes_to_erase")!
137138
offset = api.uid_get_from_cstr("key.offset")!
138139
ranges = api.uid_get_from_cstr("key.ranges")!
140+
receivers = api.uid_get_from_cstr("key.receivers")!
139141
refactor_actions = api.uid_get_from_cstr("key.refactor_actions")!
140142
request = api.uid_get_from_cstr("key.request")!
141143
results = api.uid_get_from_cstr("key.results")!

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1709,8 +1709,18 @@ extension SourceKitServer {
17091709
occurances = index.occurrences(ofUSR: usr, roles: [.declaration])
17101710
}
17111711
if symbol.isDynamic ?? false {
1712+
lazy var transitiveReceiverUsrs: [String] = transitiveSubtypeClosure(
1713+
ofUsrs: symbol.receiverUsrs ?? [],
1714+
index: index
1715+
)
17121716
occurances += occurances.flatMap {
1713-
index.occurrences(relatedToUSR: $0.symbol.usr, roles: .overrideOf)
1717+
let overrides = index.occurrences(relatedToUSR: $0.symbol.usr, roles: .overrideOf)
1718+
// Only contain overrides that are children of one of the receiver types or their subtypes.
1719+
return overrides.filter { override in
1720+
override.relations.contains(where: {
1721+
$0.roles.contains(.childOf) && transitiveReceiverUsrs.contains($0.symbol.usr)
1722+
})
1723+
}
17141724
}
17151725
}
17161726

@@ -2206,3 +2216,17 @@ fileprivate struct DocumentNotificationRequestQueue {
22062216
queue = []
22072217
}
22082218
}
2219+
2220+
/// Returns the USRs of the subtypes of `usrs` as well as their subtypes, transitively.
2221+
fileprivate func transitiveSubtypeClosure(ofUsrs usrs: [String], index: IndexStoreDB) -> [String] {
2222+
var result: [String] = []
2223+
for usr in usrs {
2224+
result.append(usr)
2225+
let directSubtypes = index.occurrences(ofUSR: usr, roles: .baseOf).flatMap { occurance in
2226+
occurance.relations.filter { $0.roles.contains(.baseOf) }.map(\.symbol.usr)
2227+
}
2228+
let transitiveSubtypes = transitiveSubtypeClosure(ofUsrs: directSubtypes, index: index)
2229+
result += transitiveSubtypes
2230+
}
2231+
return result
2232+
}

Sources/SourceKitLSP/Swift/CursorInfo.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,18 @@ extension SwiftLanguageServer {
130130

131131
let refactorActionsArray: SKDResponseArray? = dict[keys.refactor_actions]
132132

133+
let receiversArray: SKDResponseArray? = dict[keys.receivers]
134+
let receiverUsrs = receiversArray?.compactMap { $0[keys.usr] as String? } ?? []
135+
133136
return CursorInfo(
134137
SymbolDetails(
135138
name: dict[keys.name],
136139
containerName: nil,
137140
usr: dict[keys.usr],
138141
bestLocalDeclaration: location,
139142
kind: kind.asSymbolKind(self.sourcekitd.values),
140-
isDynamic: dict[keys.isDynamic] ?? false
143+
isDynamic: dict[keys.isDynamic] ?? false,
144+
receiverUsrs: receiverUsrs
141145
),
142146
annotatedDeclaration: dict[keys.annotated_decl],
143147
documentationXML: dict[keys.doc_full_as_xml],

Tests/SourceKitLSPTests/DefinitionTests.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,40 @@ class DefinitionTests: XCTestCase {
6969
]
7070
)
7171
}
72+
73+
func testJumpToDefinitionFiltersByReceiver() async throws {
74+
let ws = try await IndexedSingleSwiftFileWorkspace(
75+
"""
76+
class A {
77+
func 1️⃣doThing() {}
78+
}
79+
class B: A {}
80+
class C: B {
81+
override func 2️⃣doThing() {}
82+
}
83+
class D: A {
84+
override func doThing() {}
85+
}
86+
87+
func test(value: B) {
88+
value.3️⃣doThing()
89+
}
90+
"""
91+
)
92+
93+
let response = try await ws.testClient.send(
94+
DefinitionRequest(textDocument: TextDocumentIdentifier(ws.fileURI), position: ws.positions["3️⃣"])
95+
)
96+
guard case .locations(let locations) = response else {
97+
XCTFail("Expected locations response")
98+
return
99+
}
100+
XCTAssertEqual(
101+
locations,
102+
[
103+
Location(uri: ws.fileURI, range: Range(ws.positions["1️⃣"])),
104+
Location(uri: ws.fileURI, range: Range(ws.positions["2️⃣"])),
105+
]
106+
)
107+
}
72108
}

0 commit comments

Comments
 (0)