Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new functions for working with link completion in editor integrations #1129

Merged
merged 3 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import SymbolKit
/// An absolute link to a symbol.
///
/// You can use this model to validate a symbol link and access its different parts.
@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
public struct AbsoluteSymbolLink: CustomStringConvertible {
/// The identifier for the documentation bundle this link is from.
public let bundleID: String
Expand Down Expand Up @@ -130,8 +131,10 @@ public struct AbsoluteSymbolLink: CustomStringConvertible {
}
}

@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
extension AbsoluteSymbolLink {
/// A component of a symbol link.
@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
public struct LinkComponent: CustomStringConvertible {
/// The name of the symbol represented by the link component.
public let name: String
Expand Down Expand Up @@ -207,6 +210,7 @@ extension AbsoluteSymbolLink {
}
}

@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
extension AbsoluteSymbolLink.LinkComponent {
/// A suffix attached to a documentation link to disambiguate it from other symbols
/// that share the same base name.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Foundation
import SymbolKit

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

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

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

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

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

@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
extension UnifiedSymbolGraph.Symbol: DocCSymbolRepresentable {
public var preciseIdentifier: String? {
self.uniqueIdentifier
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2024 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation

/// A collection of API for link completion.
///
/// An example link completion workflow could look something like this;
/// Assume that there's already an partial link in progress: `First/Second-enum/`
///
/// - First, parse the link into link components using ``parse(linkString:)``.
/// - Second, narrow down the possible symbols to suggest as completion using ``SymbolInformation/matches(_:)``
/// - Third, determine the minimal unique disambiguation for each completion suggestion using ``suggestedDisambiguation(forCollidingSymbols:)``
///
/// > Tip: You can use ``SymbolInformation/hash(uniqueSymbolID:)`` to compute the hashed symbol identifiers needed for steps 2 and 3 above.
@_spi(LinkCompletion) // LinkCompletionTools isn't stable API yet
public enum LinkCompletionTools {

// MARK: Parsing

/// Parses link string into link components; each consisting of a base name and a disambiguation suffix.
///
/// - Parameter linkString: The link string to parse.
/// - Returns: A list of link components, each consisting of a base name and a disambiguation suffix.
public static func parse(linkString: String) -> [(name: String, disambiguation: ParsedDisambiguation)] {
PathHierarchy.PathParser.parse(path: linkString).components.map { pathComponent in
(name: String(pathComponent.name), disambiguation: ParsedDisambiguation(pathComponent.disambiguation) )
}
}

/// A disambiguation suffix for a parsed link component.
public enum ParsedDisambiguation: Equatable {
/// This link component isn't disambiguated.
case none

/// This path component uses a combination of kind and hash disambiguation.
///
/// At least one of `kind` and `hash` will be non-`nil`.
/// 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.
case kindAndOrHash(kind: String?, hash: String?)

/// This path component uses type signature information for disambiguation.
///
/// At least one of `parameterTypes` and `returnTypes` will be non-`nil`.
case typeSignature(parameterTypes: [String]?, returnTypes: [String]?)

// This empty-marker case is here because non-frozen enums are only available when Library Evolution is enabled,
// which is not available to Swift Packages without unsafe flags (rdar://78773361).
// This can be removed once that is available and applied to Swift-DocC (rdar://89033233).
@available(*, deprecated, message: "this enum is non-frozen and may be expanded in the future; add a `default` case instead of matching this one")
case _nonFrozenEnum_useDefaultCase

init(_ disambiguation: PathHierarchy.PathComponent.Disambiguation?) {
// This initializer is intended to be internal-only.
switch disambiguation {
case .kindAndHash(let kind, let hash):
self = .kindAndOrHash(
kind: kind.map { String($0) },
hash: hash.map { String($0) }
)
case .typeSignature(let parameterTypes, let returnTypes):
self = .typeSignature(
parameterTypes: parameterTypes.map { typeNames in typeNames.map { String($0) }},
d-ronnqvist marked this conversation as resolved.
Show resolved Hide resolved
returnTypes: returnTypes.map { typeNames in typeNames.map { String($0) }}
)
case nil:
self = .none
}
}
}

/// Suggests the minimal most readable disambiguation string for each symbol with the same name.
/// - Parameters:
/// - collidingSymbols: A list of symbols that all have the same name.
/// - Returns: A collection of disambiguation strings in the same order as the provided symbol information.
///
/// - Important: It's the callers responsibility to create symbol information that matches what the compilers emit in symbol graph files.
/// If there are mismatches, DocC may suggest disambiguation that won't resolve with the real compiler emitted symbol data.
public static func suggestedDisambiguation(forCollidingSymbols collidingSymbols: [SymbolInformation]) -> [String] {
// Track the order of the symbols so that the disambiguations can be ordered to align with their respective symbols.
var identifiersInOrder: [ResolvedIdentifier] = []
identifiersInOrder.reserveCapacity(collidingSymbols.count)

// Construct a disambiguation container with all the symbol's information.
var disambiguationContainer = PathHierarchy.DisambiguationContainer()
for symbol in collidingSymbols {
let (node, identifier) = Self._makeNodeAndIdentifier(name: "unused")
identifiersInOrder.append(identifier)

disambiguationContainer.add(
node,
kind: symbol.kind,
hash: symbol.symbolIDHash,
parameterTypes: symbol.parameterTypes,
returnTypes: symbol.returnTypes
)
}

// Compute the minimal suggested disambiguation for each symbol and return their string suffixes in the original symbol's order.
var disambiguationsInOrder = [String](repeating: "", count: collidingSymbols.count)
for (node, disambiguation) in disambiguationContainer.disambiguatedValues() {
guard let index = identifiersInOrder.firstIndex(of: node.identifier) else {
d-ronnqvist marked this conversation as resolved.
Show resolved Hide resolved
fatalError("Each node in the `DisambiguationContainer` should always have a entry in the `disambiguatedValues`")
}
disambiguationsInOrder[index] = disambiguation.makeSuffix()
}

return disambiguationsInOrder
}

/// Information about a symbol for link completion purposes.
///
/// > Note:
/// > This symbol information doesn't include the name.
/// > It's the callers responsibility to group symbols by their name.
///
/// > Important:
/// > It's the callers responsibility to create symbol information that matches what the compilers emit in symbol graph files.
/// > If there are mismatches, DocC may suggest disambiguation that won't resolve with the real compiler emitted symbol data.
public struct SymbolInformation {
/// The kind of symbol, for example `"class"` or `"func.op`.
///
/// ## See Also
/// - ``/SymbolKit/SymbolGraph/Symbol/KindIdentifier``
public var kind: String
/// A hash of the symbol's unique identifier.
///
/// ## See Also
/// - ``hash(uniqueSymbolID:)``
public var symbolIDHash: String
/// The type names of this symbol's parameters, or `nil` if this symbol has no function signature information.
///
/// A function without parameters represents i
public var parameterTypes: [String]?
/// The type names of this symbol's return value, or `nil` if this symbol has no function signature information.
public var returnTypes: [String]?

public init(
kind: String,
symbolIDHash: String,
parameterTypes: [String]? = nil,
returnTypes: [String]? = nil
) {
self.kind = kind
self.symbolIDHash = symbolIDHash
self.parameterTypes = parameterTypes
self.returnTypes = returnTypes
}

/// Creates a hashed representation of a symbol's unique identifier.
///
/// # See Also
/// - ``symbolIDHash``
public static func hash(uniqueSymbolID: String) -> String {
uniqueSymbolID.stableHashString
}

// MARK: Filtering

/// 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.
public func matches(_ parsedDisambiguation: LinkCompletionTools.ParsedDisambiguation) -> Bool {
guard let disambiguation = PathHierarchy.PathComponent.Disambiguation(parsedDisambiguation) else {
return true // No disambiguation to match against.
}

var disambiguationContainer = PathHierarchy.DisambiguationContainer()
let (node, _) = LinkCompletionTools._makeNodeAndIdentifier(name: "unused")

disambiguationContainer.add(
node,
kind: self.kind,
hash: self.symbolIDHash,
parameterTypes: self.parameterTypes,
returnTypes: self.returnTypes
)

do {
return try disambiguationContainer.find(disambiguation) != nil
} catch {
return false
}
}
}
}

private extension PathHierarchy.PathComponent.Disambiguation {
init?(_ parsedDisambiguation: LinkCompletionTools.ParsedDisambiguation) {
switch parsedDisambiguation {
case .kindAndOrHash(let kind, let hash):
self = .kindAndHash(kind: kind.map { $0[...] }, hash: hash.map { $0[...] })

case .typeSignature(let parameterTypes, let returnTypes):
self = .typeSignature(parameterTypes: parameterTypes?.map { $0[...] }, returnTypes: returnTypes?.map { $0[...] })

// Since this is within DocC we want to have an error if we don't handle new future cases.
case .none, ._nonFrozenEnum_useDefaultCase:
return nil
}
}
}
10 changes: 5 additions & 5 deletions Sources/SwiftDocC/Infrastructure/DocumentationContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1577,14 +1577,14 @@ public class DocumentationContext {
{
switch (source.kind, target.kind) {
case (.dictionaryKey, .dictionary):
let dictionaryKey = DictionaryKey(name: sourceSymbol.title, contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf))
let dictionaryKey = DictionaryKey(name: sourceSymbol.names.title, contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf))
if keysByTarget[edge.target] == nil {
keysByTarget[edge.target] = [dictionaryKey]
} else {
keysByTarget[edge.target]?.append(dictionaryKey)
}
case (.httpParameter, .httpRequest):
let parameter = HTTPParameter(name: sourceSymbol.title, source: (sourceSymbol.httpParameterSource ?? "query"), contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf))
let parameter = HTTPParameter(name: sourceSymbol.names.title, source: (sourceSymbol.httpParameterSource ?? "query"), contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf))
if parametersByTarget[edge.target] == nil {
parametersByTarget[edge.target] = [parameter]
} else {
Expand All @@ -1594,14 +1594,14 @@ public class DocumentationContext {
let body = HTTPBody(mediaType: sourceSymbol.httpMediaType, contents: [], symbol: sourceSymbol)
bodyByTarget[edge.target] = body
case (.httpParameter, .httpBody):
let parameter = HTTPParameter(name: sourceSymbol.title, source: "body", contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf))
let parameter = HTTPParameter(name: sourceSymbol.names.title, source: "body", contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf))
if bodyParametersByTarget[edge.target] == nil {
bodyParametersByTarget[edge.target] = [parameter]
} else {
bodyParametersByTarget[edge.target]?.append(parameter)
}
case (.httpResponse, .httpRequest):
let statusParts = sourceSymbol.title.split(separator: " ", maxSplits: 1)
let statusParts = sourceSymbol.names.title.split(separator: " ", maxSplits: 1)
let statusCode = UInt(statusParts[0]) ?? 0
let reason = statusParts.count > 1 ? String(statusParts[1]) : nil
let response = HTTPResponse(statusCode: statusCode, reason: reason, mediaType: sourceSymbol.httpMediaType, contents: [], symbol: sourceSymbol)
Expand Down Expand Up @@ -1649,7 +1649,7 @@ public class DocumentationContext {
if let semantic = target?.semantic as? Symbol {
// Add any body parameters to existing body record
var localBody = body
if let identifier = body.symbol?.preciseIdentifier, let bodyParameters = bodyParametersByTarget[identifier] {
if let identifier = body.symbol?.identifier.precise, let bodyParameters = bodyParametersByTarget[identifier] {
localBody.parameters = bodyParameters.sorted(by: \.name)
}
if semantic.httpBodySection == nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -806,3 +806,21 @@ private extension SymbolGraph.Relationship.Kind {
}
}
}

// MARK: Link completion

// This extension can't be defined in another file because it uses file-private API.
extension LinkCompletionTools {
/// Creates a new path hierarchy node for link completion purposes.
///
/// Use these nodes to compute disambiguation and match against parsed link components.
///
/// - 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.
static func _makeNodeAndIdentifier(name: String) -> (PathHierarchy.Node, ResolvedIdentifier) {
let node = PathHierarchy.Node(name: name)
let id = ResolvedIdentifier()

node.identifier = id
return (node, id)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ extension ExtendedTypeFormatTransformation {
relationships.append(.init(source: symbol.identifier.precise,
target: parent.identifier.precise,
kind: .inContextOf,
targetFallback: parent.title))
targetFallback: parent.names.title))
symbolIsConnectedToParent[symbol.identifier.precise] = true
}

Expand Down
Loading