Skip to content

Commit a8ad29e

Browse files
Avoid visiting symbol markup twice when there's a documentation extension file (#951)
* Get the source URL from the visited markup instead of resolving links twice. rdar://129677496 --------- Co-authored-by: Pat Shaughnessy <[email protected]> * Remove 'source' arguments that are inferred from 'range.source' * Add unrelated comment about incorrect diagnostic suggestion * Code review: test ReferenceResolver diagnostic ranges * Code review: extract docChunk computation into property * Code review: extract check that symbol inherits docs from other module --------- Co-authored-by: Pat Shaughnessy <[email protected]>
1 parent b46c7b5 commit a8ad29e

File tree

11 files changed

+278
-125
lines changed

11 files changed

+278
-125
lines changed

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 84 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -518,78 +518,91 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
518518
let results = Synchronized<[LinkResolveResult]>([])
519519
results.sync({ $0.reserveCapacity(references.count) })
520520

521+
func inheritsDocumentationFromOtherModule(_ documentationNode: DocumentationNode, symbolOriginReference: ResolvedTopicReference) -> Bool {
522+
// Check that this symbol only has documentation from an in-source documentation comment
523+
guard documentationNode.docChunks.count == 1,
524+
case .sourceCode = documentationNode.docChunks.first?.source
525+
else {
526+
return false
527+
}
528+
529+
// Check that that documentation comment is inherited from a symbol belonging to another module
530+
guard let symbolSemantic = documentationNode.semantic as? Symbol,
531+
let originSymbolSemantic = documentationCache[symbolOriginReference]?.semantic as? Symbol
532+
else {
533+
return false
534+
}
535+
return symbolSemantic.moduleReference != originSymbolSemantic.moduleReference
536+
}
537+
521538
let resolveNodeWithReference: (ResolvedTopicReference) -> Void = { [unowned self] reference in
522-
if var documentationNode = try? entity(with: reference), documentationNode.semantic is Article || documentationNode.semantic is Symbol {
523-
for doc in documentationNode.docChunks {
524-
let source: URL?
525-
switch doc.source {
526-
case _ where documentationNode.semantic is Article,
527-
.documentationExtension:
528-
source = documentLocationMap[reference]
529-
case .sourceCode(let location, _):
530-
// For symbols, first check if we should reference resolve
531-
// inherited docs or not. If we don't inherit the docs
532-
// we should also skip reference resolving the chunk.
533-
if let semantic = documentationNode.semantic as? Symbol,
534-
let origin = semantic.origin, !externalMetadata.inheritDocs
535-
{
536-
// If the two symbols are coming from different modules,
537-
// regardless if they are in the same bundle
538-
// (for example Foundation and SwiftUI), skip link resolving.
539-
if let originSymbol = documentationCache[origin.identifier]?.semantic as? Symbol,
540-
originSymbol.moduleReference != semantic.moduleReference {
541-
continue
542-
}
543-
}
544-
545-
source = location?.url()
546-
}
547-
548-
// Find the inheritance parent, if the docs are inherited.
549-
let inheritanceParentReference: ResolvedTopicReference?
550-
if let origin = (documentationNode.semantic as? Symbol)?.origin,
551-
let originReference = documentationCache.reference(symbolID: origin.identifier)
552-
{
553-
inheritanceParentReference = originReference
554-
} else {
555-
inheritanceParentReference = nil
556-
}
557-
558-
var resolver = ReferenceResolver(context: self, bundle: bundle, source: source, rootReference: reference, inheritanceParentReference: inheritanceParentReference)
539+
guard var documentationNode = try? entity(with: reference),
540+
documentationNode.semantic is Article || documentationNode.semantic is Symbol
541+
else {
542+
return
543+
}
559544

560-
// Update the cache with the resolved node.
561-
// We aggressively release used memory, since we're copying all semantic objects
562-
// on the line below while rewriting nodes with the resolved content.
563-
documentationNode.semantic = autoreleasepool { resolver.visit(documentationNode.semantic) }
545+
let symbolOriginReference = (documentationNode.semantic as? Symbol)?.origin.flatMap { origin in
546+
documentationCache.reference(symbolID: origin.identifier)
547+
}
548+
// Check if we should skip resolving links for inherited documentation from other modules.
549+
if !externalMetadata.inheritDocs,
550+
let symbolOriginReference,
551+
inheritsDocumentationFromOtherModule(documentationNode, symbolOriginReference: symbolOriginReference)
552+
{
553+
// Don't resolve any links for this symbol.
554+
return
555+
}
556+
557+
var resolver = ReferenceResolver(context: self, bundle: bundle, rootReference: reference, inheritanceParentReference: symbolOriginReference)
558+
559+
// Update the node with the markup that contains resolved references instead of authored links.
560+
documentationNode.semantic = autoreleasepool {
561+
// We use an autorelease pool to release used memory as soon as possible, since the resolver will copy each semantic value
562+
// to rewrite it and replace the authored links with resolved reference strings instead.
563+
resolver.visit(documentationNode.semantic)
564+
}
565+
566+
let problems: [Problem]
567+
if documentationNode.semantic is Article {
568+
// Diagnostics for articles have correct source ranges and don't need to be modified.
569+
problems = resolver.problems
570+
} else {
571+
// Diagnostics for in-source documentation comments need to be offset based on the start location of the comment in the source file.
572+
573+
// Get the source location
574+
let inSourceDocumentationCommentInfo = documentationNode.inSourceDocumentationChunk
575+
576+
// Post-process and filter out unwanted diagnostics (for example from inherited documentation comments)
577+
problems = resolver.problems.compactMap { problem in
578+
guard let source = problem.diagnostic.source else {
579+
// Ignore any diagnostic without a source location. These can't be meaningfully presented to the user.
580+
return nil
581+
}
564582

565-
let pageImageProblems = documentationNode.metadata?.pageImages.compactMap { pageImage in
566-
return resolver.resolve(
567-
resource: pageImage.source,
568-
range: pageImage.originalMarkup.range,
569-
severity: .warning
570-
)
571-
} ?? []
583+
if source == inSourceDocumentationCommentInfo?.url, let offset = inSourceDocumentationCommentInfo?.offset {
584+
// Diagnostics from an in-source documentation comment need to be offset based on the location of that documentation comment.
585+
var modifiedProblem = problem
586+
modifiedProblem.offsetWithRange(offset)
587+
return modifiedProblem
588+
}
572589

573-
resolver.problems.append(contentsOf: pageImageProblems)
574-
575-
var problems = resolver.problems
576-
577-
if case .sourceCode(_, let offset) = doc.source, documentationNode.kind.isSymbol {
578-
// Offset all problem ranges by the start location of the
579-
// source comment in the context of the complete file.
580-
if let docRange = offset {
581-
for i in problems.indices {
582-
problems[i].offsetWithRange(docRange)
583-
}
584-
} else {
585-
problems.removeAll()
586-
}
587-
}
588-
589-
let result: LinkResolveResult = (reference: reference, node: documentationNode, problems: problems)
590-
results.sync({ $0.append(result) })
590+
// Diagnostics from documentation extension files have correct source ranges and don't need to be modified.
591+
return problem
591592
}
592593
}
594+
595+
// Also resolve the node's page images. This isn't part of the node's 'semantic' value (resolved above).
596+
let pageImageProblems = documentationNode.metadata?.pageImages.compactMap { pageImage in
597+
return resolver.resolve(
598+
resource: pageImage.source,
599+
range: pageImage.originalMarkup.range,
600+
severity: .warning
601+
)
602+
} ?? []
603+
604+
let result: LinkResolveResult = (reference: reference, node: documentationNode, problems: problems + pageImageProblems)
605+
results.sync({ $0.append(result) })
593606
}
594607

595608
// Resolve links concurrently if there are no external resolvers.
@@ -604,7 +617,8 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
604617
// Record symbol links as symbol "mentions" for automatic cross references
605618
// on rendered symbol documentation.
606619
if let article = result.node.semantic as? Article,
607-
case .article = DocumentationContentRenderer.roleForArticle(article, nodeKind: result.node.kind) {
620+
case .article = DocumentationContentRenderer.roleForArticle(article, nodeKind: result.node.kind)
621+
{
608622
for markup in article.abstractSection?.content ?? [] {
609623
var mentions = SymbolLinkCollector(context: self, article: result.node.reference, baseWeight: 2)
610624
mentions.visit(markup)
@@ -652,7 +666,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
652666
autoreleasepool {
653667
let url = technologyResult.source
654668
let unresolvedTechnology = technologyResult.value
655-
var resolver = ReferenceResolver(context: self, bundle: bundle, source: url)
669+
var resolver = ReferenceResolver(context: self, bundle: bundle)
656670
let technology = resolver.visit(unresolvedTechnology) as! Technology
657671
diagnosticEngine.emit(resolver.problems)
658672

@@ -724,7 +738,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
724738
autoreleasepool {
725739
let url = tutorialResult.source
726740
let unresolvedTutorial = tutorialResult.value
727-
var resolver = ReferenceResolver(context: self, bundle: bundle, source: url)
741+
var resolver = ReferenceResolver(context: self, bundle: bundle)
728742
let tutorial = resolver.visit(unresolvedTutorial) as! Tutorial
729743
diagnosticEngine.emit(resolver.problems)
730744

@@ -758,7 +772,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
758772
autoreleasepool {
759773
let url = articleResult.source
760774
let unresolvedTutorialArticle = articleResult.value
761-
var resolver = ReferenceResolver(context: self, bundle: bundle, source: url)
775+
var resolver = ReferenceResolver(context: self, bundle: bundle)
762776
let article = resolver.visit(unresolvedTutorialArticle) as! TutorialArticle
763777
diagnosticEngine.emit(resolver.problems)
764778

Sources/SwiftDocC/Model/DocumentationNode.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,16 @@ public struct DocumentationNode {
9494
/// property.
9595
var docChunks: [DocumentationChunk]
9696

97+
/// Returns information about the node's in-source documentation comment chunk, or `nil` if the node doesn't have an in-source documentation chunk.
98+
var inSourceDocumentationChunk: (url: URL?, offset: SymbolGraph.LineList.SourceRange?)? {
99+
for docChunk in docChunks {
100+
guard case .sourceCode(let location, let offset) = docChunk.source else { continue }
101+
102+
return (url: location?.url, offset: offset)
103+
}
104+
return nil
105+
}
106+
97107
/// Linkable in-content sections.
98108
var anchorSections = [AnchorSection]()
99109

0 commit comments

Comments
 (0)