Skip to content

Commit cc50129

Browse files
Merge pull request #2216 from matthewbastien/docc-func-disambiguation
[DocC Live Preview] Support parameter and return type disambiguations
2 parents 45534b0 + b09c7d4 commit cc50129

File tree

5 files changed

+315
-133
lines changed

5 files changed

+315
-133
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: 73 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,65 +10,98 @@
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 {
28-
return nil
27+
ofDocCSymbolLink symbolLink: DocCSymbolLink,
28+
fetchSymbolGraph: @Sendable (SymbolLocation) async throws -> String?
29+
) async throws -> SymbolOccurrence? {
30+
guard let topLevelSymbolName = symbolLink.components.last?.name else {
31+
throw DocCCheckedIndexError.emptyDocCSymbolLink
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
3234
var topLevelSymbolOccurrences: [SymbolOccurrence] = []
33-
forEachCanonicalSymbolOccurrence(byName: topLevelSymbol.name) { symbolOccurrence in
35+
forEachCanonicalSymbolOccurrence(byName: topLevelSymbolName) { symbolOccurrence in
3436
topLevelSymbolOccurrences.append(symbolOccurrence)
3537
return true // continue
3638
}
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
39+
// Determine which of the symbol occurrences actually matches the symbol link
40+
var result: [SymbolOccurrence] = []
41+
for occurrence in topLevelSymbolOccurrences {
42+
let info = try await doccSymbolInformation(ofUSR: occurrence.symbol.usr, fetchSymbolGraph: fetchSymbolGraph)
43+
if let info, info.matches(symbolLink) {
44+
result.append(occurrence)
5545
}
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-
}
46+
}
47+
// Ensure that this is deterministic by sorting the results
48+
result.sort()
49+
if result.count > 1 {
50+
logger.debug("Multiple symbols found for DocC symbol link '\(symbolLink.linkString)'")
51+
}
52+
return result.first
53+
}
54+
55+
/// Find the DocCSymbolLink for a given symbol USR.
56+
///
57+
/// - Parameters:
58+
/// - usr: The symbol USR to find in the index.
59+
/// - fetchSymbolGraph: Callback that returns a SymbolGraph for a given SymbolLocation
60+
package func doccSymbolInformation(
61+
ofUSR usr: String,
62+
fetchSymbolGraph: (SymbolLocation) async throws -> String?
63+
) async throws -> DocCSymbolInformation? {
64+
guard let topLevelSymbolOccurrence = primaryDefinitionOrDeclarationOccurrence(ofUSR: usr) else {
65+
return nil
66+
}
67+
let moduleName = topLevelSymbolOccurrence.location.moduleName
68+
var symbols = [topLevelSymbolOccurrence]
69+
// Find any parent symbols
70+
var symbolOccurrence: SymbolOccurrence = topLevelSymbolOccurrence
71+
while let parentSymbolOccurrence = symbolOccurrence.parent(self) {
72+
symbols.insert(parentSymbolOccurrence, at: 0)
73+
symbolOccurrence = parentSymbolOccurrence
74+
}
75+
// Fetch symbol information from the symbol graph
76+
var components = [DocCSymbolInformation.Component(fromModuleName: moduleName)]
77+
for symbolOccurence in symbols {
78+
guard let rawSymbolGraph = try await fetchSymbolGraph(symbolOccurence.location) else {
79+
throw DocCCheckedIndexError.noSymbolGraph(symbolOccurence.symbol.usr)
6280
}
63-
guard components.isEmpty else {
64-
return false
81+
let symbolGraph = try JSONDecoder().decode(SymbolGraph.self, from: Data(rawSymbolGraph.utf8))
82+
guard let symbol = symbolGraph.symbols[symbolOccurence.symbol.usr] else {
83+
throw DocCCheckedIndexError.symbolNotFound(symbolOccurence.symbol.usr)
6584
}
66-
return true
67-
}.sorted()
68-
if symbolOccurences.count > 1 {
69-
logger.debug("Multiple symbols found for DocC symbol link '\(symbolLink.linkString)'")
85+
components.append(DocCSymbolInformation.Component(fromSymbol: symbol))
86+
}
87+
return DocCSymbolInformation(components: components)
88+
}
89+
}
90+
91+
enum DocCCheckedIndexError: LocalizedError {
92+
case emptyDocCSymbolLink
93+
case noSymbolGraph(String)
94+
case symbolNotFound(String)
95+
96+
var errorDescription: String? {
97+
switch self {
98+
case .emptyDocCSymbolLink:
99+
"The provided DocCSymbolLink was empty and could not be resolved"
100+
case .noSymbolGraph(let usr):
101+
"Unable to locate symbol graph for \(usr)"
102+
case .symbolNotFound(let usr):
103+
"Symbol \(usr) was not found in its symbol graph"
70104
}
71-
return symbolOccurences.first
72105
}
73106
}
74107

Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,29 @@ 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+
) { (snapshot, compileCommand) in
75+
let (_, _, symbolGraph) = try await languageService.cursorInfo(
76+
snapshot,
77+
compileCommand: compileCommand,
78+
Range(snapshot.position(of: location)),
79+
includeSymbolGraph: true
80+
)
81+
return symbolGraph
82+
}
83+
}
84+
)
6385
else {
6486
throw ResponseError.requestFailed(doccDocumentationError: .symbolNotFound(symbolName))
6587
}

Sources/SourceKitLSP/Swift/DoccDocumentation.swift

Lines changed: 26 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,21 @@ 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+
) { (snapshot, compileCommand) in
83+
let (_, _, symbolGraph) = try await self.cursorInfo(
84+
snapshot,
85+
compileCommand: compileCommand,
86+
Range(snapshot.position(of: symbolLocation)),
87+
includeSymbolGraph: true
88+
)
89+
return symbolGraph
90+
}
91+
}
7792
)
7893
}
7994
return try await documentationManager.renderDocCDocumentation(
@@ -90,14 +105,18 @@ extension SwiftLanguageService {
90105
workspace: Workspace,
91106
documentationManager: DocCDocumentationManager,
92107
catalogURL: URL?,
93-
for symbolUSR: String
108+
for symbolUSR: String,
109+
fetchSymbolGraph: @Sendable (SymbolLocation) async throws -> String?
94110
) async throws -> String? {
95111
guard let catalogURL else {
96112
return nil
97113
}
98114
let catalogIndex = try await documentationManager.catalogIndex(for: catalogURL)
99115
guard let index = workspace.index(checkedFor: .deletedFiles),
100-
let symbolInformation = DocCSymbolInformation(fromUSR: symbolUSR, in: index),
116+
let symbolInformation = try await index.doccSymbolInformation(
117+
ofUSR: symbolUSR,
118+
fetchSymbolGraph: fetchSymbolGraph
119+
),
101120
let markupExtensionFileURL = catalogIndex.documentationExtension(for: symbolInformation)
102121
else {
103122
return nil
@@ -140,7 +159,10 @@ fileprivate struct DocumentableSymbol {
140159
} else if let functionDecl = node.as(FunctionDeclSyntax.self) {
141160
self = DocumentableSymbol(node: functionDecl, position: functionDecl.name.positionAfterSkippingLeadingTrivia)
142161
} else if let subscriptDecl = node.as(SubscriptDeclSyntax.self) {
143-
self = DocumentableSymbol(node: subscriptDecl, position: subscriptDecl.positionAfterSkippingLeadingTrivia)
162+
self = DocumentableSymbol(
163+
node: subscriptDecl.subscriptKeyword,
164+
position: subscriptDecl.subscriptKeyword.positionAfterSkippingLeadingTrivia
165+
)
144166
} else if let variableDecl = node.as(VariableDeclSyntax.self) {
145167
guard let identifier = variableDecl.bindings.only?.pattern.as(IdentifierPatternSyntax.self) else {
146168
return nil

0 commit comments

Comments
 (0)