Skip to content

Commit 28c03c5

Browse files
authored
Add new functions for working with link completion in editor integrations (#1129)
* Add new functions for completing links in editor integrations rdar://141689095 * Deprecate `DocCSymbolRepresentable`, `AbsoluteSymbolLink`, and related API * Address code review feedback: - Iterate over identifiers instead of over disambiguations - Add test to verify that order of parameters matter - Use optional chaining instead of optional `map`.
1 parent 890a58d commit 28c03c5

File tree

11 files changed

+425
-22
lines changed

11 files changed

+425
-22
lines changed

Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/AbsoluteSymbolLink.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import SymbolKit
1414
/// An absolute link to a symbol.
1515
///
1616
/// You can use this model to validate a symbol link and access its different parts.
17+
@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
1718
public struct AbsoluteSymbolLink: CustomStringConvertible {
1819
/// The identifier for the documentation bundle this link is from.
1920
public let bundleID: String
@@ -130,8 +131,10 @@ public struct AbsoluteSymbolLink: CustomStringConvertible {
130131
}
131132
}
132133

134+
@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
133135
extension AbsoluteSymbolLink {
134136
/// A component of a symbol link.
137+
@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
135138
public struct LinkComponent: CustomStringConvertible {
136139
/// The name of the symbol represented by the link component.
137140
public let name: String
@@ -207,6 +210,7 @@ extension AbsoluteSymbolLink {
207210
}
208211
}
209212

213+
@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
210214
extension AbsoluteSymbolLink.LinkComponent {
211215
/// A suffix attached to a documentation link to disambiguate it from other symbols
212216
/// that share the same base name.

Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/DocCSymbolRepresentable.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Foundation
1212
import SymbolKit
1313

1414
/// A type that can be converted to a DocC symbol.
15+
@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
1516
public protocol DocCSymbolRepresentable: Equatable {
1617
/// A namespaced, unique identifier for the kind of symbol.
1718
///
@@ -31,6 +32,7 @@ public protocol DocCSymbolRepresentable: Equatable {
3132
var title: String { get }
3233
}
3334

35+
@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
3436
public extension DocCSymbolRepresentable {
3537
/// The given symbol information as a symbol link component.
3638
///
@@ -49,6 +51,7 @@ public extension DocCSymbolRepresentable {
4951
}
5052
}
5153

54+
@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
5255
extension AbsoluteSymbolLink.LinkComponent {
5356
/// Given an array of symbols that are overloads for the symbol represented
5457
/// by this link component, returns those that are precisely identified by the component.
@@ -135,6 +138,7 @@ extension AbsoluteSymbolLink.LinkComponent {
135138
}
136139
}
137140

141+
@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
138142
public extension Collection where Element: DocCSymbolRepresentable {
139143
/// Given a collection of colliding symbols, returns the disambiguation suffix required
140144
/// for each symbol to disambiguate it from the others in the collection.
@@ -173,6 +177,7 @@ extension SymbolGraph.Symbol: @retroactive Equatable {}
173177
extension UnifiedSymbolGraph.Symbol: @retroactive Equatable {}
174178
#endif
175179

180+
@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
176181
extension SymbolGraph.Symbol: DocCSymbolRepresentable {
177182
public var preciseIdentifier: String? {
178183
self.identifier.precise
@@ -191,6 +196,7 @@ extension SymbolGraph.Symbol: DocCSymbolRepresentable {
191196
}
192197
}
193198

199+
@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
194200
extension UnifiedSymbolGraph.Symbol: DocCSymbolRepresentable {
195201
public var preciseIdentifier: String? {
196202
self.uniqueIdentifier
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
13+
/// A collection of API for link completion.
14+
///
15+
/// An example link completion workflow could look something like this;
16+
/// Assume that there's already an partial link in progress: `First/Second-enum/`
17+
///
18+
/// - First, parse the link into link components using ``parse(linkString:)``.
19+
/// - Second, narrow down the possible symbols to suggest as completion using ``SymbolInformation/matches(_:)``
20+
/// - Third, determine the minimal unique disambiguation for each completion suggestion using ``suggestedDisambiguation(forCollidingSymbols:)``
21+
///
22+
/// > Tip: You can use ``SymbolInformation/hash(uniqueSymbolID:)`` to compute the hashed symbol identifiers needed for steps 2 and 3 above.
23+
@_spi(LinkCompletion) // LinkCompletionTools isn't stable API yet
24+
public enum LinkCompletionTools {
25+
26+
// MARK: Parsing
27+
28+
/// Parses link string into link components; each consisting of a base name and a disambiguation suffix.
29+
///
30+
/// - Parameter linkString: The link string to parse.
31+
/// - Returns: A list of link components, each consisting of a base name and a disambiguation suffix.
32+
public static func parse(linkString: String) -> [(name: String, disambiguation: ParsedDisambiguation)] {
33+
PathHierarchy.PathParser.parse(path: linkString).components.map { pathComponent in
34+
(name: String(pathComponent.name), disambiguation: ParsedDisambiguation(pathComponent.disambiguation) )
35+
}
36+
}
37+
38+
/// A disambiguation suffix for a parsed link component.
39+
public enum ParsedDisambiguation: Equatable {
40+
/// This link component isn't disambiguated.
41+
case none
42+
43+
/// This path component uses a combination of kind and hash disambiguation.
44+
///
45+
/// At least one of `kind` and `hash` will be non-`nil`.
46+
/// It's never _necessary_ to specify both a `kind` and a `hash` to disambiguate a link component, but it's supported for the developer to include both.
47+
case kindAndOrHash(kind: String?, hash: String?)
48+
49+
/// This path component uses type signature information for disambiguation.
50+
///
51+
/// At least one of `parameterTypes` and `returnTypes` will be non-`nil`.
52+
case typeSignature(parameterTypes: [String]?, returnTypes: [String]?)
53+
54+
// This empty-marker case is here because non-frozen enums are only available when Library Evolution is enabled,
55+
// which is not available to Swift Packages without unsafe flags (rdar://78773361).
56+
// This can be removed once that is available and applied to Swift-DocC (rdar://89033233).
57+
@available(*, deprecated, message: "this enum is non-frozen and may be expanded in the future; add a `default` case instead of matching this one")
58+
case _nonFrozenEnum_useDefaultCase
59+
60+
init(_ disambiguation: PathHierarchy.PathComponent.Disambiguation?) {
61+
// This initializer is intended to be internal-only.
62+
switch disambiguation {
63+
case .kindAndHash(let kind, let hash):
64+
self = .kindAndOrHash(
65+
kind: kind.map { String($0) },
66+
hash: hash.map { String($0) }
67+
)
68+
case .typeSignature(let parameterTypes, let returnTypes):
69+
self = .typeSignature(
70+
parameterTypes: parameterTypes?.map { String($0) },
71+
returnTypes: returnTypes?.map { String($0) }
72+
)
73+
case nil:
74+
self = .none
75+
}
76+
}
77+
}
78+
79+
/// Suggests the minimal most readable disambiguation string for each symbol with the same name.
80+
/// - Parameters:
81+
/// - collidingSymbols: A list of symbols that all have the same name.
82+
/// - Returns: A collection of disambiguation strings in the same order as the provided symbol information.
83+
///
84+
/// - Important: It's the callers responsibility to create symbol information that matches what the compilers emit in symbol graph files.
85+
/// If there are mismatches, DocC may suggest disambiguation that won't resolve with the real compiler emitted symbol data.
86+
public static func suggestedDisambiguation(forCollidingSymbols collidingSymbols: [SymbolInformation]) -> [String] {
87+
// Track the order of the symbols so that the disambiguations can be ordered to align with their respective symbols.
88+
var identifiersInOrder: [ResolvedIdentifier] = []
89+
identifiersInOrder.reserveCapacity(collidingSymbols.count)
90+
91+
// Construct a disambiguation container with all the symbol's information.
92+
var disambiguationContainer = PathHierarchy.DisambiguationContainer()
93+
for symbol in collidingSymbols {
94+
let (node, identifier) = Self._makeNodeAndIdentifier(name: "unused")
95+
identifiersInOrder.append(identifier)
96+
97+
disambiguationContainer.add(
98+
node,
99+
kind: symbol.kind,
100+
hash: symbol.symbolIDHash,
101+
parameterTypes: symbol.parameterTypes,
102+
returnTypes: symbol.returnTypes
103+
)
104+
}
105+
106+
let disambiguatedValues = disambiguationContainer.disambiguatedValues()
107+
// Compute the minimal suggested disambiguation for each symbol and return their string suffixes in the original symbol's order.
108+
return identifiersInOrder.map { identifier in
109+
guard let (_, disambiguation) = disambiguatedValues.first(where: { $0.value.identifier == identifier }) else {
110+
fatalError("Each node in the `DisambiguationContainer` should always have a entry in the `disambiguatedValues`")
111+
}
112+
return disambiguation.makeSuffix()
113+
}
114+
}
115+
116+
/// Information about a symbol for link completion purposes.
117+
///
118+
/// > Note:
119+
/// > This symbol information doesn't include the name.
120+
/// > It's the callers responsibility to group symbols by their name.
121+
///
122+
/// > Important:
123+
/// > It's the callers responsibility to create symbol information that matches what the compilers emit in symbol graph files.
124+
/// > If there are mismatches, DocC may suggest disambiguation that won't resolve with the real compiler emitted symbol data.
125+
public struct SymbolInformation {
126+
/// The kind of symbol, for example `"class"` or `"func.op`.
127+
///
128+
/// ## See Also
129+
/// - ``/SymbolKit/SymbolGraph/Symbol/KindIdentifier``
130+
public var kind: String
131+
/// A hash of the symbol's unique identifier.
132+
///
133+
/// ## See Also
134+
/// - ``hash(uniqueSymbolID:)``
135+
public var symbolIDHash: String
136+
/// The type names of this symbol's parameters, or `nil` if this symbol has no function signature information.
137+
///
138+
/// A function without parameters represents i
139+
public var parameterTypes: [String]?
140+
/// The type names of this symbol's return value, or `nil` if this symbol has no function signature information.
141+
public var returnTypes: [String]?
142+
143+
public init(
144+
kind: String,
145+
symbolIDHash: String,
146+
parameterTypes: [String]? = nil,
147+
returnTypes: [String]? = nil
148+
) {
149+
self.kind = kind
150+
self.symbolIDHash = symbolIDHash
151+
self.parameterTypes = parameterTypes
152+
self.returnTypes = returnTypes
153+
}
154+
155+
/// Creates a hashed representation of a symbol's unique identifier.
156+
///
157+
/// # See Also
158+
/// - ``symbolIDHash``
159+
public static func hash(uniqueSymbolID: String) -> String {
160+
uniqueSymbolID.stableHashString
161+
}
162+
163+
// MARK: Filtering
164+
165+
/// Returns a Boolean value that indicates whether this symbol information matches the parsed disambiguation from one of the link components of a parsed link string.
166+
public func matches(_ parsedDisambiguation: LinkCompletionTools.ParsedDisambiguation) -> Bool {
167+
guard let disambiguation = PathHierarchy.PathComponent.Disambiguation(parsedDisambiguation) else {
168+
return true // No disambiguation to match against.
169+
}
170+
171+
var disambiguationContainer = PathHierarchy.DisambiguationContainer()
172+
let (node, _) = LinkCompletionTools._makeNodeAndIdentifier(name: "unused")
173+
174+
disambiguationContainer.add(
175+
node,
176+
kind: self.kind,
177+
hash: self.symbolIDHash,
178+
parameterTypes: self.parameterTypes,
179+
returnTypes: self.returnTypes
180+
)
181+
182+
do {
183+
return try disambiguationContainer.find(disambiguation) != nil
184+
} catch {
185+
return false
186+
}
187+
}
188+
}
189+
}
190+
191+
private extension PathHierarchy.PathComponent.Disambiguation {
192+
init?(_ parsedDisambiguation: LinkCompletionTools.ParsedDisambiguation) {
193+
switch parsedDisambiguation {
194+
case .kindAndOrHash(let kind, let hash):
195+
self = .kindAndHash(kind: kind.map { $0[...] }, hash: hash.map { $0[...] })
196+
197+
case .typeSignature(let parameterTypes, let returnTypes):
198+
self = .typeSignature(parameterTypes: parameterTypes?.map { $0[...] }, returnTypes: returnTypes?.map { $0[...] })
199+
200+
// Since this is within DocC we want to have an error if we don't handle new future cases.
201+
case .none, ._nonFrozenEnum_useDefaultCase:
202+
return nil
203+
}
204+
}
205+
}

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1577,14 +1577,14 @@ public class DocumentationContext {
15771577
{
15781578
switch (source.kind, target.kind) {
15791579
case (.dictionaryKey, .dictionary):
1580-
let dictionaryKey = DictionaryKey(name: sourceSymbol.title, contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf))
1580+
let dictionaryKey = DictionaryKey(name: sourceSymbol.names.title, contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf))
15811581
if keysByTarget[edge.target] == nil {
15821582
keysByTarget[edge.target] = [dictionaryKey]
15831583
} else {
15841584
keysByTarget[edge.target]?.append(dictionaryKey)
15851585
}
15861586
case (.httpParameter, .httpRequest):
1587-
let parameter = HTTPParameter(name: sourceSymbol.title, source: (sourceSymbol.httpParameterSource ?? "query"), contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf))
1587+
let parameter = HTTPParameter(name: sourceSymbol.names.title, source: (sourceSymbol.httpParameterSource ?? "query"), contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf))
15881588
if parametersByTarget[edge.target] == nil {
15891589
parametersByTarget[edge.target] = [parameter]
15901590
} else {
@@ -1594,14 +1594,14 @@ public class DocumentationContext {
15941594
let body = HTTPBody(mediaType: sourceSymbol.httpMediaType, contents: [], symbol: sourceSymbol)
15951595
bodyByTarget[edge.target] = body
15961596
case (.httpParameter, .httpBody):
1597-
let parameter = HTTPParameter(name: sourceSymbol.title, source: "body", contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf))
1597+
let parameter = HTTPParameter(name: sourceSymbol.names.title, source: "body", contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf))
15981598
if bodyParametersByTarget[edge.target] == nil {
15991599
bodyParametersByTarget[edge.target] = [parameter]
16001600
} else {
16011601
bodyParametersByTarget[edge.target]?.append(parameter)
16021602
}
16031603
case (.httpResponse, .httpRequest):
1604-
let statusParts = sourceSymbol.title.split(separator: " ", maxSplits: 1)
1604+
let statusParts = sourceSymbol.names.title.split(separator: " ", maxSplits: 1)
16051605
let statusCode = UInt(statusParts[0]) ?? 0
16061606
let reason = statusParts.count > 1 ? String(statusParts[1]) : nil
16071607
let response = HTTPResponse(statusCode: statusCode, reason: reason, mediaType: sourceSymbol.httpMediaType, contents: [], symbol: sourceSymbol)
@@ -1649,7 +1649,7 @@ public class DocumentationContext {
16491649
if let semantic = target?.semantic as? Symbol {
16501650
// Add any body parameters to existing body record
16511651
var localBody = body
1652-
if let identifier = body.symbol?.preciseIdentifier, let bodyParameters = bodyParametersByTarget[identifier] {
1652+
if let identifier = body.symbol?.identifier.precise, let bodyParameters = bodyParametersByTarget[identifier] {
16531653
localBody.parameters = bodyParameters.sorted(by: \.name)
16541654
}
16551655
if semantic.httpBodySection == nil {

Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,3 +806,21 @@ private extension SymbolGraph.Relationship.Kind {
806806
}
807807
}
808808
}
809+
810+
// MARK: Link completion
811+
812+
// This extension can't be defined in another file because it uses file-private API.
813+
extension LinkCompletionTools {
814+
/// Creates a new path hierarchy node for link completion purposes.
815+
///
816+
/// Use these nodes to compute disambiguation and match against parsed link components.
817+
///
818+
/// - Important: The nodes and identifier are only intended for link completion purposes. _Don't_ add them to the path hierarchy or try and resolve links for them.
819+
static func _makeNodeAndIdentifier(name: String) -> (PathHierarchy.Node, ResolvedIdentifier) {
820+
let node = PathHierarchy.Node(name: name)
821+
let id = ResolvedIdentifier()
822+
823+
node.identifier = id
824+
return (node, id)
825+
}
826+
}

Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypeFormatTransformation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ extension ExtendedTypeFormatTransformation {
491491
relationships.append(.init(source: symbol.identifier.precise,
492492
target: parent.identifier.precise,
493493
kind: .inContextOf,
494-
targetFallback: parent.title))
494+
targetFallback: parent.names.title))
495495
symbolIsConnectedToParent[symbol.identifier.precise] = true
496496
}
497497

0 commit comments

Comments
 (0)