Skip to content

Commit 1ccccc3

Browse files
committed
Show overriden functions when performing jump-to-definition on a dynamic call
Fixes #809 rdar://114864256
1 parent e6cf723 commit 1ccccc3

File tree

5 files changed

+74
-14
lines changed

5 files changed

+74
-14
lines changed

Sources/LanguageServerProtocol/Requests/SymbolInfoRequest.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,25 @@ public struct SymbolDetails: ResponseType, Hashable {
8181
/// The kind of the symbol
8282
public var kind: SymbolKind?
8383

84+
/// Whether the symbol is a dynamic call for which it isn't known which method will be invoked at runtime. This is
85+
/// the case for protocol methods and class functions.
86+
///
87+
/// Optional because `clangd` does not return whether a symbol is dynamic.
88+
public var isDynamic: Bool?
89+
8490
public init(
8591
name: String?,
86-
containerName: String? = nil,
92+
containerName: String?,
8793
usr: String?,
88-
bestLocalDeclaration: Location? = nil,
89-
kind: SymbolKind? = nil
94+
bestLocalDeclaration: Location?,
95+
kind: SymbolKind?,
96+
isDynamic: Bool
9097
) {
9198
self.name = name
9299
self.containerName = containerName
93100
self.usr = usr
94101
self.bestLocalDeclaration = bestLocalDeclaration
95102
self.kind = kind
103+
self.isDynamic = isDynamic
96104
}
97105
}

Sources/SourceKitD/sourcekitd_uids.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public struct sourcekitd_keys {
4444
public let groupname: sourcekitd_uid_t
4545
public let id: sourcekitd_uid_t
4646
public let is_system: sourcekitd_uid_t
47+
public let isDynamic: sourcekitd_uid_t
4748
public let kind: sourcekitd_uid_t
4849
public let length: sourcekitd_uid_t
4950
public let line: sourcekitd_uid_t
@@ -122,6 +123,7 @@ public struct sourcekitd_keys {
122123
groupname = api.uid_get_from_cstr("key.groupname")!
123124
id = api.uid_get_from_cstr("key.id")!
124125
is_system = api.uid_get_from_cstr("key.is_system")!
126+
isDynamic = api.uid_get_from_cstr("key.is_dynamic")!
125127
kind = api.uid_get_from_cstr("key.kind")!
126128
length = api.uid_get_from_cstr("key.length")!
127129
line = api.uid_get_from_cstr("key.line")!

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1667,7 +1667,7 @@ extension SourceKitServer {
16671667
symbols: [SymbolDetails],
16681668
index: IndexStoreDB?,
16691669
useLocalFallback: Bool = false,
1670-
extractOccurrences: (String, IndexStoreDB) -> [SymbolOccurrence]
1670+
extractOccurrences: (SymbolDetails, IndexStoreDB) -> [SymbolOccurrence]
16711671
) -> [(occurrence: SymbolOccurrence?, location: Location)] {
16721672
guard let symbol = symbols.first else {
16731673
return []
@@ -1680,11 +1680,11 @@ extension SourceKitServer {
16801680
fallback = []
16811681
}
16821682

1683-
guard let usr = symbol.usr, let index = index else {
1683+
guard let index = index else {
16841684
return fallback
16851685
}
16861686

1687-
let occurs = extractOccurrences(usr, index)
1687+
let occurs = extractOccurrences(symbol, index)
16881688
let resolved = occurs.compactMap { occur in
16891689
indexToLSPLocation(occur.location).map {
16901690
(occurrence: occur, location: $0)
@@ -1725,12 +1725,24 @@ extension SourceKitServer {
17251725
}
17261726

17271727
let resolved = self.extractIndexedOccurrences(symbols: symbols, index: index, useLocalFallback: true) {
1728-
(usr, index) in
1728+
(symbolDetails, index) in
1729+
guard symbolDetails.isDynamic ?? false else {
1730+
// If the symbol isn't dynamic, we won't get more information from the index compared to what we already have
1731+
// in the symbolDetails response. Return an empty array so that `extractIndexedOccurrences` falls back to the
1732+
// location in the symbol details response.
1733+
return []
1734+
}
1735+
guard let usr = symbolDetails.usr else { return [] }
17291736
logger.info("performing indexed jump-to-def with usr \(usr)")
17301737
var occurs = index.occurrences(ofUSR: usr, roles: [.definition])
17311738
if occurs.isEmpty {
17321739
occurs = index.occurrences(ofUSR: usr, roles: [.declaration])
17331740
}
1741+
if symbolDetails.isDynamic ?? false {
1742+
occurs += occurs.flatMap {
1743+
index.occurrences(relatedToUSR: $0.symbol.usr, roles: .overrideOf)
1744+
}
1745+
}
17341746
return occurs
17351747
}
17361748

@@ -1783,7 +1795,8 @@ extension SourceKitServer {
17831795
)
17841796
)
17851797
let index = await self.workspaceForDocument(uri: req.textDocument.uri)?.index
1786-
let extractedResult = self.extractIndexedOccurrences(symbols: symbols, index: index) { (usr, index) in
1798+
let extractedResult = self.extractIndexedOccurrences(symbols: symbols, index: index) { (symbolDetails, index) in
1799+
guard let usr = symbolDetails.usr else { return [] }
17871800
var occurs = index.occurrences(ofUSR: usr, roles: .baseOf)
17881801
if occurs.isEmpty {
17891802
occurs = index.occurrences(relatedToUSR: usr, roles: .overrideOf)
@@ -1806,7 +1819,8 @@ extension SourceKitServer {
18061819
)
18071820
)
18081821
let index = await self.workspaceForDocument(uri: req.textDocument.uri)?.index
1809-
let extractedResult = self.extractIndexedOccurrences(symbols: symbols, index: index) { (usr, index) in
1822+
let extractedResult = self.extractIndexedOccurrences(symbols: symbols, index: index) { (symbolDetails, index) in
1823+
guard let usr = symbolDetails.usr else { return [] }
18101824
logger.info("performing indexed jump-to-def with usr \(usr)")
18111825
var roles: SymbolRole = [.reference]
18121826
if req.context.includeDeclaration {
@@ -1852,8 +1866,9 @@ extension SourceKitServer {
18521866
)
18531867
let index = await self.workspaceForDocument(uri: req.textDocument.uri)?.index
18541868
// For call hierarchy preparation we only locate the definition
1855-
let extractedResult = self.extractIndexedOccurrences(symbols: symbols, index: index) { (usr, index) in
1856-
index.occurrences(ofUSR: usr, roles: [.definition, .declaration])
1869+
let extractedResult = self.extractIndexedOccurrences(symbols: symbols, index: index) { (symbolDetails, index) in
1870+
guard let usr = symbolDetails.usr else { return [] }
1871+
return index.occurrences(ofUSR: usr, roles: [.definition, .declaration])
18571872
}
18581873
return extractedResult.compactMap { info -> CallHierarchyItem? in
18591874
guard let occurrence = info.occurrence else {
@@ -2009,8 +2024,9 @@ extension SourceKitServer {
20092024
guard let index = await self.workspaceForDocument(uri: req.textDocument.uri)?.index else {
20102025
return []
20112026
}
2012-
let extractedResult = self.extractIndexedOccurrences(symbols: symbols, index: index) { (usr, index) in
2013-
index.occurrences(ofUSR: usr, roles: [.definition, .declaration])
2027+
let extractedResult = self.extractIndexedOccurrences(symbols: symbols, index: index) { (symbolDetails, index) in
2028+
guard let usr = symbolDetails.usr else { return [] }
2029+
return index.occurrences(ofUSR: usr, roles: [.definition, .declaration])
20142030
}
20152031
return extractedResult.compactMap { info -> TypeHierarchyItem? in
20162032
guard let occurrence = info.occurrence else {

Sources/SourceKitLSP/Swift/CursorInfo.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ extension SwiftLanguageServer {
131131
containerName: nil,
132132
usr: dict[keys.usr],
133133
bestLocalDeclaration: location,
134-
kind: kind.asSymbolKind(self.sourcekitd.values)
134+
kind: kind.asSymbolKind(self.sourcekitd.values),
135+
isDynamic: dict[keys.isDynamic] ?? false
135136
),
136137
annotatedDeclaration: dict[keys.annotated_decl],
137138
documentationXML: dict[keys.doc_full_as_xml],

Tests/SourceKitLSPTests/DefinitionTests.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,37 @@ class DefinitionTests: XCTestCase {
3636
}
3737
XCTAssertEqual(locations, [Location(uri: uri, range: Range(positions["1️⃣"]))])
3838
}
39+
40+
func testJumpToDefinitionIncludesOverrides() async throws {
41+
let ws = try await IndexedSingleSwiftFileWorkspace(
42+
"""
43+
protocol TestProtocol {
44+
func 1️⃣doThing()
45+
}
46+
47+
struct TestImpl: TestProtocol {
48+
func 2️⃣doThing() { }
49+
}
50+
51+
func anyTestProtocol(value: any TestProtocol) {
52+
value.3️⃣doThing()
53+
}
54+
"""
55+
)
56+
57+
let response = try await ws.testClient.send(
58+
DefinitionRequest(textDocument: TextDocumentIdentifier(ws.fileURI), position: ws.positions["3️⃣"])
59+
)
60+
guard case .locations(let locations) = response else {
61+
XCTFail("Expected locations response")
62+
return
63+
}
64+
XCTAssertEqual(
65+
locations,
66+
[
67+
Location(uri: ws.fileURI, range: Range(ws.positions["1️⃣"])),
68+
Location(uri: ws.fileURI, range: Range(ws.positions["2️⃣"])),
69+
]
70+
)
71+
}
3972
}

0 commit comments

Comments
 (0)