Skip to content

Commit 2f2879e

Browse files
support function parameter and return type disambiguations in textDocument/doccDocumentation requests
1 parent fe20eac commit 2f2879e

File tree

5 files changed

+300
-132
lines changed

5 files changed

+300
-132
lines changed

Sources/DocCDocumentation/DocCSymbolInformation.swift

Lines changed: 17 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,30 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import Foundation
14-
import IndexStoreDB
15-
package import SemanticIndex
1614
@_spi(LinkCompletion) @preconcurrency import SwiftDocC
15+
import SwiftExtensions
1716
import SymbolKit
1817

1918
package struct DocCSymbolInformation {
20-
let components: [(name: String, information: LinkCompletionTools.SymbolInformation)]
19+
struct Component {
20+
let name: String
21+
let information: LinkCompletionTools.SymbolInformation
2122

22-
/// Find the DocCSymbolLink for a given symbol USR.
23-
///
24-
/// - Parameters:
25-
/// - usr: The symbol USR to find in the index.
26-
/// - index: The CheckedIndex to search within.
27-
package init?(fromUSR usr: String, in index: CheckedIndex) {
28-
guard let topLevelSymbolOccurrence = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: usr) else {
29-
return nil
23+
init(fromModuleName moduleName: String) {
24+
self.name = moduleName
25+
self.information = LinkCompletionTools.SymbolInformation(fromModuleName: moduleName)
3026
}
31-
let moduleName = topLevelSymbolOccurrence.location.moduleName
32-
var components = [topLevelSymbolOccurrence]
33-
// Find any parent symbols
34-
var symbolOccurrence: SymbolOccurrence = topLevelSymbolOccurrence
35-
while let parentSymbolOccurrence = symbolOccurrence.parent(index) {
36-
components.insert(parentSymbolOccurrence, at: 0)
37-
symbolOccurrence = parentSymbolOccurrence
27+
28+
init(fromSymbol symbol: SymbolGraph.Symbol) {
29+
self.name = symbol.pathComponents.last ?? symbol.names.title
30+
self.information = LinkCompletionTools.SymbolInformation(symbol: symbol)
3831
}
39-
self.components =
40-
[(name: moduleName, LinkCompletionTools.SymbolInformation(fromModuleName: moduleName))]
41-
+ components.map {
42-
(name: $0.symbol.name, information: LinkCompletionTools.SymbolInformation(fromSymbolOccurrence: $0))
43-
}
32+
}
33+
34+
let components: [Component]
35+
36+
init(components: [Component]) {
37+
self.components = components
4438
}
4539

4640
package func matches(_ link: DocCSymbolLink) -> Bool {
@@ -55,73 +49,11 @@ package struct DocCSymbolInformation {
5549

5650
fileprivate typealias KindIdentifier = SymbolGraph.Symbol.KindIdentifier
5751

58-
extension SymbolOccurrence {
59-
var doccSymbolKind: String {
60-
switch symbol.kind {
61-
case .module:
62-
KindIdentifier.module.identifier
63-
case .namespace, .namespaceAlias:
64-
KindIdentifier.namespace.identifier
65-
case .macro:
66-
KindIdentifier.macro.identifier
67-
case .enum:
68-
KindIdentifier.enum.identifier
69-
case .struct:
70-
KindIdentifier.struct.identifier
71-
case .class:
72-
KindIdentifier.class.identifier
73-
case .protocol:
74-
KindIdentifier.protocol.identifier
75-
case .extension:
76-
KindIdentifier.extension.identifier
77-
case .union:
78-
KindIdentifier.union.identifier
79-
case .typealias:
80-
KindIdentifier.typealias.identifier
81-
case .function:
82-
KindIdentifier.func.identifier
83-
case .variable:
84-
KindIdentifier.var.identifier
85-
case .field:
86-
KindIdentifier.property.identifier
87-
case .enumConstant:
88-
KindIdentifier.case.identifier
89-
case .instanceMethod:
90-
KindIdentifier.func.identifier
91-
case .classMethod:
92-
KindIdentifier.func.identifier
93-
case .staticMethod:
94-
KindIdentifier.func.identifier
95-
case .instanceProperty:
96-
KindIdentifier.property.identifier
97-
case .classProperty, .staticProperty:
98-
KindIdentifier.typeProperty.identifier
99-
case .constructor:
100-
KindIdentifier.`init`.identifier
101-
case .destructor:
102-
KindIdentifier.deinit.identifier
103-
case .conversionFunction:
104-
KindIdentifier.func.identifier
105-
case .unknown, .using, .concept, .commentTag, .parameter:
106-
"unknown"
107-
}
108-
}
109-
}
110-
11152
extension LinkCompletionTools.SymbolInformation {
11253
init(fromModuleName moduleName: String) {
11354
self.init(
11455
kind: KindIdentifier.module.identifier,
11556
symbolIDHash: Self.hash(uniqueSymbolID: moduleName)
11657
)
11758
}
118-
119-
init(fromSymbolOccurrence occurrence: SymbolOccurrence) {
120-
self.init(
121-
kind: occurrence.doccSymbolKind,
122-
symbolIDHash: Self.hash(uniqueSymbolID: occurrence.symbol.usr),
123-
parameterTypes: nil,
124-
returnTypes: nil
125-
)
126-
}
12759
}

Sources/DocCDocumentation/IndexStoreDB+Extensions.swift

Lines changed: 56 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,65 +10,82 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import Foundation
1314
package import IndexStoreDB
1415
import SKLogging
1516
import SemanticIndex
16-
@_spi(LinkCompletion) import SwiftDocC
17+
@preconcurrency @_spi(LinkCompletion) import SwiftDocC
18+
import SwiftExtensions
19+
import SymbolKit
1720

1821
extension CheckedIndex {
1922
/// Find a `SymbolOccurrence` that is considered the primary definition of the symbol with the given `DocCSymbolLink`.
2023
///
2124
/// If the `DocCSymbolLink` has an ambiguous definition, the most important role of this function is to deterministically return
2225
/// the same result every time.
2326
package func primaryDefinitionOrDeclarationOccurrence(
24-
ofDocCSymbolLink symbolLink: DocCSymbolLink
25-
) -> SymbolOccurrence? {
26-
var components = symbolLink.components
27-
guard components.count > 0 else {
27+
ofDocCSymbolLink symbolLink: DocCSymbolLink,
28+
fetchSymbolGraph: @Sendable (_: SymbolLocation) async throws -> String?
29+
) async throws -> SymbolOccurrence? {
30+
guard !symbolLink.components.isEmpty else {
2831
return nil
2932
}
30-
// Do a lookup to find the top level symbol
31-
let topLevelSymbol = components.removeLast()
33+
// Find all occurrences of the symbol by name alone
34+
let topLevelSymbolName = symbolLink.components.last!.name
3235
var topLevelSymbolOccurrences: [SymbolOccurrence] = []
33-
forEachCanonicalSymbolOccurrence(byName: topLevelSymbol.name) { symbolOccurrence in
36+
forEachCanonicalSymbolOccurrence(byName: topLevelSymbolName) { symbolOccurrence in
3437
topLevelSymbolOccurrences.append(symbolOccurrence)
3538
return true // continue
3639
}
37-
topLevelSymbolOccurrences = topLevelSymbolOccurrences.filter {
38-
let symbolInformation = LinkCompletionTools.SymbolInformation(fromSymbolOccurrence: $0)
39-
return symbolInformation.matches(topLevelSymbol.disambiguation)
40-
}
41-
// Search each potential symbol's parents to find an exact match
42-
let symbolOccurences = topLevelSymbolOccurrences.filter { topLevelSymbolOccurrence in
43-
var components = components
44-
var symbolOccurrence = topLevelSymbolOccurrence
45-
while let nextComponent = components.popLast(), let parentSymbolOccurrence = symbolOccurrence.parent(self) {
46-
let parentSymbolInformation = LinkCompletionTools.SymbolInformation(
47-
fromSymbolOccurrence: parentSymbolOccurrence
48-
)
49-
guard parentSymbolOccurrence.symbol.name == nextComponent.name,
50-
parentSymbolInformation.matches(nextComponent.disambiguation)
51-
else {
52-
return false
53-
}
54-
symbolOccurrence = parentSymbolOccurrence
40+
// Determine which of the symbol occurrences actually matches the symbol link
41+
var result: [SymbolOccurrence] = []
42+
for occurrence in topLevelSymbolOccurrences {
43+
let info = try await doccSymbolInformation(ofUSR: occurrence.symbol.usr, fetchSymbolGraph: fetchSymbolGraph)
44+
if let info, info.matches(symbolLink) {
45+
result.append(occurrence)
5546
}
56-
// If we have exactly one component left, check to see if it's the module name
57-
if components.count == 1 {
58-
let lastComponent = components.removeLast()
59-
guard lastComponent.name == topLevelSymbolOccurrence.location.moduleName else {
60-
return false
61-
}
47+
}
48+
// Ensure that this is deterministic by sorting the results
49+
result.sort()
50+
if result.count > 1 {
51+
logger.debug("Multiple symbols found for DocC symbol link '\(symbolLink.linkString)'")
52+
}
53+
return result.first
54+
}
55+
56+
/// Find the DocCSymbolLink for a given symbol USR.
57+
///
58+
/// - Parameters:
59+
/// - usr: The symbol USR to find in the index.
60+
/// - fetchSymbolGraph: Callback that returns a SymbolGraph for a given SymbolLocation
61+
package func doccSymbolInformation(
62+
ofUSR usr: String,
63+
fetchSymbolGraph: (_: SymbolLocation) async throws -> String?
64+
) async throws -> DocCSymbolInformation? {
65+
guard let topLevelSymbolOccurrence = primaryDefinitionOrDeclarationOccurrence(ofUSR: usr) else {
66+
return nil
67+
}
68+
let moduleName = topLevelSymbolOccurrence.location.moduleName
69+
var symbols = [topLevelSymbolOccurrence]
70+
// Find any parent symbols
71+
var symbolOccurrence: SymbolOccurrence = topLevelSymbolOccurrence
72+
while let parentSymbolOccurrence = symbolOccurrence.parent(self) {
73+
symbols.insert(parentSymbolOccurrence, at: 0)
74+
symbolOccurrence = parentSymbolOccurrence
75+
}
76+
// Fetch symbol information from the symbol graph
77+
var components = [DocCSymbolInformation.Component(fromModuleName: moduleName)]
78+
for symbolOccurence in symbols {
79+
guard let rawSymbolGraph = try await fetchSymbolGraph(symbolOccurence.location) else {
80+
return nil
6281
}
63-
guard components.isEmpty else {
64-
return false
82+
let symbolGraph = try JSONDecoder().decode(SymbolGraph.self, from: Data(rawSymbolGraph.utf8))
83+
guard let symbol = symbolGraph.symbols[symbolOccurence.symbol.usr] else {
84+
return nil
6585
}
66-
return true
67-
}.sorted()
68-
if symbolOccurences.count > 1 {
69-
logger.debug("Multiple symbols found for DocC symbol link '\(symbolLink.linkString)'")
86+
components.append(DocCSymbolInformation.Component(fromSymbol: symbol))
7087
}
71-
return symbolOccurences.first
88+
return DocCSymbolInformation(components: components)
7289
}
7390
}
7491

Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,30 @@ extension DocumentationLanguageService {
5959
throw ResponseError.requestFailed(doccDocumentationError: .indexNotAvailable)
6060
}
6161
guard let symbolLink = DocCSymbolLink(linkString: symbolName),
62-
let symbolOccurrence = index.primaryDefinitionOrDeclarationOccurrence(ofDocCSymbolLink: symbolLink)
62+
let symbolOccurrence = try await index.primaryDefinitionOrDeclarationOccurrence(
63+
ofDocCSymbolLink: symbolLink,
64+
fetchSymbolGraph: { location in
65+
guard let symbolWorkspace = try await workspaceForDocument(uri: location.documentUri),
66+
let languageService = try await languageService(for: location.documentUri, .swift, in: symbolWorkspace)
67+
as? SwiftLanguageService
68+
else {
69+
throw ResponseError.internalError("Unable to find Swift language service for \(location.documentUri)")
70+
}
71+
return try await languageService.withSnapshotFromDiskOpenedInSourcekitd(
72+
uri: location.documentUri,
73+
fallbackSettingsAfterTimeout: false
74+
) {
75+
(snapshot, compileCommand) in
76+
let (_, _, symbolGraph) = try await languageService.cursorInfo(
77+
snapshot,
78+
compileCommand: compileCommand,
79+
Range(snapshot.position(of: location)),
80+
includeSymbolGraph: true
81+
)
82+
return symbolGraph
83+
}
84+
}
85+
)
6386
else {
6487
throw ResponseError.requestFailed(doccDocumentationError: .symbolNotFound(symbolName))
6588
}

Sources/SourceKitLSP/Swift/DoccDocumentation.swift

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import BuildSystemIntegration
1515
import DocCDocumentation
1616
import Foundation
17+
import IndexStoreDB
1718
package import LanguageServerProtocol
1819
import SemanticIndex
1920
import SKLogging
@@ -73,7 +74,22 @@ extension SwiftLanguageService {
7374
workspace: workspace,
7475
documentationManager: documentationManager,
7576
catalogURL: catalogURL,
76-
for: symbolUSR
77+
for: symbolUSR,
78+
fetchSymbolGraph: { symbolLocation in
79+
try await withSnapshotFromDiskOpenedInSourcekitd(
80+
uri: symbolLocation.documentUri,
81+
fallbackSettingsAfterTimeout: false
82+
) {
83+
(snapshot, compileCommand) in
84+
let (_, _, symbolGraph) = try await self.cursorInfo(
85+
snapshot,
86+
compileCommand: compileCommand,
87+
Range(snapshot.position(of: symbolLocation)),
88+
includeSymbolGraph: true
89+
)
90+
return symbolGraph
91+
}
92+
}
7793
)
7894
}
7995
return try await documentationManager.renderDocCDocumentation(
@@ -90,14 +106,18 @@ extension SwiftLanguageService {
90106
workspace: Workspace,
91107
documentationManager: DocCDocumentationManager,
92108
catalogURL: URL?,
93-
for symbolUSR: String
109+
for symbolUSR: String,
110+
fetchSymbolGraph: @Sendable (_: SymbolLocation) async throws -> String?
94111
) async throws -> String? {
95112
guard let catalogURL else {
96113
return nil
97114
}
98115
let catalogIndex = try await documentationManager.catalogIndex(for: catalogURL)
99116
guard let index = workspace.index(checkedFor: .deletedFiles),
100-
let symbolInformation = DocCSymbolInformation(fromUSR: symbolUSR, in: index),
117+
let symbolInformation = try await index.doccSymbolInformation(
118+
ofUSR: symbolUSR,
119+
fetchSymbolGraph: fetchSymbolGraph
120+
),
101121
let markupExtensionFileURL = catalogIndex.documentationExtension(for: symbolInformation)
102122
else {
103123
return nil
@@ -140,7 +160,10 @@ fileprivate struct DocumentableSymbol {
140160
} else if let functionDecl = node.as(FunctionDeclSyntax.self) {
141161
self = DocumentableSymbol(node: functionDecl, position: functionDecl.name.positionAfterSkippingLeadingTrivia)
142162
} else if let subscriptDecl = node.as(SubscriptDeclSyntax.self) {
143-
self = DocumentableSymbol(node: subscriptDecl, position: subscriptDecl.positionAfterSkippingLeadingTrivia)
163+
self = DocumentableSymbol(
164+
node: subscriptDecl.subscriptKeyword,
165+
position: subscriptDecl.subscriptKeyword.positionAfterSkippingLeadingTrivia
166+
)
144167
} else if let variableDecl = node.as(VariableDeclSyntax.self) {
145168
guard let identifier = variableDecl.bindings.only?.pattern.as(IdentifierPatternSyntax.self) else {
146169
return nil

0 commit comments

Comments
 (0)