Skip to content

Support resolving documentation links to content in other DocC archives #710

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5d8fec5
Add ability to encode/decode the link resolver information
d-ronnqvist Sep 8, 2023
4c7e46f
Add feature flag to write link resolution info to output archive.
d-ronnqvist Sep 20, 2023
43fe73c
Add argument to pass documentation dependencies
d-ronnqvist Sep 20, 2023
d97d5b1
Read external page content from linkable-entities.json
d-ronnqvist Sep 20, 2023
82e378e
Move LinkResolver wrapper into its own file
d-ronnqvist Sep 22, 2023
57f1be8
Merge branch 'main' into path-hierarchy-serialization
d-ronnqvist Sep 22, 2023
1ed7c92
Merge branch 'main' into path-hierarchy-serialization
d-ronnqvist Sep 25, 2023
c647055
Merge branch 'main' into path-hierarchy-serialization
d-ronnqvist Sep 26, 2023
bb32fae
Document new 'includeTaskGroups' argument
d-ronnqvist Sep 26, 2023
ad3546a
Only encode/decode paths for non-symbols
d-ronnqvist Sep 26, 2023
8ccc13f
Apply variant content to external topic render references
d-ronnqvist Sep 26, 2023
e418cb1
Fail to resolve external link if resolver has no content for that page
d-ronnqvist Sep 26, 2023
dc8ea18
Include external topic references in render reference dependencies
d-ronnqvist Sep 26, 2023
c3eed9c
Add radars to 2 todo comments to track follow up work
d-ronnqvist Sep 26, 2023
b38615f
Add more documentation to new external resolver classes
d-ronnqvist Sep 27, 2023
8aa3506
Remove convenience accessor for hierarchyBasedLinkResolver
d-ronnqvist Sep 27, 2023
57abc35
Add more documentation about how the link resolver is encoded/decoded.
d-ronnqvist Sep 27, 2023
9cd4f74
Make 'initializeElement(at:to:)' available before Swift 5.8
d-ronnqvist Sep 27, 2023
b57c2b9
Only encode the symbol identifier in the link hierarchy.
d-ronnqvist Sep 28, 2023
ee0961f
Merge branch 'main' into path-hierarchy-serialization
d-ronnqvist Sep 28, 2023
068f6f0
Merge branch 'main' into path-hierarchy-serialization
d-ronnqvist Oct 3, 2023
29136e8
Fix bug where content wasn't looked up for external on-page elements
d-ronnqvist Oct 4, 2023
65ee2ba
Leverage `encodeIfTrue` and `encodeIfNotEmpty` for link hierarchy
d-ronnqvist Oct 4, 2023
a2fab86
Extract and describe closure that names colliding nodes in ambiguous …
d-ronnqvist Oct 4, 2023
eff39ae
Add comments that describe processing of symbols and non-symbols
d-ronnqvist Oct 4, 2023
e7bf211
Use SPI for 'dependencyArchives' property
d-ronnqvist Oct 5, 2023
5798b9b
Check if link is already externally resolved before trying locally
d-ronnqvist Oct 5, 2023
7c999ed
Add documentation for `Convert/dependencies`
d-ronnqvist Oct 5, 2023
50f90f9
Clarify local variable name for remaining path components
d-ronnqvist Oct 5, 2023
2625fb3
Finish incomplete sentence in documentation
d-ronnqvist Oct 5, 2023
c54901b
Pluralize private variable
d-ronnqvist Oct 5, 2023
01590e9
Use indices to iterate over path hierarchy file representation nodes
d-ronnqvist Oct 5, 2023
e1f41f4
Move note about unstable format to documentation comment
d-ronnqvist Oct 5, 2023
a98f8df
Gate refined error message behind feature flag
d-ronnqvist Oct 5, 2023
80a983c
Fix build errors after merging main
d-ronnqvist Oct 5, 2023
6511d2a
Remove test assertions that no longer make sense
d-ronnqvist Oct 5, 2023
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
4 changes: 4 additions & 0 deletions Sources/SwiftDocC/Infrastructure/ConvertOutputConsumer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,15 @@ public protocol ConvertOutputConsumer {

/// Consumes build metadata created during a conversion.
func consume(buildMetadata: BuildMetadata) throws

/// Consumes a file representation of the local link resolution information.
func consume(linkResolutionInformation: SerializableLinkResolutionInformation) throws
}

// Default implementations that discard the documentation conversion products, for consumers that don't need these
// values.
public extension ConvertOutputConsumer {
func consume(renderReferenceStore: RenderReferenceStore) throws {}
func consume(buildMetadata: BuildMetadata) throws {}
func consume(linkResolutionInformation: SerializableLinkResolutionInformation) throws {}
}
44 changes: 29 additions & 15 deletions Sources/SwiftDocC/Infrastructure/DocumentationContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,8 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
}
}

/// A link resolver that resolves references by finding them in path hierarchy.
///
/// The link resolver is `nil` until some documentation content is registered with the context.
/// It's safe to access the link resolver during symbol registration and at later points in the registration and conversion.
var hierarchyBasedLinkResolver: PathHierarchyBasedLinkResolver! = nil
/// A class that resolves documentation links by orchestrating calls to other link resolver implementations.
public var linkResolver = LinkResolver()

/// The provider of documentation bundles for this context.
var dataProvider: DocumentationContextDataProvider
Expand Down Expand Up @@ -200,6 +197,8 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
/// A list of non-topic links that can be resolved.
var nodeAnchorSections = [ResolvedTopicReference: AnchorSection]()

var externalCache = [ResolvedTopicReference: LinkResolver.ExternalEntity]()

/// A list of all the problems that was encountered while registering and processing the documentation bundles in this context.
public var problems: [Problem] {
return diagnosticEngine.problems
Expand Down Expand Up @@ -361,7 +360,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
/// - dataProvider: The provider that removed this bundle.
/// - bundle: The bundle that was removed.
public func dataProvider(_ dataProvider: DocumentationContextDataProvider, didRemoveBundle bundle: DocumentationBundle) throws {
hierarchyBasedLinkResolver?.unregisterBundle(identifier: bundle.identifier)
linkResolver.localResolver?.unregisterBundle(identifier: bundle.identifier)

// Purge the reference cache for this bundle and disable reference caching for
// this bundle moving forward.
Expand Down Expand Up @@ -1153,7 +1152,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
var moduleReferences = [String: ResolvedTopicReference]()

// Build references for all symbols in all of this module's symbol graphs.
let symbolReferences = hierarchyBasedLinkResolver.referencesForSymbols(in: symbolGraphLoader.unifiedGraphs, bundle: bundle, context: self)
let symbolReferences = linkResolver.localResolver.referencesForSymbols(in: symbolGraphLoader.unifiedGraphs, bundle: bundle, context: self)

// Set the index and cache storage capacity to avoid ad-hoc storage resizing.
symbolIndex.reserveCapacity(symbolReferences.count)
Expand Down Expand Up @@ -1270,7 +1269,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
// Only add the symbol mapping now if the path hierarchy based resolver is the main implementation.
// If it is only used for mismatch checking then we must wait until the documentation cache code path has traversed and updated all the colliding nodes.
// Otherwise the mappings will save the unmodified references and the hierarchy based resolver won't find the expected parent nodes when resolving links.
hierarchyBasedLinkResolver.addMappingForSymbols(symbolIndex: symbolIndex)
linkResolver.localResolver.addMappingForSymbols(symbolIndex: symbolIndex)

// Track the symbols that have multiple matching documentation extension files for diagnostics.
var symbolsWithMultipleDocumentationExtensionMatches = [ResolvedTopicReference: [SemanticResult<Article>]]()
Expand Down Expand Up @@ -1817,7 +1816,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
topicGraph.addNode(graphNode)
documentationCache[reference] = documentation

hierarchyBasedLinkResolver.addRootArticle(article, anchorSections: documentation.anchorSections)
linkResolver.localResolver.addRootArticle(article, anchorSections: documentation.anchorSections)
for anchor in documentation.anchorSections {
nodeAnchorSections[anchor.reference] = anchor
}
Expand Down Expand Up @@ -1873,7 +1872,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
let graphNode = TopicGraph.Node(reference: reference, kind: .article, source: .file(url: article.source), title: title)
topicGraph.addNode(graphNode)

hierarchyBasedLinkResolver.addArticle(article, anchorSections: documentation.anchorSections)
linkResolver.localResolver.addArticle(article, anchorSections: documentation.anchorSections)
for anchor in documentation.anchorSections {
nodeAnchorSections[anchor.reference] = anchor
}
Expand Down Expand Up @@ -2078,6 +2077,17 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
}
}

discoveryGroup.async(queue: discoveryQueue) { [unowned self] in
do {
try linkResolver.loadExternalResolvers()
} catch {
// Pipe the error out of the dispatch queue.
discoveryError.sync({
if $0 == nil { $0 = error }
})
}
}

discoveryGroup.wait()

try shouldContinueRegistration()
Expand Down Expand Up @@ -2128,7 +2138,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
options = globalOptions.first
}

self.hierarchyBasedLinkResolver = hierarchyBasedResolver
self.linkResolver.localResolver = hierarchyBasedResolver
hierarchyBasedResolver.addMappingForRoots(bundle: bundle)
for tutorial in tutorials {
hierarchyBasedResolver.addTutorial(tutorial)
Expand Down Expand Up @@ -2187,7 +2197,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
}

try shouldContinueRegistration()
var allCuratedReferences = try crawlSymbolCuration(in: hierarchyBasedLinkResolver.topLevelSymbols(), bundle: bundle)
var allCuratedReferences = try crawlSymbolCuration(in: linkResolver.localResolver.topLevelSymbols(), bundle: bundle)

// Store the list of manually curated references if doc coverage is on.
if shouldStoreManuallyCuratedReferences {
Expand Down Expand Up @@ -2222,7 +2232,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
// Emit warnings for any remaining uncurated files.
emitWarningsForUncuratedTopics()

hierarchyBasedLinkResolver.addAnchorForSymbols(symbolIndex: symbolIndex, documentationCache: documentationCache)
linkResolver.localResolver.addAnchorForSymbols(symbolIndex: symbolIndex, documentationCache: documentationCache)

// Fifth, resolve links in nodes that are added solely via curation
try preResolveExternalLinks(references: Array(allCuratedReferences), bundle: bundle)
Expand Down Expand Up @@ -2329,7 +2339,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
/// - Returns: An ordered list of symbol references that have been added to the topic graph automatically.
private func autoCurateSymbolsInTopicGraph(engine: DiagnosticEngine) -> [(child: ResolvedTopicReference, parent: ResolvedTopicReference)] {
var automaticallyCuratedSymbols = [(ResolvedTopicReference, ResolvedTopicReference)]()
hierarchyBasedLinkResolver.traverseSymbolAndParentPairs { reference, parentReference in
linkResolver.localResolver.traverseSymbolAndParentPairs { reference, parentReference in
guard let topicGraphNode = topicGraph.nodeWithReference(reference),
let topicGraphParentNode = topicGraph.nodeWithReference(parentReference),
// Check that the node hasn't got any parents from manual curation
Expand Down Expand Up @@ -2535,6 +2545,9 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
let referenceWithoutFragment = reference.withFragment(nil)
return try entity(with: referenceWithoutFragment).availableSourceLanguages
} catch ContextError.notFound {
if let externalEntity = externalCache[reference] {
return externalEntity.sourceLanguages
}
preconditionFailure("Reference does not have an associated documentation node.")
} catch {
fatalError("Unexpected error when retrieving source languages: \(error)")
Expand Down Expand Up @@ -2646,7 +2659,8 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
public func resolve(_ reference: TopicReference, in parent: ResolvedTopicReference, fromSymbolLink isCurrentlyResolvingSymbolLink: Bool = false) -> TopicReferenceResolutionResult {
switch reference {
case .unresolved(let unresolvedReference):
return hierarchyBasedLinkResolver.resolve(unresolvedReference, in: parent, fromSymbolLink: isCurrentlyResolvingSymbolLink, context: self)
return linkResolver.resolve(unresolvedReference, in: parent, fromSymbolLink: isCurrentlyResolvingSymbolLink, context: self)

case .resolved(let resolved):
// This reference is already resolved (either as a success or a failure), so don't change anything.
return resolved
Expand Down
21 changes: 20 additions & 1 deletion Sources/SwiftDocC/Infrastructure/DocumentationConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -330,14 +330,20 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
}

if emitDigest {
let nodeLinkSummaries = entity.externallyLinkableElementSummaries(context: context, renderNode: renderNode)
let nodeLinkSummaries = entity.externallyLinkableElementSummaries(context: context, renderNode: renderNode, includeTaskGroups: true)
let nodeIndexingRecords = try renderNode.indexingRecords(onPage: identifier)

resultsGroup.async(queue: resultsSyncQueue) {
assets.merge(renderNode.assetReferences, uniquingKeysWith: +)
linkSummaries.append(contentsOf: nodeLinkSummaries)
indexingRecords.append(contentsOf: nodeIndexingRecords)
}
} else if FeatureFlags.current.isExperimentalLinkHierarchySerializationEnabled {
let nodeLinkSummaries = entity.externallyLinkableElementSummaries(context: context, renderNode: renderNode, includeTaskGroups: false)

resultsGroup.async(queue: resultsSyncQueue) {
linkSummaries.append(contentsOf: nodeLinkSummaries)
}
}
} catch {
recordProblem(from: error, in: &results, withIdentifier: "render-node")
Expand All @@ -362,6 +368,19 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
}
}

if FeatureFlags.current.isExperimentalLinkHierarchySerializationEnabled {
do {
let serializableLinkInformation = try context.linkResolver.localResolver.prepareForSerialization(bundleID: bundle.identifier)
try outputConsumer.consume(linkResolutionInformation: serializableLinkInformation)

if !emitDigest {
try outputConsumer.consume(linkableElementSummaries: linkSummaries)
}
} catch {
recordProblem(from: error, in: &conversionProblems, withIdentifier: "link-resolver")
}
}

if emitDigest {
do {
try outputConsumer.consume(problems: context.problems + conversionProblems)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ struct DocumentationCurator {
context.topicGraph.addNode(curatedNode)

// Move the article from the article cache to the documentation
context.hierarchyBasedLinkResolver.addArticle(filename: articleFilename, reference: reference, anchorSections: documentationNode.anchorSections)
context.linkResolver.localResolver.addArticle(filename: articleFilename, reference: reference, anchorSections: documentationNode.anchorSections)

context.documentationCache[reference] = documentationNode
for anchor in documentationNode.anchorSections {
Expand Down
Loading