Skip to content

Commit 0b27c07

Browse files
authored
Update link resolution to distinguish between failed and not-yet resolved references. (#22)
* Update link resolution to distinguish between failed and not-yet resolved references. (rdar://81334652) * Address review feedback - Label `problemForUnresolvedReference` arguments - Combine switch cases to remove duplicated code - Single quote reference in error message for consistency - Add missing word in documentation comment.
1 parent f202411 commit 0b27c07

23 files changed

+244
-150
lines changed

Sources/SwiftDocC/Benchmark/Metrics/ExternalTopicsHash.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,16 @@ extension Benchmark {
2727
return
2828
}
2929

30-
// Make a flat string of all resolved external topics.
30+
// Make a flat string of all successfully resolved external topics.
3131
// Note: We have to sort the URLs to produce a stable checksum.
32-
let sourceString = context.externallyResolvedLinks.map({ $0.value.absoluteString }).sorted().joined()
32+
let sourceString = context.externallyResolvedLinks.values.compactMap({
33+
switch $0 {
34+
case .success(let resolved):
35+
return resolved.absoluteString
36+
case .failure(_, _):
37+
return nil
38+
}
39+
}).sorted().joined()
3340
+ context.externallyResolvedSymbols.map({ $0.absoluteString }).sorted().joined()
3441

3542
result = .string(Checksum.md5(of: Data(sourceString.utf8)))

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,8 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
205205
/// This is tracked to exclude external symbols from the build output. Information about external references is still included for the local pages that makes the external reference.
206206
var externallyResolvedSymbols = Set<ResolvedTopicReference>()
207207

208-
/// All the link references that have been resolved from external sources.
209-
var externallyResolvedLinks = [ValidatedURL: ResolvedTopicReference]()
208+
/// All the link references that have been resolved from external sources, either successfully or not.
209+
var externallyResolvedLinks = [ValidatedURL: TopicReferenceResolutionResult]()
210210

211211
/// The mapping of external symbol identifiers to known disambiguated symbol path components.
212212
///
@@ -430,29 +430,30 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
430430
}
431431
}
432432

433-
try Set(collectedExternalLinks).compactMap { externalLink -> (url: ValidatedURL, resolved: ResolvedTopicReference)? in
433+
try Set(collectedExternalLinks).compactMap { externalLink -> (url: ValidatedURL, resolved: TopicReferenceResolutionResult)? in
434434
guard let referenceBundleIdentifier = externalLink.unresolved.topicURL.components.host else {
435435
assertionFailure("Should not hit this code path, url is verified to have an external bundle id.")
436436
return nil
437437
}
438438

439439
if let externalResolver = externalReferenceResolvers[referenceBundleIdentifier] {
440440
let reference = externalResolver.resolve(.unresolved(externalLink.unresolved), sourceLanguage: externalLink.targetLanguage)
441-
if case .resolved(let resolvedReference) = reference {
441+
if case .success(let resolvedReference) = reference {
442442
// Add the resolved entity to the documentation cache.
443443
if let externallyResolvedNode = try externalEntity(with: resolvedReference) {
444444
documentationCache[resolvedReference] = externallyResolvedNode
445445
}
446-
return (externalLink.unresolved.topicURL, resolvedReference)
447446
}
447+
return (externalLink.unresolved.topicURL, reference)
448448
}
449449

450450
return nil
451451
}
452452
.forEach { pair in
453453
externallyResolvedLinks[pair.url] = pair.resolved
454-
if pair.url.absoluteString != pair.resolved.absoluteString,
455-
let url = pair.resolved.url.flatMap(ValidatedURL.init) {
454+
if case .success(let resolvedReference) = pair.resolved,
455+
pair.url.absoluteString != resolvedReference.absoluteString,
456+
let url = resolvedReference.url.flatMap(ValidatedURL.init) {
456457
// If the resolved reference has a different URL than the link cache both URLs
457458
// so we can resolve both unresolved and resolved references.
458459
externallyResolvedLinks[url] = pair.resolved
@@ -605,7 +606,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
605606
topicGraph.addEdge(from: volumeNode, to: chapterNode)
606607

607608
for tutorialReference in chapter.topicReferences {
608-
guard case let .resolved(tutorialReference) = tutorialReference.topic,
609+
guard case let .resolved(.success(tutorialReference)) = tutorialReference.topic,
609610
let tutorialNode = topicGraph.nodeWithReference(tutorialReference) else {
610611
continue
611612
}
@@ -2152,7 +2153,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
21522153

21532154
If none of these succeeds we will return the original unresolved reference.
21542155
*/
2155-
public func resolve(_ reference: TopicReference, in parent: ResolvedTopicReference, fromSymbolLink isCurrentlyResolvingSymbolLink: Bool = false) -> TopicReference {
2156+
public func resolve(_ reference: TopicReference, in parent: ResolvedTopicReference, fromSymbolLink isCurrentlyResolvingSymbolLink: Bool = false) -> TopicReferenceResolutionResult {
21562157
switch reference {
21572158
case .unresolved(let unresolvedReference):
21582159

@@ -2164,7 +2165,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
21642165
// When resolving a symbol link, ignore non-symbol matches,
21652166
// do continue to try resolving the symbol as if cached match was not found.
21662167
} else {
2167-
return .resolved(cachedReference)
2168+
return .success(cachedReference)
21682169
}
21692170
}
21702171

@@ -2173,7 +2174,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
21732174
// Ensure we are resolving either relative links or "doc:" scheme links
21742175
guard reference.url?.scheme == nil || ResolvedTopicReference.urlHasResolvedTopicScheme(reference.url) else {
21752176
// Not resolvable in the topic graph
2176-
return reference
2177+
return .failure(unresolvedReference, errorMessage: "Reference URL \(reference.description.singleQuoted) doesn't have \"doc:\" scheme.")
21772178
}
21782179

21792180
// Fall back on the parent's bundle identifier for relative paths
@@ -2186,7 +2187,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
21862187
/// Returns the given reference if it resolves. Otherwise, returns nil.
21872188
///
21882189
/// Adds any non-resolving reference to the `allCandidateURLs` collection.
2189-
func attemptToResolve(_ reference: ResolvedTopicReference) -> TopicReference? {
2190+
func attemptToResolve(_ reference: ResolvedTopicReference) -> TopicReferenceResolutionResult? {
21902191
if topicGraph.nodeWithReference(reference) != nil {
21912192
let resolved = ResolvedTopicReference(bundleIdentifier: referenceBundleIdentifier, path: reference.url.path, fragment: reference.fragment, sourceLanguage: reference.sourceLanguage)
21922193
// If resolving a symbol link, only match symbol nodes.
@@ -2195,9 +2196,9 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
21952196
return nil
21962197
}
21972198
cacheReference(resolved, withKey: ResolvedTopicReference.cacheIdentifier(unresolvedReference, fromSymbolLink: isCurrentlyResolvingSymbolLink, in: parent))
2198-
return .resolved(resolved)
2199+
return .success(resolved)
21992200
} else if reference.fragment != nil, nodeAnchorSections.keys.contains(reference) {
2200-
return .resolved(reference)
2201+
return .success(reference)
22012202
} else {
22022203
allCandidateURLs.append(reference.url)
22032204
return nil
@@ -2283,10 +2284,14 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
22832284
}
22842285

22852286
// 5. Check if a pre-resolved external link.
2286-
if let bundleID = unresolvedReference.topicURL.components.host,
2287-
externalReferenceResolvers[bundleID] != nil,
2288-
let resolvedExternalReference = externallyResolvedLinks[unresolvedReference.topicURL] {
2289-
return .resolved(resolvedExternalReference)
2287+
if let bundleID = unresolvedReference.topicURL.components.host {
2288+
if externalReferenceResolvers[bundleID] != nil,
2289+
let resolvedExternalReference = externallyResolvedLinks[unresolvedReference.topicURL] {
2290+
// Return the successful or failed externally resolved reference.
2291+
return resolvedExternalReference
2292+
} else if !registeredBundles.contains(where: { $0.identifier == bundleID }) {
2293+
return .failure(unresolvedReference, errorMessage: "No external resolver registered for \(bundleID.singleQuoted).")
2294+
}
22902295
}
22912296

22922297
// External symbols are already pre-resolved while loading the symbol graph
@@ -2298,18 +2303,20 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
22982303
let unresolvedReference = UnresolvedTopicReference(topicURL: ValidatedURL(candidateURL)!)
22992304
let reference = fallbackResolver.resolve(.unresolved(unresolvedReference), sourceLanguage: parent.sourceLanguage)
23002305

2301-
if case .resolved(let resolvedReference) = reference {
2306+
if case .success(let resolvedReference) = reference {
23022307
cacheReference(resolvedReference, withKey: ResolvedTopicReference.cacheIdentifier(unresolvedReference, fromSymbolLink: isCurrentlyResolvingSymbolLink, in: parent))
2303-
return .resolved(resolvedReference)
2308+
return .success(resolvedReference)
23042309
}
23052310
}
23062311
}
23072312

23082313
// Give up: there is no local or external document for this reference.
2309-
return reference
2310-
case .resolved:
2311-
// This reference is already resolved, so don't change anything.
2312-
return reference
2314+
2315+
// External references which failed to resolve will already have returned a more specific error message.
2316+
return .failure(unresolvedReference, errorMessage: "No local documentation matches this reference.")
2317+
case .resolved(let resolved):
2318+
// This reference is already resolved (either as a success or a failure), so don't change anything.
2319+
return resolved
23132320
}
23142321
}
23152322

Sources/SwiftDocC/Infrastructure/DocumentationCurator.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ struct DocumentationCurator {
4747
}
4848
let maybeResolved = context.resolve(.unresolved(unresolved), in: resolved, fromSymbolLink: true)
4949

50-
if case let .resolved(resolved) = maybeResolved {
50+
if case let .success(resolved) = maybeResolved {
5151
return resolved
5252
}
5353
return nil
@@ -65,7 +65,7 @@ struct DocumentationCurator {
6565
}
6666
let maybeResolved = context.resolve(.unresolved(unresolved), in: resolved)
6767

68-
if case let .resolved(resolved) = maybeResolved {
68+
if case let .success(resolved) = maybeResolved {
6969
// The link resolves to a known topic.
7070
if let node = context.topicGraph.nodeWithReference(resolved) {
7171
// Make sure to remove any articles that have been registered in the topic graph
@@ -84,9 +84,12 @@ struct DocumentationCurator {
8484

8585
// Check if the link has been externally resolved already.
8686
if let bundleID = unresolved.topicURL.components.host,
87-
context.externalReferenceResolvers[bundleID] != nil || context.fallbackReferenceResolvers[bundleID] != nil,
88-
let resolvedExternalReference = context.externallyResolvedLinks[unresolved.topicURL] {
89-
return resolvedExternalReference
87+
context.externalReferenceResolvers[bundleID] != nil || context.fallbackReferenceResolvers[bundleID] != nil {
88+
if case .success(let resolvedExternalReference) = context.externallyResolvedLinks[unresolved.topicURL] {
89+
return resolvedExternalReference
90+
} else {
91+
return nil // This link has already failed to resolve.
92+
}
9093
}
9194

9295
// Try extracting an article from the cache

Sources/SwiftDocC/Infrastructure/External Data/ExternalReferenceResolver.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,29 @@ import Foundation
2424
/// for the node with the documentation content for that reference. Because this content isn't part of the compiled bundle, it won't be included in the build output. However, references to this node from the bundle's content
2525
/// may incorporate, for example, the external node's title, kind, or abstract in their output.
2626
///
27+
/// If the reference doesn't exist in the external source of documentation or if an error occurs while attempting to resolve the reference, the external resolver returns information about the error.
28+
///
2729
/// In addition to the information in the documentation node, the external resolver may be asked to provide a web URL that can be used to navigate to this resource. When the render node translator converts a documentation node
2830
/// that has an external reference in its content to a render node, this provided web URL is the link to the external content.
2931
///
3032
/// ## See Also
3133
/// - ``DocumentationContext/externalReferenceResolvers``
3234
/// - ``LinkDestinationSummary``
3335
/// - ``ExternalSymbolResolver``
36+
/// - ``TopicReferenceResolutionResult``
3437
public protocol ExternalReferenceResolver {
3538

3639
/// Attempts to resolve an unresolved reference for an external topic.
3740
///
38-
/// Your implementation returns a resolved reference if the topic exists in the external source of documentation, or the original unresolved reference if the topic doesn't exist in the external source.
41+
/// Your implementation returns a resolved reference if the topic exists in the external source of documentation, or information about why the reference failed to resolve if the topic doesn't exist in the external source.
42+
///
43+
/// Your implementation will only be called once for a given unresolved reference. Failures are assumed to persist for the duration of the documentation build.
3944
///
4045
/// - Parameters:
4146
/// - reference: The unresolved reference.
4247
/// - sourceLanguage: The source language of the reference, in case the reference exists in multiple languages.
43-
/// - Returns: The resolved reference for the topic, or the original unresolved reference if the topic doesn't exist in the external source.
44-
func resolve(_ reference: TopicReference, sourceLanguage: SourceLanguage) -> TopicReference
48+
/// - Returns: The resolved reference for the topic, or information about why the resolver failed to resolve the reference.
49+
func resolve(_ reference: TopicReference, sourceLanguage: SourceLanguage) -> TopicReferenceResolutionResult
4550

4651
/// Creates a new documentation node with the documentation content for the external reference.
4752
///

Sources/SwiftDocC/Infrastructure/External Data/FallbackReferenceResolver.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ import Foundation
2121
public protocol FallbackReferenceResolver {
2222
/// Attempts to resolve an unresolved reference for a topic that couldn't be resolved locally.
2323
///
24-
/// Your implementation returns a resolved reference if the topic exists in the external source of documentation, or the original
25-
/// unresolved reference if the topic doesn't exist in the external source.
24+
/// Your implementation returns a resolved reference if the topic exists in the external source of documentation, or information about why the reference failed to resolve if the topic doesn't exist in the external source.
25+
///
26+
/// Your implementation will only be called once for a given unresolved reference. Failures are assumed to persist for the duration of the documentation build.
2627
///
2728
/// - Parameters:
2829
/// - reference: The unresolved reference.
2930
/// - sourceLanguage: The source language of the reference, in case the reference exists in multiple languages.
30-
/// - Returns: The resolved reference for the topic, or the original unresolved reference if the topic doesn't exist in the external
31-
/// source.
32-
func resolve(_ reference: TopicReference, sourceLanguage: SourceLanguage) -> TopicReference
31+
/// - Returns: The resolved reference for the topic, or information about why the resolver failed to resolve the reference.
32+
func resolve(_ reference: TopicReference, sourceLanguage: SourceLanguage) -> TopicReferenceResolutionResult
3333

3434
/// Creates a new documentation node with the documentation content for the external reference, if the given reference was
3535
/// resolved by this resolver.

0 commit comments

Comments
 (0)