diff --git a/Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift b/Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift index 7250f6d646..f859419918 100644 --- a/Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift +++ b/Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift @@ -150,14 +150,15 @@ public struct ConvertService: DocumentationService { provider = inMemoryProvider } - let context = try DocumentationContext(dataProvider: workspace) - context.knownDisambiguatedSymbolPathComponents = request.knownDisambiguatedSymbolPathComponents + var configuration = DocumentationContext.Configuration() + + configuration.convertServiceConfiguration.knownDisambiguatedSymbolPathComponents = request.knownDisambiguatedSymbolPathComponents // Enable support for generating documentation for standalone articles and tutorials. - context.allowsRegisteringArticlesWithoutTechnologyRoot = true - context.considerDocumentationExtensionsThatDoNotMatchSymbolsAsResolved = true + configuration.convertServiceConfiguration.allowsRegisteringArticlesWithoutTechnologyRoot = true + configuration.convertServiceConfiguration.considerDocumentationExtensionsThatDoNotMatchSymbolsAsResolved = true - context.configureSymbolGraph = { symbolGraph in + configuration.convertServiceConfiguration.symbolGraphTransformer = { symbolGraph in for (symbolIdentifier, overridingDocumentationComment) in request.overridingDocumentationComments ?? [:] { symbolGraph.symbols[symbolIdentifier]?.docComment = SymbolGraph.LineList( overridingDocumentationComment.map(SymbolGraph.LineList.Line.init(_:)) @@ -172,10 +173,12 @@ public struct ConvertService: DocumentationService { convertRequestIdentifier: messageIdentifier ) - context.convertServiceFallbackResolver = resolver - context.globalExternalSymbolResolver = resolver + configuration.convertServiceConfiguration.fallbackResolver = resolver + configuration.externalDocumentationConfiguration.globalSymbolResolver = resolver } - + + let context = try DocumentationContext(dataProvider: workspace, configuration: configuration) + var converter = try self.converter ?? DocumentationConverter( documentationBundleURL: request.bundleLocation ?? URL(fileURLWithPath: "/"), emitDigest: false, diff --git a/Sources/SwiftDocC/Infrastructure/Context/Deprecated/DocumentationContext+Deprecated.swift b/Sources/SwiftDocC/Infrastructure/Context/Deprecated/DocumentationContext+Deprecated.swift new file mode 100644 index 0000000000..6e4aadd6bc --- /dev/null +++ b/Sources/SwiftDocC/Infrastructure/Context/Deprecated/DocumentationContext+Deprecated.swift @@ -0,0 +1,36 @@ +/* + 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 +*/ + +extension DocumentationContext { + + @available(*, deprecated, renamed: "configuration.externalMetadata", message: "Use 'configuration.externalMetadata' instead. This deprecated API will be removed after Swift 6.2 is released.") + public var externalMetadata: ExternalMetadata { + get { configuration.externalMetadata } + set { configuration.externalMetadata = newValue } + } + + @available(*, deprecated, renamed: "configuration.externalDocumentationConfiguration.sources", message: "Use 'configuration.externalDocumentationConfiguration.sources' instead. This deprecated API will be removed after Swift 6.2 is released.") + public var externalDocumentationSources: [BundleIdentifier: ExternalDocumentationSource] { + get { configuration.externalDocumentationConfiguration.sources } + set { configuration.externalDocumentationConfiguration.sources = newValue } + } + + @available(*, deprecated, renamed: "configuration.externalDocumentationConfiguration.globalSymbolResolver", message: "Use 'configuration.externalDocumentationConfiguration.globalSymbolResolver' instead. This deprecated API will be removed after Swift 6.2 is released.") + public var globalExternalSymbolResolver: GlobalExternalSymbolResolver? { + get { configuration.externalDocumentationConfiguration.globalSymbolResolver } + set { configuration.externalDocumentationConfiguration.globalSymbolResolver = newValue } + } + + @available(*, deprecated, renamed: "configuration.experimentalCoverageConfiguration.shouldStoreManuallyCuratedReferences", message: "Use 'configuration.experimentalCoverageConfiguration.shouldStoreManuallyCuratedReferences' instead. This deprecated API will be removed after Swift 6.2 is released.") + public var shouldStoreManuallyCuratedReferences: Bool { + get { configuration.experimentalCoverageConfiguration.shouldStoreManuallyCuratedReferences } + set { configuration.experimentalCoverageConfiguration.shouldStoreManuallyCuratedReferences = newValue } + } +} diff --git a/Sources/SwiftDocC/Infrastructure/Context/DocumentationContext+Configuration.swift b/Sources/SwiftDocC/Infrastructure/Context/DocumentationContext+Configuration.swift new file mode 100644 index 0000000000..b4dd0cf40d --- /dev/null +++ b/Sources/SwiftDocC/Infrastructure/Context/DocumentationContext+Configuration.swift @@ -0,0 +1,98 @@ +/* + 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 +import SymbolKit + +extension DocumentationContext { + /// A collection of configuration to apply to a documentation context during initialization. + public struct Configuration { + // This type exists so that the context doesn't need to be modified in-between creation and registration of the data provider. + // It's a small step towards making the context fully immutable. + + /// Creates a new default configuration. + public init() {} + + // MARK: Convert Service configuration + + /// Configuration specific to the ``ConvertService``. + var convertServiceConfiguration = ConvertServiceConfiguration() + + /// A collection of configuration specific to the ``ConvertService``. + struct ConvertServiceConfiguration { + /// The mapping of external symbol identifiers to known disambiguated symbol path components. + /// + /// In situations where the local documentation context doesn't contain all of the current module's + /// symbols, for example when using a ``ConvertService`` with a partial symbol graph, + /// the documentation context is otherwise unable to accurately detect a collision for a given symbol and correctly + /// disambiguate its path components. This value can be used to inject already disambiguated symbol + /// path components into the documentation context. + var knownDisambiguatedSymbolPathComponents: [String: [String]]? + + /// Controls whether bundle registration should allow registering articles when no technology root is defined. + /// + /// Set this property to `true` to enable registering documentation for standalone articles, + /// for example when using ``ConvertService``. + var allowsRegisteringArticlesWithoutTechnologyRoot = false + + /// Controls whether documentation extension files are considered resolved even when they don't match a symbol. + /// + /// Set this property to `true` to always consider documentation extensions as "resolved", for example when using ``ConvertService``. + /// + /// > Note: + /// > Setting this property tor `true` means taking over the responsibility to match documentation extension files to symbols + /// > diagnosing unmatched documentation extension files, and diagnostic symbols that match multiple documentation extension files. + var considerDocumentationExtensionsThatDoNotMatchSymbolsAsResolved = false + + /// A resolver that attempts to resolve local references to content that wasn't included in the catalog or symbol input. + /// + /// > Warning: + /// > Setting a fallback reference resolver makes accesses to the context non-thread-safe. + /// > This is because the fallback resolver can run during both local link resolution and during rendering, which both happen concurrently for each page. + /// > In practice this shouldn't matter because the convert service only builds documentation for one page. + var fallbackResolver: ConvertServiceFallbackResolver? + + /// A closure that modifies each symbol graph before the context registers the symbol graph's information. + var symbolGraphTransformer: ((inout SymbolGraph) -> ())? = nil + } + + // MARK: External metadata + + /// External metadata injected into the context, for example via command line arguments. + public var externalMetadata = ExternalMetadata() + + // MARK: External documentation + + /// Configuration related to external sources of documentation. + public var externalDocumentationConfiguration = ExternalDocumentationConfiguration() + + /// A collection of configuration related to external sources of documentation. + public struct ExternalDocumentationConfiguration { + /// The lookup of external documentation sources by their bundle identifiers. + public var sources: [BundleIdentifier: ExternalDocumentationSource] = [:] + /// A type that resolves all symbols that are referenced in symbol graph files but can't be found in any of the locally available symbol graph files. + public var globalSymbolResolver: GlobalExternalSymbolResolver? + /// A list of URLs to documentation archives that the local documentation depends on. + @_spi(ExternalLinks) // This needs to be public SPI so that the ConvertAction can set it. + public var dependencyArchives: [URL] = [] + } + + // MARK: Experimental coverage + + /// Configuration related to the experimental documentation coverage feature. + package var experimentalCoverageConfiguration = ExperimentalCoverageConfiguration() + + /// A collection of configuration related to the experimental documentation coverage feature. + package struct ExperimentalCoverageConfiguration { + /// Controls whether the context stores the set of references that are manually curated. + package var shouldStoreManuallyCuratedReferences: Bool = false + } + } +} diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift index c33b774430..6ff807ecef 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift @@ -116,6 +116,15 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { /// The provider of documentation bundles for this context. var dataProvider: DocumentationContextDataProvider + /// A collection of configuration for this context. + public package(set) var configuration: Configuration { + get { _configuration } + @available(*, deprecated, message: "Pass a configuration at initialization. This property will become read-only after Swift 6.2 is released.") + set { _configuration = newValue } + } + // Having a deprecated setter above requires a computed property. + private var _configuration: Configuration + /// The graph of all the documentation content and their relationships to each other. /// /// > Important: The topic graph has no awareness of source language specific edges. @@ -123,35 +132,6 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { /// User-provided global options for this documentation conversion. var options: Options? - - /// A value to control whether the set of manually curated references found during bundle registration should be stored. Defaults to `false`. Setting this property to `false` clears any stored references from `manuallyCuratedReferences`. - public var shouldStoreManuallyCuratedReferences: Bool = false { - didSet { - if shouldStoreManuallyCuratedReferences == false { - manuallyCuratedReferences = nil - } - } - } - - /// Controls whether bundle registration should allow registering articles when no technology root is defined. - /// - /// Set this property to `true` to enable registering documentation for standalone articles, - /// for example when using ``ConvertService``. - var allowsRegisteringArticlesWithoutTechnologyRoot: Bool = false - - /// Controls whether documentation extension files are considered resolved even when they don't match a symbol. - /// - /// Set this property to `true` to always consider documentation extensions as "resolved", for example when using ``ConvertService``. - /// - /// > Note: - /// > Setting this property tor `true` means taking over the responsibility to match documentation extension files to symbols - /// > diagnosing unmatched documentation extension files, and diagnostic symbols that match multiple documentation extension files. - var considerDocumentationExtensionsThatDoNotMatchSymbolsAsResolved: Bool = false - - /// A closure that modifies each symbol graph that the context registers. - /// - /// Set this property if you need to modify symbol graphs before the context registers its information. - var configureSymbolGraph: ((inout SymbolGraph) -> ())? = nil /// The set of all manually curated references if `shouldStoreManuallyCuratedReferences` was true at the time of processing and has remained `true` since.. Nil if curation has not been processed yet. public private(set) var manuallyCuratedReferences: Set? @@ -224,32 +204,11 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { /// The engine that collects problems encountered while registering and processing the documentation bundles in this context. public var diagnosticEngine: DiagnosticEngine - /// The lookup of external documentation sources by their bundle identifiers. - public var externalDocumentationSources = [BundleIdentifier: ExternalDocumentationSource]() - - /// A resolver that attempts to resolve local references to content that wasn't included in the catalog or symbol input. - /// - /// - Warning: Setting a fallback reference resolver makes accesses to the context non-thread-safe. This is because the fallback resolver can run during both local link - /// resolution and during rendering, which both happen concurrently for each page. In practice this shouldn't matter because the convert service only builds documentation for one page. - var convertServiceFallbackResolver: ConvertServiceFallbackResolver? - - /// A type that resolves all symbols that are referenced in symbol graph files but can't be found in any of the locally available symbol graph files. - public var globalExternalSymbolResolver: GlobalExternalSymbolResolver? - /// All the link references that have been resolved from external sources, either successfully or not. /// /// The unsuccessful links are tracked so that the context doesn't attempt to re-resolve the unsuccessful links during rendering which runs concurrently for each page. var externallyResolvedLinks = [ValidatedURL: TopicReferenceResolutionResult]() - /// The mapping of external symbol identifiers to known disambiguated symbol path components. - /// - /// In situations where the local documentation context doesn't contain all of the current module's - /// symbols, for example when using a ``ConvertService`` with a partial symbol graph, - /// the documentation context is otherwise unable to accurately detect a collision for a given symbol and correctly - /// disambiguate its path components. This value can be used to inject already disambiguated symbol - /// path components into the documentation context. - var knownDisambiguatedSymbolPathComponents: [String: [String]]? - /// A temporary structure to hold a semantic value that hasn't yet had its links resolved. /// /// These temporary values are only expected to exist while the documentation is being built. Once the documentation bundles have been fully registered and the topic graph @@ -281,20 +240,25 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { /// The key to lookup a documentation extension file is the symbol reference from its title (level 1 heading). var uncuratedDocumentationExtensions = [ResolvedTopicReference: SemanticResult
]() - /// External metadata injected into the context, for example via command line arguments. - public var externalMetadata = ExternalMetadata() - /// Mentions of symbols within articles. var articleSymbolMentions = ArticleSymbolMentions() /// Initializes a documentation context with a given `dataProvider` and registers all the documentation bundles that it provides. /// - /// - Parameter dataProvider: The data provider to register bundles from. - /// - Parameter diagnosticEngine: The pre-configured engine that will collect problems encountered during compilation. + /// - Parameters: + /// - dataProvider: The data provider to register bundles from. + /// - diagnosticEngine: The pre-configured engine that will collect problems encountered during compilation. + /// - configuration: A collection of configuration for the created context. /// - Throws: If an error is encountered while registering a documentation bundle. - public init(dataProvider: DocumentationContextDataProvider, diagnosticEngine: DiagnosticEngine = .init()) throws { + public init( + dataProvider: DocumentationContextDataProvider, + diagnosticEngine: DiagnosticEngine = .init(), + configuration: Configuration = .init() + ) throws { self.dataProvider = dataProvider self.diagnosticEngine = diagnosticEngine + self._configuration = configuration + self.dataProvider.delegate = self for bundle in dataProvider.bundles.values { @@ -451,7 +415,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { /// - localBundleID: The local bundle ID, used to identify and skip absolute fully qualified local links. private func preResolveExternalLinks(semanticObjects: [ReferencedSemanticObject], localBundleID: BundleIdentifier) { // If there are no external resolvers added we will not resolve any links. - guard !externalDocumentationSources.isEmpty else { return } + guard !configuration.externalDocumentationConfiguration.sources.isEmpty else { return } let collectedExternalLinks = Synchronized([String: Set]()) semanticObjects.concurrentPerform { _, semantic in @@ -473,7 +437,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { } for (bundleID, collectedLinks) in collectedExternalLinks.sync({ $0 }) { - guard let externalResolver = externalDocumentationSources[bundleID] else { + guard let externalResolver = configuration.externalDocumentationConfiguration.sources[bundleID] else { continue } for externalLink in collectedLinks { @@ -539,7 +503,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { documentationCache.reference(symbolID: origin.identifier) } // Check if we should skip resolving links for inherited documentation from other modules. - if !externalMetadata.inheritDocs, + if !configuration.externalMetadata.inheritDocs, let symbolOriginReference, inheritsDocumentationFromOtherModule(documentationNode, symbolOriginReference: symbolOriginReference) { @@ -825,7 +789,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { let document = Document(parsing: source, source: url, options: [.parseBlockDirectives, .parseSymbolLinks]) // Check for non-inclusive language in all types of docs if that diagnostic severity is required. - if externalMetadata.diagnosticLevel >= NonInclusiveLanguageChecker.severity { + if configuration.externalMetadata.diagnosticLevel >= NonInclusiveLanguageChecker.severity { var langChecker = NonInclusiveLanguageChecker(sourceFile: url) langChecker.visit(document) diagnosticEngine.emit(langChecker.problems) @@ -1265,7 +1229,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { uncuratedDocumentationExtensions[resolved] = documentationExtension } case .failure(_, let errorInfo): - guard !considerDocumentationExtensionsThatDoNotMatchSymbolsAsResolved else { + guard !configuration.convertServiceConfiguration.considerDocumentationExtensionsThatDoNotMatchSymbolsAsResolved else { // The ConvertService relies on old implementation detail where documentation extension files were always considered "resolved" even when they didn't match a symbol. // // Don't rely on this behavior for new functionality. The behavior will be removed once we have a new solution to meets the needs of the ConvertService. (rdar://108563483) @@ -1579,7 +1543,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { in symbols: [String: UnifiedSymbolGraph.Symbol], relationships: [UnifiedSymbolGraph.Selector: Set] ) throws { - if globalExternalSymbolResolver == nil, linkResolver.externalResolvers.isEmpty { + if configuration.externalDocumentationConfiguration.globalSymbolResolver == nil, linkResolver.externalResolvers.isEmpty { // Context has no mechanism for resolving external symbol links. No reason to gather any symbols to resolve. return } @@ -1611,7 +1575,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { // TODO: When the symbol graph includes the precise identifiers for conditional availability, those symbols should also be resolved (rdar://63768609). func resolveSymbol(symbolID: String) -> (ResolvedTopicReference, LinkResolver.ExternalEntity)? { - if let globalResult = globalExternalSymbolResolver?.symbolReferenceAndEntity(withPreciseIdentifier: symbolID) { + if let globalResult = configuration.externalDocumentationConfiguration.globalSymbolResolver?.symbolReferenceAndEntity(withPreciseIdentifier: symbolID) { return globalResult } for externalResolver in linkResolver.externalResolvers.values { @@ -2101,12 +2065,12 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { symbolGraphLoader = SymbolGraphLoader( bundle: bundle, dataProvider: self.dataProvider, - configureSymbolGraph: configureSymbolGraph + symbolGraphTransformer: configuration.convertServiceConfiguration.symbolGraphTransformer ) do { try symbolGraphLoader.loadAll() - let pathHierarchy = PathHierarchy(symbolGraphLoader: symbolGraphLoader, bundleName: urlReadablePath(bundle.displayName), knownDisambiguatedPathComponents: knownDisambiguatedSymbolPathComponents) + let pathHierarchy = PathHierarchy(symbolGraphLoader: symbolGraphLoader, bundleName: urlReadablePath(bundle.displayName), knownDisambiguatedPathComponents: configuration.convertServiceConfiguration.knownDisambiguatedSymbolPathComponents) hierarchyBasedResolver = PathHierarchyBasedLinkResolver(pathHierarchy: pathHierarchy) } catch { // Pipe the error out of the dispatch queue. @@ -2154,7 +2118,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { discoveryGroup.async(queue: discoveryQueue) { [unowned self] in do { - try linkResolver.loadExternalResolvers() + try linkResolver.loadExternalResolvers(dependencyArchives: configuration.externalDocumentationConfiguration.dependencyArchives) } catch { // Pipe the error out of the dispatch queue. discoveryError.sync({ @@ -2232,7 +2196,10 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { try shouldContinueRegistration() - if topicGraph.nodes.isEmpty, !otherArticles.isEmpty, !allowsRegisteringArticlesWithoutTechnologyRoot { + if topicGraph.nodes.isEmpty, + !otherArticles.isEmpty, + !configuration.convertServiceConfiguration.allowsRegisteringArticlesWithoutTechnologyRoot + { synthesizeArticleOnlyRootPage(articles: &otherArticles, bundle: bundle) } @@ -2247,7 +2214,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { // Articles that will be automatically curated can be resolved but they need to be pre registered before resolving links. let rootNodeForAutomaticCuration = soleRootModuleReference.flatMap(topicGraph.nodeWithReference(_:)) - if allowsRegisteringArticlesWithoutTechnologyRoot || rootNodeForAutomaticCuration != nil { + if configuration.convertServiceConfiguration.allowsRegisteringArticlesWithoutTechnologyRoot || rootNodeForAutomaticCuration != nil { otherArticles = registerArticles(otherArticles, in: bundle) try shouldContinueRegistration() } @@ -2279,7 +2246,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { var allCuratedReferences = try crawlSymbolCuration(in: linkResolver.localResolver.topLevelSymbols(), bundle: bundle) // Store the list of manually curated references if doc coverage is on. - if shouldStoreManuallyCuratedReferences { + if configuration.experimentalCoverageConfiguration.shouldStoreManuallyCuratedReferences { manuallyCuratedReferences = allCuratedReferences } @@ -2317,7 +2284,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { preResolveExternalLinks(references: Array(allCuratedReferences), localBundleID: bundle.identifier) resolveLinks(curatedReferences: allCuratedReferences, bundle: bundle) - if convertServiceFallbackResolver != nil { + if configuration.convertServiceConfiguration.fallbackResolver != nil { // When the ``ConvertService`` builds documentation for a single page there won't be a module or root // reference to auto-curate the page under, so the regular local link resolution code path won't visit // the single page. To ensure that links are resolved, explicitly visit all pages. @@ -2664,8 +2631,8 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { } private func externalEntity(with reference: ResolvedTopicReference) -> LinkResolver.ExternalEntity? { - return externalDocumentationSources[reference.bundleIdentifier].map({ $0.entity(with: reference) }) - ?? convertServiceFallbackResolver?.entityIfPreviouslyResolved(with: reference) + return configuration.externalDocumentationConfiguration.sources[reference.bundleIdentifier].map({ $0.entity(with: reference) }) + ?? configuration.convertServiceConfiguration.fallbackResolver?.entityIfPreviouslyResolved(with: reference) } /** @@ -2857,7 +2824,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { return localAsset } - if let fallbackAssetResolver = convertServiceFallbackResolver, + if let fallbackAssetResolver = configuration.convertServiceConfiguration.fallbackResolver, let externallyResolvedAsset = fallbackAssetResolver.resolve(assetNamed: name) { assetManagers[bundleIdentifier, default: DataAssetManager()] .register(dataAsset: externallyResolvedAsset, forName: name) @@ -2890,7 +2857,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { if let assetManager = assetManagers[parent.bundleIdentifier] { if let localName = assetManager.bestKey(forAssetName: name) { return localName - } else if let fallbackAssetManager = convertServiceFallbackResolver { + } else if let fallbackAssetManager = configuration.convertServiceConfiguration.fallbackResolver { return fallbackAssetManager.resolve(assetNamed: name) != nil ? name : nil } return nil diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationConverter.swift b/Sources/SwiftDocC/Infrastructure/DocumentationConverter.swift index d075f14f16..0f9a240e36 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationConverter.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationConverter.swift @@ -164,15 +164,6 @@ public struct DocumentationConverter: DocumentationConverterProtocol { self.diagnosticEngine = diagnosticEngine self.symbolIdentifiersWithExpandedDocumentation = symbolIdentifiersWithExpandedDocumentation self.experimentalModifyCatalogWithGeneratedCuration = experimentalModifyCatalogWithGeneratedCuration - - // Inject current platform versions if provided - if var currentPlatforms { - // Add missing platforms if their fallback platform is present. - for (platform, fallbackPlatform) in DefaultAvailability.fallbackPlatforms where currentPlatforms[platform.displayName] == nil { - currentPlatforms[platform.displayName] = currentPlatforms[fallbackPlatform.displayName] - } - self.context.externalMetadata.currentPlatforms = currentPlatforms - } } /// Returns the first bundle in the source directory, if any. diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationCurator.swift b/Sources/SwiftDocC/Infrastructure/DocumentationCurator.swift index f2677e8cff..4e14268ba4 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationCurator.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationCurator.swift @@ -81,7 +81,7 @@ struct DocumentationCurator { // Check if the link has been externally resolved already. if let bundleID = unresolved.topicURL.components.host, - context.externalDocumentationSources[bundleID] != nil || context.convertServiceFallbackResolver != nil { + context.configuration.externalDocumentationConfiguration.sources[bundleID] != nil || context.configuration.convertServiceConfiguration.fallbackResolver != nil { if case .success(let resolvedExternalReference) = context.externallyResolvedLinks[unresolved.topicURL] { return resolvedExternalReference } else { diff --git a/Sources/SwiftDocC/Infrastructure/External Data/ExternalMetadata.swift b/Sources/SwiftDocC/Infrastructure/External Data/ExternalMetadata.swift index 11f32b6639..9360fb25ce 100644 --- a/Sources/SwiftDocC/Infrastructure/External Data/ExternalMetadata.swift +++ b/Sources/SwiftDocC/Infrastructure/External Data/ExternalMetadata.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-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 @@ -80,7 +80,5 @@ public struct ExternalMetadata { public var isGeneratedBundle = false /// The granularity of diagnostics to emit via the engine. - /// - /// > Note: This setting is set by the convert command. public var diagnosticLevel: DiagnosticSeverity = .warning } diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver.swift index da02eac648..c64f95d766 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver.swift @@ -13,10 +13,6 @@ import SymbolKit /// A class that resolves documentation links by orchestrating calls to other link resolver implementations. public class LinkResolver { - /// A list of URLs to documentation archives that the local documentation depends on. - @_spi(ExternalLinks) // This needs to be public SPI so that the ConvertAction can set it. - public var dependencyArchives: [URL] = [] - var fileManager: FileManagerProtocol = FileManager.default /// The link resolver to use to resolve links in the local bundle var localResolver: PathHierarchyBasedLinkResolver! @@ -28,7 +24,8 @@ public class LinkResolver { var externalResolvers: [String: ExternalPathHierarchyResolver] = [:] /// Create link resolvers for all documentation archive dependencies. - func loadExternalResolvers() throws { + /// - Parameter dependencyArchives: A list of URLs to documentation archives that the local documentation depends on. + func loadExternalResolvers(dependencyArchives: [URL]) throws { let resolvers = try dependencyArchives.compactMap { try ExternalPathHierarchyResolver(dependencyArchive: $0, fileManager: fileManager) } @@ -150,7 +147,7 @@ private extension LinkResolver { // because their markup or symbol information wasn't passed as catalog or symbol graph input to DocC. context.externallyResolvedLinks[linkText] = result if case .success(let reference) = result { - context.externalCache[reference] = context.convertServiceFallbackResolver?.entityIfPreviouslyResolved(with: reference) + context.externalCache[reference] = context.configuration.convertServiceConfiguration.fallbackResolver?.entityIfPreviouslyResolved(with: reference) } } } @@ -169,7 +166,7 @@ private final class FallbackResolverBasedLinkResolver { private func resolve(_ unresolvedReference: UnresolvedTopicReference, in parent: ResolvedTopicReference, fromSymbolLink isCurrentlyResolvingSymbolLink: Bool, context: DocumentationContext) -> TopicReferenceResolutionResult? { // Check if a fallback reference resolver should resolve this let referenceBundleIdentifier = unresolvedReference.bundleIdentifier ?? parent.bundleIdentifier - guard let fallbackResolver = context.convertServiceFallbackResolver, + guard let fallbackResolver = context.configuration.convertServiceConfiguration.fallbackResolver, let knownBundleIdentifier = context.registeredBundles.first(where: { $0.identifier == referenceBundleIdentifier || urlReadablePath($0.displayName) == referenceBundleIdentifier })?.identifier, fallbackResolver.bundleIdentifier == knownBundleIdentifier else { diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift index 5cf488e502..047ae9acf4 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift @@ -288,7 +288,7 @@ final class PathHierarchyBasedLinkResolver { let symbol = unifiedSymbol let uniqueIdentifier = unifiedSymbol.uniqueIdentifier - if let pathComponents = context.knownDisambiguatedSymbolPathComponents?[uniqueIdentifier], + if let pathComponents = context.configuration.convertServiceConfiguration.knownDisambiguatedSymbolPathComponents?[uniqueIdentifier], let componentsCount = symbol.defaultSymbol?.pathComponents.count, pathComponents.count == componentsCount { diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift index 6dc01370ab..45826009a8 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift @@ -21,7 +21,7 @@ struct SymbolGraphLoader { private(set) var graphLocations: [String: [SymbolKit.GraphCollector.GraphKind]] = [:] private var dataProvider: DocumentationContextDataProvider private var bundle: DocumentationBundle - private var configureSymbolGraph: ((inout SymbolGraph) -> ())? = nil + private var symbolGraphTransformer: ((inout SymbolGraph) -> ())? = nil /// Creates a new loader, initialized with the given bundle. /// - Parameters: @@ -30,11 +30,11 @@ struct SymbolGraphLoader { init( bundle: DocumentationBundle, dataProvider: DocumentationContextDataProvider, - configureSymbolGraph: ((inout SymbolGraph) -> ())? = nil + symbolGraphTransformer: ((inout SymbolGraph) -> ())? = nil ) { self.bundle = bundle self.dataProvider = dataProvider - self.configureSymbolGraph = configureSymbolGraph + self.symbolGraphTransformer = symbolGraphTransformer } /// A strategy to decode symbol graphs. @@ -76,7 +76,7 @@ struct SymbolGraphLoader { symbolGraph = try SymbolGraphConcurrentDecoder.decode(data) } - configureSymbolGraph?(&symbolGraph) + symbolGraphTransformer?(&symbolGraph) let (moduleName, isMainSymbolGraph) = Self.moduleNameFor(symbolGraph, at: symbolGraphURL) // If the bundle provides availability defaults add symbol availability data. diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphRelationshipsBuilder.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphRelationshipsBuilder.swift index a6b69ce8ce..4ab6a6278c 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphRelationshipsBuilder.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphRelationshipsBuilder.swift @@ -381,7 +381,10 @@ struct SymbolGraphRelationshipsBuilder { // Remove any inherited docs from the original symbol if the feature is disabled. // However, when the docs are inherited from within the same module, its content can be resolved in // the local context, so keeping those inherited docs provide a better user experience. - if !context.externalMetadata.inheritDocs, let unifiedSymbol = inherited.unifiedSymbol, unifiedSymbol.documentedSymbol?.isDocCommentFromSameModule(symbolModuleName: moduleName) == false { + if !context.configuration.externalMetadata.inheritDocs, + let unifiedSymbol = inherited.unifiedSymbol, + unifiedSymbol.documentedSymbol?.isDocCommentFromSameModule(symbolModuleName: moduleName) == false + { unifiedSymbol.docComment.removeAll() } } diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift b/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift index af56bd238b..127a17866c 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift @@ -129,9 +129,9 @@ public struct DefaultAvailability: Codable, Equatable { /// Fallback availability information for platforms we either don't emit SGFs for /// or have the same availability information as another platform. - static let fallbackPlatforms: [PlatformName : PlatformName] = [ - .catalyst:.iOS, - .iPadOS:.iOS + package static let fallbackPlatforms: [PlatformName: PlatformName] = [ + .catalyst: .iOS, + .iPadOS: .iOS, ] /// Creates a default availability module. diff --git a/Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift b/Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift index eb2c6badee..25bff51df3 100644 --- a/Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift +++ b/Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift @@ -209,9 +209,10 @@ public class DocumentationContentRenderer { // We verify that this is a symbol with defined availability // and that we're feeding in a current set of platforms to the context. guard let symbol = node.semantic as? Symbol, - let currentPlatforms = documentationContext.externalMetadata.currentPlatforms, - !currentPlatforms.isEmpty, - let symbolAvailability = symbol.availability else { return false } + let currentPlatforms = documentationContext.configuration.externalMetadata.currentPlatforms, + !currentPlatforms.isEmpty, + let symbolAvailability = symbol.availability + else { return false } // Verify that if current platforms are in beta, they match the introduced version of the symbol for availability in symbolAvailability.availability { @@ -223,9 +224,10 @@ public class DocumentationContentRenderer { // If we don't have introduced and current versions for the current platform // we can't tell if the symbol is beta. guard let name = availability.domain.map({ PlatformName(operatingSystemName: $0.rawValue) }), - // Use the display name of the platform when looking up the current platforms - // as we expect that form on the command line. - let current = documentationContext.externalMetadata.currentPlatforms?[name.displayName] else { + // Use the display name of the platform when looking up the current platforms + // as we expect that form on the command line. + let current = documentationContext.configuration.externalMetadata.currentPlatforms?[name.displayName] + else { return false } diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift index 9c19000123..d71d811d2e 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift @@ -141,7 +141,7 @@ public struct RenderNodeTranslator: SemanticVisitor { node.hierarchy = hierarchy.hierarchy node.metadata.category = technology.name node.metadata.categoryPathComponent = hierarchy.technology.url.lastPathComponent - } else if !context.allowsRegisteringArticlesWithoutTechnologyRoot { + } else if !context.configuration.convertServiceConfiguration.allowsRegisteringArticlesWithoutTechnologyRoot { // This tutorial is not curated, so we don't generate a render node. // We've warned about this during semantic analysis. return nil @@ -839,7 +839,7 @@ public struct RenderNodeTranslator: SemanticVisitor { if let availability = article.metadata?.availability, !availability.isEmpty { let renderAvailability = availability.compactMap({ let currentPlatform = PlatformName(metadataPlatform: $0.platform).flatMap { name in - context.externalMetadata.currentPlatforms?[name.displayName] + context.configuration.externalMetadata.currentPlatforms?[name.displayName] } return .init($0, current: currentPlatform) }).sorted(by: AvailabilityRenderOrder.compare) @@ -1246,7 +1246,7 @@ public struct RenderNodeTranslator: SemanticVisitor { return nil } guard let name = availability.domain.map({ PlatformName(operatingSystemName: $0.rawValue) }), - let currentPlatform = context.externalMetadata.currentPlatforms?[name.displayName] else { + let currentPlatform = context.configuration.externalMetadata.currentPlatforms?[name.displayName] else { // No current platform provided by the context return AvailabilityRenderItem(availability, current: nil) } @@ -1256,7 +1256,7 @@ public struct RenderNodeTranslator: SemanticVisitor { .filter({ !($0.unconditionallyUnavailable == true) }) .sorted(by: AvailabilityRenderOrder.compare) } ?? .init(defaultValue: - defaultAvailability(for: bundle, moduleName: moduleName.symbolName, currentPlatforms: context.externalMetadata.currentPlatforms)? + defaultAvailability(for: bundle, moduleName: moduleName.symbolName, currentPlatforms: context.configuration.externalMetadata.currentPlatforms)? .filter({ !($0.unconditionallyUnavailable == true) }) .sorted(by: AvailabilityRenderOrder.compare) ) @@ -1264,7 +1264,7 @@ public struct RenderNodeTranslator: SemanticVisitor { if let availability = documentationNode.metadata?.availability, !availability.isEmpty { let renderAvailability = availability.compactMap({ let currentPlatform = PlatformName(metadataPlatform: $0.platform).flatMap { name in - context.externalMetadata.currentPlatforms?[name.displayName] + context.configuration.externalMetadata.currentPlatforms?[name.displayName] } return .init($0, current: currentPlatform) }).sorted(by: AvailabilityRenderOrder.compare) @@ -1330,7 +1330,10 @@ public struct RenderNodeTranslator: SemanticVisitor { // In case `inheritDocs` is disabled and there is actually origin data for the symbol, then include origin information as abstract. // Generate the placeholder abstract only in case there isn't an authored abstract coming from a doc extension. - if !context.externalMetadata.inheritDocs, let origin = (documentationNode.semantic as! Symbol).origin, symbol.abstractSection == nil { + if !context.configuration.externalMetadata.inheritDocs, + let origin = (documentationNode.semantic as! Symbol).origin, + symbol.abstractSection == nil + { // Create automatic abstract for inherited symbols. node.abstract = [.text("Inherited from "), .codeVoice(code: origin.displayName), .text(".")] } else { diff --git a/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift b/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift index 17d15ad64a..6328b60d98 100644 --- a/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift +++ b/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift @@ -52,18 +52,7 @@ public struct ConvertAction: Action, RecreatingContext { let sourceRepository: SourceRepository? - private(set) var context: DocumentationContext { - didSet { - // current platforms? - - switch documentationCoverageOptions.level { - case .detailed, .brief: - self.context.shouldStoreManuallyCuratedReferences = true - case .none: - break - } - } - } + private(set) var context: DocumentationContext private let workspace: DocumentationWorkspace private var currentDataProvider: DocumentationWorkspaceDataProvider? private var injectedDataProvider: DocumentationWorkspaceDataProvider? @@ -192,22 +181,26 @@ public struct ConvertAction: Action, RecreatingContext { } self.diagnosticEngine = engine - self.context = try context ?? DocumentationContext(dataProvider: workspace, diagnosticEngine: engine) self.diagnosticLevel = filterLevel - self.context.externalMetadata.diagnosticLevel = self.diagnosticLevel - self.context.linkResolver.dependencyArchives = dependencies + var configuration = context?.configuration ?? DocumentationContext.Configuration() + + configuration.externalMetadata.diagnosticLevel = filterLevel // Inject current platform versions if provided - if let currentPlatforms { - self.context.externalMetadata.currentPlatforms = currentPlatforms + if var currentPlatforms { + // Add missing platforms if their fallback platform is present. + for (platform, fallbackPlatform) in DefaultAvailability.fallbackPlatforms where currentPlatforms[platform.displayName] == nil { + currentPlatforms[platform.displayName] = currentPlatforms[fallbackPlatform.displayName] + } + configuration.externalMetadata.currentPlatforms = currentPlatforms } // Inject user-set flags. - self.context.externalMetadata.inheritDocs = inheritDocs + configuration.externalMetadata.inheritDocs = inheritDocs switch documentationCoverageOptions.level { case .detailed, .brief: - self.context.shouldStoreManuallyCuratedReferences = true + configuration.experimentalCoverageConfiguration.shouldStoreManuallyCuratedReferences = true case .none: break } @@ -221,12 +214,22 @@ public struct ConvertAction: Action, RecreatingContext { allowArbitraryCatalogDirectories: allowArbitraryCatalogDirectories ) } else { - self.context.externalMetadata.isGeneratedBundle = true + configuration.externalMetadata.isGeneratedBundle = true dataProvider = GeneratedDataProvider(symbolGraphDataLoader: { url in fileManager.contents(atPath: url.path) }) } + if let outOfProcessResolver { + configuration.externalDocumentationConfiguration.sources[outOfProcessResolver.bundleIdentifier] = outOfProcessResolver + configuration.externalDocumentationConfiguration.globalSymbolResolver = outOfProcessResolver + } + configuration.externalDocumentationConfiguration.dependencyArchives = dependencies + + (context as _DeprecatedConfigurationSetAccess?)?.configuration = configuration + + self.context = try context ?? DocumentationContext(dataProvider: workspace, diagnosticEngine: engine, configuration: configuration) + self.converter = DocumentationConverter( documentationBundleURL: documentationBundleURL, emitDigest: emitDigest, @@ -403,14 +406,7 @@ public struct ConvertAction: Action, RecreatingContext { diagnosticEngine.flush() } - if let outOfProcessResolver { - context.externalDocumentationSources[outOfProcessResolver.bundleIdentifier] = outOfProcessResolver - context.globalExternalSymbolResolver = outOfProcessResolver - } - - let temporaryFolder = try createTempFolder( - with: htmlTemplateDirectory) - + let temporaryFolder = try createTempFolder(with: htmlTemplateDirectory) defer { try? fileManager.removeItem(at: temporaryFolder) @@ -609,3 +605,8 @@ public struct ConvertAction: Action, RecreatingContext { return try Self.moveOutput(from: from, to: to, fileManager: fileManager) } } + +private protocol _DeprecatedConfigurationSetAccess: AnyObject { + var configuration: DocumentationContext.Configuration { get set } +} +extension DocumentationContext: _DeprecatedConfigurationSetAccess {} diff --git a/Tests/SwiftDocCTests/Benchmark/TopicGraphHashTests.swift b/Tests/SwiftDocCTests/Benchmark/TopicGraphHashTests.swift index c02daad552..a3aa445d1b 100644 --- a/Tests/SwiftDocCTests/Benchmark/TopicGraphHashTests.swift +++ b/Tests/SwiftDocCTests/Benchmark/TopicGraphHashTests.swift @@ -79,32 +79,30 @@ class TopicGraphHashTests: XCTestCase { /// Verify that we safely produce the topic graph hash when external symbols /// participate in the documentation hierarchy. rdar://76419740 func testProducesTopicGraphHashWhenResolvedExternalReferencesInTaskGroups() throws { - // Copy the test bundle and add external links to the MyKit Topics. - let workspace = DocumentationWorkspace() - let (tempURL, _, _) = try testBundleAndContext(copying: "TestBundle") + let resolver = TestMultiResultExternalReferenceResolver() + resolver.entitiesToReturn = [ + "/article": .success(.init(referencePath: "/externally/resolved/path/to/article")), + "/article2": .success(.init(referencePath: "/externally/resolved/path/to/article2")), + + "/externally/resolved/path/to/article": .success(.init(referencePath: "/externally/resolved/path/to/article")), + "/externally/resolved/path/to/article2": .success(.init(referencePath: "/externally/resolved/path/to/article2")), + ] - try """ - # ``MyKit`` - MyKit module root symbol - ## Topics - ### Task Group - - - - - - - - - """.write(to: tempURL.appendingPathComponent("documentation").appendingPathComponent("mykit.md"), atomically: true, encoding: .utf8) - - // Load the new test bundle - let dataProvider = try LocalFileSystemDataProvider(rootURL: tempURL) - guard let bundle = try dataProvider.bundles().first else { - XCTFail("Failed to create a temporary test bundle") - return + let (_, bundle, context) = try testBundleAndContext(copying: "TestBundle", externalResolvers: [ + "com.external.testbundle" : resolver + ]) { url in + // Add external links to the MyKit Topics. + try """ + # ``MyKit`` + MyKit module root symbol + ## Topics + ### Task Group + - + - + - + - + """.write(to: url.appendingPathComponent("documentation").appendingPathComponent("mykit.md"), atomically: true, encoding: .utf8) } - try workspace.registerProvider(dataProvider) - let context = try DocumentationContext(dataProvider: workspace) - - // Add external resolver - context.externalDocumentationSources = ["com.external.testbundle" : ExternalReferenceResolverTests.TestExternalReferenceResolver()] // Get MyKit symbol let entity = try context.entity(with: .init(bundleIdentifier: bundle.identifier, path: "/documentation/MyKit", sourceLanguage: .swift)) @@ -114,8 +112,8 @@ class TopicGraphHashTests: XCTestCase { XCTAssertEqual(taskGroupLinks, [ "doc://org.swift.docc.example/documentation/Test-Bundle/article", "doc://org.swift.docc.example/documentation/Test-Bundle/article2", - "doc://com.external.testbundle/article", - "doc://com.external.testbundle/article2", + "doc://com.external.testbundle/externally/resolved/path/to/article", + "doc://com.external.testbundle/externally/resolved/path/to/article2", ]) // Verify correct hierarchy under `MyKit` in the topic graph dump including external symbols. diff --git a/Tests/SwiftDocCTests/Infrastructure/ExternalPathHierarchyResolverTests.swift b/Tests/SwiftDocCTests/Infrastructure/ExternalPathHierarchyResolverTests.swift index 809a29225f..1adc33e3d1 100644 --- a/Tests/SwiftDocCTests/Infrastructure/ExternalPathHierarchyResolverTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/ExternalPathHierarchyResolverTests.swift @@ -722,6 +722,9 @@ class ExternalPathHierarchyResolverTests: XCTestCase { XCTAssertEqual(linkResolutionInformation.pathHierarchy.nodes.count - linkResolutionInformation.nonSymbolPaths.count, 5 /* 4 symbols & 1 module */) XCTAssertEqual(linkSummaries.count, 5 /* 4 symbols & 1 module */) + var configuration = DocumentationContext.Configuration() + configuration.externalDocumentationConfiguration.dependencyArchives = [URL(fileURLWithPath: "/Dependency.doccarchive")] + // After building the dependency, let (mainBundle, mainContext) = try loadBundle( catalog: Folder(name: "Main.docc", content: [ @@ -788,9 +791,7 @@ class ExternalPathHierarchyResolverTests: XCTestCase { JSONFile(name: "link-hierarchy.json", content: linkResolutionInformation), ]) ], - configureContext: { - $0.linkResolver.dependencyArchives = [URL(fileURLWithPath: "/Dependency.doccarchive")] - } + configuration: configuration ) XCTAssertEqual(mainContext.knownPages.count, 3 /* 2 symbols & 1 module*/) @@ -882,9 +883,10 @@ class ExternalPathHierarchyResolverTests: XCTestCase { "Mac Catalyst": PlatformVersion(VersionTriplet(4, 0, 0), beta: true), "iPadOS": PlatformVersion(VersionTriplet(4, 0, 0), beta: true), ] - let linkResolvers = try makeLinkResolversForTestBundle(named: "AvailabilityBetaBundle") { context in - context.externalMetadata.currentPlatforms = platformMetadata - } + var configuration = DocumentationContext.Configuration() + + configuration.externalMetadata.currentPlatforms = platformMetadata + let linkResolvers = try makeLinkResolversForTestBundle(named: "AvailabilityBetaBundle", configuration: configuration) // MyClass is only available on beta platforms (macos=1.0.0, watchos=2.0.0, tvos=3.0.0, ios=4.0.0) try linkResolvers.assertBetaStatus(authoredLink: "/MyKit/MyClass", isBeta: true) @@ -989,9 +991,9 @@ class ExternalPathHierarchyResolverTests: XCTestCase { } } - private func makeLinkResolversForTestBundle(named testBundleName: String, configureContext: ((DocumentationContext) throws -> Void)? = nil) throws -> LinkResolvers { + private func makeLinkResolversForTestBundle(named testBundleName: String, configuration: DocumentationContext.Configuration = .init()) throws -> LinkResolvers { let bundleURL = try XCTUnwrap(Bundle.module.url(forResource: testBundleName, withExtension: "docc", subdirectory: "Test Bundles")) - let (_, bundle, context) = try loadBundle(from: bundleURL, configureContext: configureContext) + let (_, bundle, context) = try loadBundle(from: bundleURL, configuration: configuration) let localResolver = try XCTUnwrap(context.linkResolver.localResolver) diff --git a/Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift b/Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift index b464b0f9c0..fd1dbecd54 100644 --- a/Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift @@ -76,8 +76,9 @@ class ExternalReferenceResolverTests: XCTestCase { let bundle = try XCTUnwrap(automaticDataProvider.bundles().first) let workspace = DocumentationWorkspace() - let context = try DocumentationContext(dataProvider: workspace) - context.externalDocumentationSources = ["com.external.testbundle" : TestExternalReferenceResolver()] + var configuration = DocumentationContext.Configuration() + configuration.externalDocumentationConfiguration.sources = ["com.external.testbundle" : TestExternalReferenceResolver()] + let context = try DocumentationContext(dataProvider: workspace, configuration: configuration) let dataProvider = PrebuiltLocalFileSystemDataProvider(bundles: [bundle]) try workspace.registerProvider(dataProvider) @@ -167,6 +168,9 @@ class ExternalReferenceResolverTests: XCTestCase { ) } + // This test verifies the behavior of a deprecated functionality (changing external documentation sources after registering the documentation) + // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. + @available(*, deprecated) func testResolvesReferencesExternallyOnlyWhenFallbackResolversAreSet() throws { let workspace = DocumentationWorkspace() let bundle = try testBundle(named: "TestBundle") @@ -179,8 +183,8 @@ class ExternalReferenceResolverTests: XCTestCase { let parent = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "", sourceLanguage: .swift) do { - context.externalDocumentationSources = [:] - context.convertServiceFallbackResolver = nil + context.configuration.externalDocumentationConfiguration.sources = [:] + context.configuration.convertServiceConfiguration.fallbackResolver = nil if case .success = context.resolve(.unresolved(unresolved), in: parent) { XCTFail("The reference was unexpectedly resolved.") @@ -207,8 +211,8 @@ class ExternalReferenceResolverTests: XCTestCase { } } - context.externalDocumentationSources = [:] - context.convertServiceFallbackResolver = TestFallbackResolver(bundleIdentifier: "org.swift.docc.example") + context.configuration.externalDocumentationConfiguration.sources = [:] + context.configuration.convertServiceConfiguration.fallbackResolver = TestFallbackResolver(bundleIdentifier: "org.swift.docc.example") guard case let .success(resolved) = context.resolve(.unresolved(unresolved), in: parent) else { XCTFail("The reference was unexpectedly unresolved.") @@ -222,7 +226,7 @@ class ExternalReferenceResolverTests: XCTestCase { XCTAssertEqual(expectedURL, resolved.url) try workspace.unregisterProvider(dataProvider) - context.externalDocumentationSources = [:] + context.configuration.externalDocumentationConfiguration.sources = [:] guard case .failure = context.resolve(.unresolved(unresolved), in: parent) else { XCTFail("Unexpectedly resolved \(unresolved.topicURL) despite removing a data provider for it") return @@ -231,17 +235,10 @@ class ExternalReferenceResolverTests: XCTestCase { } func testLoadEntityForExternalReference() throws { - let workspace = DocumentationWorkspace() - let bundle = try testBundle(named: "TestBundle") - let dataProvider = PrebuiltLocalFileSystemDataProvider(bundles: [bundle]) - try workspace.registerProvider(dataProvider) - let context = try DocumentationContext(dataProvider: workspace) - context.externalDocumentationSources = ["com.external.testbundle" : TestExternalReferenceResolver()] - + let (_, context) = try testBundleAndContext(named: "TestBundle", externalResolvers: ["com.external.testbundle" : TestExternalReferenceResolver()]) let identifier = ResolvedTopicReference(bundleIdentifier: "com.external.testbundle", path: "/externally/resolved/path", sourceLanguage: .swift) XCTAssertThrowsError(try context.entity(with: ResolvedTopicReference(bundleIdentifier: "some.other.bundle", path: identifier.path, sourceLanguage: .swift))) - XCTAssertThrowsError(try context.entity(with: identifier)) } @@ -263,20 +260,13 @@ class ExternalReferenceResolverTests: XCTestCase { for fixture in fixtures { let (resolvedEntityKind, renderNodeKind) = fixture - let workspace = DocumentationWorkspace() - let context = try DocumentationContext(dataProvider: workspace) - let externalResolver = TestExternalReferenceResolver() externalResolver.bundleIdentifier = "com.test.external" externalResolver.expectedReferencePath = "/path/to/external/symbol" externalResolver.resolvedEntityTitle = "ClassName" externalResolver.resolvedEntityKind = resolvedEntityKind - context.externalDocumentationSources = [externalResolver.bundleIdentifier: externalResolver] - let bundle = try testBundle(named: "TestBundle") - - let dataProvider = PrebuiltLocalFileSystemDataProvider(bundles: [bundle]) - try workspace.registerProvider(dataProvider) + let (bundle, context) = try testBundleAndContext(named: "TestBundle", externalResolvers: [externalResolver.bundleIdentifier: externalResolver]) let converter = DocumentationNodeConverter(bundle: bundle, context: context) let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/tutorials/Test-Bundle/TestTutorial", sourceLanguage: .swift)) @@ -361,8 +351,7 @@ class ExternalReferenceResolverTests: XCTestCase { externalResolver.resolvedEntityTitle = "ClassName" externalResolver.resolvedEntityKind = .class - let tempFolder = try createTempFolder(content: [ - Folder(name: "SingleArticleWithExternalLink.docc", content: [ + let tempFolder = Folder(name: "SingleArticleWithExternalLink.docc", content: [ TextFile(name: "article.md", utf8Content: """ # Article with external link @@ -372,15 +361,11 @@ class ExternalReferenceResolverTests: XCTestCase { Link to an external page: """) - ]) ]) - let workspace = DocumentationWorkspace() - let context = try DocumentationContext(dataProvider: workspace) - context.externalDocumentationSources = [externalResolver.bundleIdentifier: externalResolver] - let dataProvider = try LocalFileSystemDataProvider(rootURL: tempFolder) - try workspace.registerProvider(dataProvider) - let bundle = try XCTUnwrap(workspace.bundles.first?.value) + var configuration = DocumentationContext.Configuration() + configuration.externalDocumentationConfiguration.sources = [externalResolver.bundleIdentifier: externalResolver] + let (bundle, context) = try loadBundle(catalog: tempFolder, configuration: configuration) let converter = DocumentationNodeConverter(bundle: bundle, context: context) let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/article", sourceLanguage: .swift)) @@ -616,32 +601,30 @@ class ExternalReferenceResolverTests: XCTestCase { // Tests that external references are included in task groups, rdar://72119391 func testResolveExternalReferenceInTaskGroups() throws { - // Copy the test bundle and add external links to the MyKit Topics. - let workspace = DocumentationWorkspace() - let (tempURL, _, _) = try testBundleAndContext(copying: "TestBundle") - - try """ - # ``MyKit`` - MyKit module root symbol - ## Topics - ### Task Group - - - - - - - - - """.write(to: tempURL.appendingPathComponent("documentation").appendingPathComponent("mykit.md"), atomically: true, encoding: .utf8) - - // Load the new test bundle - let dataProvider = try LocalFileSystemDataProvider(rootURL: tempURL) - guard let bundle = try dataProvider.bundles().first else { - XCTFail("Failed to create a temporary test bundle") - return - } - try workspace.registerProvider(dataProvider) - let context = try DocumentationContext(dataProvider: workspace) + let resolver = TestMultiResultExternalReferenceResolver() + resolver.entitiesToReturn = [ + "/article": .success(.init(referencePath: "/externally/resolved/path/to/article")), + "/article2": .success(.init(referencePath: "/externally/resolved/path/to/article2")), + + "/externally/resolved/path/to/article": .success(.init(referencePath: "/externally/resolved/path/to/article")), + "/externally/resolved/path/to/article2": .success(.init(referencePath: "/externally/resolved/path/to/article2")), + ] - // Add external resolver - context.externalDocumentationSources = ["com.external.testbundle" : TestExternalReferenceResolver()] + let (_, bundle, context) = try testBundleAndContext(copying: "TestBundle", externalResolvers: [ + "com.external.testbundle" : resolver + ]) { url in + // Add external links to the MyKit Topics. + try """ + # ``MyKit`` + MyKit module root symbol + ## Topics + ### Task Group + - + - + - + - + """.write(to: url.appendingPathComponent("documentation").appendingPathComponent("mykit.md"), atomically: true, encoding: .utf8) + } // Get MyKit symbol let entity = try context.entity(with: .init(bundleIdentifier: bundle.identifier, path: "/documentation/MyKit", sourceLanguage: .swift)) @@ -651,8 +634,8 @@ class ExternalReferenceResolverTests: XCTestCase { XCTAssertEqual(taskGroupLinks, [ "doc://org.swift.docc.example/documentation/Test-Bundle/article", "doc://org.swift.docc.example/documentation/Test-Bundle/article2", - "doc://com.external.testbundle/article", - "doc://com.external.testbundle/article2", + "doc://com.external.testbundle/externally/resolved/path/to/article", + "doc://com.external.testbundle/externally/resolved/path/to/article2", ]) } diff --git a/Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift b/Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift index c4d3a67c33..d65c2185ea 100644 --- a/Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift @@ -2138,11 +2138,11 @@ class PathHierarchyTests: XCTestCase { } do { - let (_, _, context) = try loadBundle(from: bundleURL) { context in - context.knownDisambiguatedSymbolPathComponents = [ - "s:5MyKit0A5ClassC10myFunctionyyF": ["MyClass-swift.class", "myFunction()"] - ] - } + var configuration = DocumentationContext.Configuration() + configuration.convertServiceConfiguration.knownDisambiguatedSymbolPathComponents = [ + "s:5MyKit0A5ClassC10myFunctionyyF": ["MyClass-swift.class", "myFunction()"] + ] + let (_, _, context) = try loadBundle(from: bundleURL, configuration: configuration) let tree = context.linkResolver.localResolver.pathHierarchy try assertFindsPath("/MyKit/MyClass-swift.class/myFunction()", in: tree, asSymbolID: "s:5MyKit0A5ClassC10myFunctionyyF") @@ -2157,11 +2157,11 @@ class PathHierarchyTests: XCTestCase { } do { - let (_, _, context) = try loadBundle(from: bundleURL) { context in - context.knownDisambiguatedSymbolPathComponents = [ - "s:5MyKit0A5ClassC10myFunctionyyF": ["MyClass-swift.class-hash", "myFunction()"] - ] - } + var configuration = DocumentationContext.Configuration() + configuration.convertServiceConfiguration.knownDisambiguatedSymbolPathComponents = [ + "s:5MyKit0A5ClassC10myFunctionyyF": ["MyClass-swift.class-hash", "myFunction()"] + ] + let (_, _, context) = try loadBundle(from: bundleURL, configuration: configuration) let tree = context.linkResolver.localResolver.pathHierarchy try assertFindsPath("/MyKit/MyClass-swift.class-hash/myFunction()", in: tree, asSymbolID: "s:5MyKit0A5ClassC10myFunctionyyF") @@ -2169,7 +2169,6 @@ class PathHierarchyTests: XCTestCase { try assertPathNotFound("/MyKit/MyClass-swift.class", in: tree) try assertPathNotFound("/MyKit/MyClass-swift.class-hash", in: tree) - XCTAssertEqual(tree.caseInsensitiveDisambiguatedPaths()["s:5MyKit0A5ClassC10myFunctionyyF"], "/MyKit/MyClass-class-hash/myFunction()") diff --git a/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphLoaderTests.swift b/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphLoaderTests.swift index 7031ae7eae..5841b7ebab 100644 --- a/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphLoaderTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphLoaderTests.swift @@ -1677,7 +1677,7 @@ class SymbolGraphLoaderTests: XCTestCase { return SymbolGraphLoader( bundle: bundle, dataProvider: workspace, - configureSymbolGraph: configureSymbolGraph + symbolGraphTransformer: configureSymbolGraph ) } } diff --git a/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift b/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift index 7eb586bc8d..5dcf9e4b63 100644 --- a/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift +++ b/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift @@ -1223,18 +1223,13 @@ class SemaToRenderNodeTests: XCTestCase { } } - let workspace = DocumentationWorkspace() - let context = try DocumentationContext(dataProvider: workspace) - - context.externalDocumentationSources = ["com.test.external": TestReferenceResolver()] - context.globalExternalSymbolResolver = TestSymbolResolver() - let testBundleURL = Bundle.module.url( forResource: "TestBundle", withExtension: "docc", subdirectory: "Test Bundles")! - let dataProvider = try LocalFileSystemDataProvider(rootURL: testBundleURL) - try workspace.registerProvider(dataProvider) - - let bundle = workspace.bundles.values.first! + let (_, bundle, context) = try loadBundle( + from: testBundleURL, + externalResolvers: ["com.test.external": TestReferenceResolver()], + externalSymbolResolver: TestSymbolResolver() + ) // Symbols are loaded XCTAssertFalse(context.documentationCache.isEmpty) @@ -1906,101 +1901,109 @@ Document } func testRendersBetaViolators() throws { - let (bundle, context) = try testBundleAndContext(named: "TestBundle") - - let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift) - let node = try context.entity(with: reference) - let symbol = node.semantic as! Symbol + func makeTestBundle(currentPlatforms: [String : PlatformVersion]?, file: StaticString = #file, line: UInt = #line) throws -> (DocumentationBundle, DocumentationContext, ResolvedTopicReference) { + var configuration = DocumentationContext.Configuration() + configuration.externalMetadata.currentPlatforms = currentPlatforms + + let (_, bundle, context) = try testBundleAndContext(copying: "TestBundle", configuration: configuration) + + let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift) + return (bundle, context, reference) + } // Not a beta platform do { - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) - let renderNode = translator.visitSymbol(symbol) as! RenderNode + let (bundle, context, reference) = try makeTestBundle(currentPlatforms: nil) + + let node = try context.entity(with: reference) + let renderNode = try DocumentationNodeConverter(bundle: bundle, context: context).convert(node) // Verify platform beta was plumbed all the way to the render JSON XCTAssertEqual(renderNode.metadata.platforms?.first?.isBeta, false) } - // Beta platform - context.externalMetadata = ExternalMetadata() - context.externalMetadata.currentPlatforms = ["tvOS": PlatformVersion(VersionTriplet(100, 0, 0), beta: true)] - // Different platform is beta do { - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) - let renderNode = translator.visitSymbol(symbol) as! RenderNode + let (bundle, context, reference) = try makeTestBundle(currentPlatforms: [ + "tvOS": PlatformVersion(VersionTriplet(100, 0, 0), beta: true) + ]) + + let node = try context.entity(with: reference) + let renderNode = try DocumentationNodeConverter(bundle: bundle, context: context).convert(node) // Verify platform beta was plumbed all the way to the render JSON XCTAssertEqual(renderNode.metadata.platforms?.first?.isBeta, false) } // Beta platform but *not* matching the introduced version - context.externalMetadata.currentPlatforms = ["macOS": PlatformVersion(VersionTriplet(100, 0, 0), beta: true)] do { - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) - let renderNode = translator.visitSymbol(symbol) as! RenderNode + let (bundle, context, reference) = try makeTestBundle(currentPlatforms: [ + "macOS": PlatformVersion(VersionTriplet(100, 0, 0), beta: true) + ]) + + let node = try context.entity(with: reference) + let renderNode = try DocumentationNodeConverter(bundle: bundle, context: context).convert(node) // Verify platform beta was plumbed all the way to the render JSON XCTAssertEqual(renderNode.metadata.platforms?.first?.isBeta, false) } // Beta platform matching the introduced version - context.externalMetadata.currentPlatforms = ["macOS": PlatformVersion(VersionTriplet(10, 15, 0), beta: true)] do { - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) - let renderNode = translator.visitSymbol(symbol) as! RenderNode + let (bundle, context, reference) = try makeTestBundle(currentPlatforms: [ + "macOS": PlatformVersion(VersionTriplet(10, 15, 0), beta: true) + ]) + + let node = try context.entity(with: reference) + let renderNode = try DocumentationNodeConverter(bundle: bundle, context: context).convert(node) // Verify platform beta was plumbed all the way to the render JSON XCTAssertEqual(renderNode.metadata.platforms?.first?.isBeta, true) } // Beta platform earlier than the introduced version - context.externalMetadata.currentPlatforms = ["macOS": PlatformVersion(VersionTriplet(10, 14, 0), beta: true)] do { - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) - let renderNode = translator.visitSymbol(symbol) as! RenderNode + let (bundle, context, reference) = try makeTestBundle(currentPlatforms: [ + "macOS": PlatformVersion(VersionTriplet(10, 14, 0), beta: true) + ]) + + let node = try context.entity(with: reference) + let renderNode = try DocumentationNodeConverter(bundle: bundle, context: context).convert(node) // Verify platform beta was plumbed all the way to the render JSON XCTAssertEqual(renderNode.metadata.platforms?.first?.isBeta, true) } // Set only some platforms to beta & the exact version MyClass is being introduced at - context.externalMetadata.currentPlatforms = [ - "macOS": PlatformVersion(VersionTriplet(10, 15, 0), beta: true), - "watchOS": PlatformVersion(VersionTriplet(9, 0, 0), beta: true), - "tvOS": PlatformVersion(VersionTriplet(1, 0, 0), beta: true), - ] do { - let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/MyKit", sourceLanguage: .swift) - let node = try context.entity(with: reference) - let symbol = node.semantic as! Symbol + let (bundle, context, reference) = try makeTestBundle(currentPlatforms: [ + "macOS": PlatformVersion(VersionTriplet(10, 15, 0), beta: true), + "watchOS": PlatformVersion(VersionTriplet(9, 0, 0), beta: true), + "tvOS": PlatformVersion(VersionTriplet(1, 0, 0), beta: true), + ]) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) - let renderNode = translator.visitSymbol(symbol) as! RenderNode + let node = try context.entity(with: reference) + let renderNode = try DocumentationNodeConverter(bundle: bundle, context: context).convert(node) // Verify task group link is beta XCTAssertEqual((renderNode.references["doc://org.swift.docc.example/documentation/MyKit/MyClass"] as? TopicRenderReference)?.isBeta, false) } // Set all platforms to beta & the exact version MyClass is being introduced at to test beta SDK documentation - context.externalMetadata.currentPlatforms = [ - "macOS": PlatformVersion(VersionTriplet(10, 15, 0), beta: true), - "watchOS": PlatformVersion(VersionTriplet(6, 0, 0), beta: true), - "tvOS": PlatformVersion(VersionTriplet(13, 0, 0), beta: true), - "iOS": PlatformVersion(VersionTriplet(13, 0, 0), beta: true), - ] - do { - let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/MyKit", sourceLanguage: .swift) - let node = try context.entity(with: reference) - let symbol = node.semantic as! Symbol + let (bundle, context, reference) = try makeTestBundle(currentPlatforms: [ + "macOS": PlatformVersion(VersionTriplet(10, 15, 0), beta: true), + "watchOS": PlatformVersion(VersionTriplet(6, 0, 0), beta: true), + "tvOS": PlatformVersion(VersionTriplet(13, 0, 0), beta: true), + "iOS": PlatformVersion(VersionTriplet(13, 0, 0), beta: true), + ]) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) - let renderNode = translator.visitSymbol(symbol) as! RenderNode + let node = try context.entity(with: reference) + let renderNode = try XCTUnwrap(DocumentationNodeConverter(bundle: bundle, context: context).convert(node)) // Verify task group link is beta XCTAssertEqual((renderNode.references["doc://org.swift.docc.example/documentation/MyKit/MyClass"] as? TopicRenderReference)?.isBeta, true) @@ -2008,43 +2011,30 @@ Document // Set all platforms to beta where the symbol is available, // some platforms not beta but the symbol is not available there. - context.externalMetadata.currentPlatforms = [ + let (bundle, context, reference) = try makeTestBundle(currentPlatforms: [ "macOS": PlatformVersion(VersionTriplet(10, 15, 0), beta: true), "watchOS": PlatformVersion(VersionTriplet(6, 0, 0), beta: true), "tvOS": PlatformVersion(VersionTriplet(13, 0, 0), beta: true), "iOS": PlatformVersion(VersionTriplet(13, 0, 0), beta: true), "FictionalOS": PlatformVersion(VersionTriplet(42, 0, 0), beta: false), "ImaginaryOS": PlatformVersion(VersionTriplet(3, 3, 3), beta: false), - ] + ]) + + let node = try context.entity(with: reference) + let renderNode = try XCTUnwrap(DocumentationNodeConverter(bundle: bundle, context: context).convert(node)) + + // Verify task group link is beta + XCTAssertEqual((renderNode.references["doc://org.swift.docc.example/documentation/MyKit/MyClass"] as? TopicRenderReference)?.isBeta, true) - do { - let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/MyKit", sourceLanguage: .swift) - let node = try context.entity(with: reference) - let symbol = node.semantic as! Symbol - - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) - let renderNode = translator.visitSymbol(symbol) as! RenderNode - - // Verify task group link is beta - XCTAssertEqual((renderNode.references["doc://org.swift.docc.example/documentation/MyKit/MyClass"] as? TopicRenderReference)?.isBeta, true) - } - // Add ImaginaryOS platform - but make it unconditionally unavailable and // verify that it doesn't affect the beta status do { // Add an extra platform where the symbol is not available. - let renderReference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/MyKit", sourceLanguage: .swift) - let renderReferenceNode = try context.entity(with: renderReference) - let renderReferenceSymbol = try XCTUnwrap(renderReferenceNode.semantic as? Symbol) + let renderReferenceSymbol = try XCTUnwrap(node.semantic as? Symbol) renderReferenceSymbol.availability?.availability.append(SymbolGraph.Symbol.Availability.AvailabilityItem(domain: SymbolGraph.Symbol.Availability.Domain(rawValue: "ImaginaryOS"), introducedVersion: nil, deprecatedVersion: nil, obsoletedVersion: nil, message: nil, renamed: nil, isUnconditionallyDeprecated: false, isUnconditionallyUnavailable: true, willEventuallyBeDeprecated: false)) // Verify the rendered reference - let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/MyKit", sourceLanguage: .swift) - let node = try context.entity(with: reference) - let symbol = node.semantic as! Symbol - - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) - let renderNode = translator.visitSymbol(symbol) as! RenderNode + let renderNode = try XCTUnwrap(DocumentationNodeConverter(bundle: bundle, context: context).convert(node)) // Verify task group link is beta XCTAssertEqual((renderNode.references["doc://org.swift.docc.example/documentation/MyKit/MyClass"] as? TopicRenderReference)?.isBeta, true) @@ -2851,9 +2841,10 @@ Document let bundleURL = Bundle.module.url( forResource: "TestBundle", withExtension: "docc", subdirectory: "Test Bundles")! - let (_, bundle, context) = try loadBundle(from: bundleURL, configureContext: { context in - context.externalMetadata.inheritDocs = true - }) + var configuration = DocumentationContext.Configuration() + configuration.externalMetadata.inheritDocs = true + + let (_, bundle, context) = try loadBundle(from: bundleURL, configuration: configuration) // Verify that we don't reference resolve inherited docs. XCTAssertFalse(context.diagnosticEngine.problems.contains(where: { problem in diff --git a/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift b/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift index 6662c6a555..0e273145b5 100644 --- a/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift @@ -12,6 +12,7 @@ import Foundation import XCTest import SymbolKit @testable import SwiftDocC +import SwiftDocCTestUtilities class DefaultAvailabilityTests: XCTestCase { @@ -101,54 +102,38 @@ class DefaultAvailabilityTests: XCTestCase { // Test whether the default availability is merged with beta status from the command line func testBundleWithDefaultAvailabilityInBetaDocs() throws { - // Copy an Info.plist with default availability - let (_, bundle, context) = try testBundleAndContext(copying: "TestBundle", excludingPaths: []) { (url) in - try? FileManager.default.removeItem(at: url.appendingPathComponent("Info.plist")) - try? FileManager.default.copyItem(at: self.infoPlistAvailabilityURL, to: url.appendingPathComponent("Info.plist")) - } - - // Set a beta status for the docs (which would normally be set via command line argument) - context.externalMetadata.currentPlatforms = [ + // Beta status for the docs (which would normally be set via command line argument) + try assertRenderedPlatformsFor(currentPlatforms: [ "macOS": PlatformVersion(VersionTriplet(10, 15, 1), beta: true), "Mac Catalyst": PlatformVersion(VersionTriplet(13, 5, 0), beta: true), - ] - - // Test if the module availability is also "beta" for the "macOS" platform, - // verify that the Mac Catalyst platform's name (including a space) is rendered correctly - do { - let identifier = ResolvedTopicReference(bundleIdentifier: "org.swift.docc.example", path: "/documentation/MyKit", fragment: nil, sourceLanguage: .swift) - let node = try context.entity(with: identifier) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) - let renderNode = translator.visit(node.semantic) as! RenderNode - - XCTAssertEqual(renderNode.metadata.platforms?.map({ "\($0.name ?? "") \($0.introduced ?? "")\($0.isBeta == true ? "(beta)" : "")" }).sorted(), [ - "Mac Catalyst 13.5(beta)", - "macOS 10.15.1(beta)", - ]) - } - + ], equal: [ + "Mac Catalyst 13.5(beta)", + "macOS 10.15.1(beta)", + ]) + // Repeat the assertions, but use an earlier platform version this time - context.externalMetadata.currentPlatforms = [ + try assertRenderedPlatformsFor(currentPlatforms: [ "macOS": PlatformVersion(VersionTriplet(10, 14, 1), beta: true), "Mac Catalyst": PlatformVersion(VersionTriplet(13, 5, 0), beta: true), - ] - - // Test if the module availability is also "beta" for the "macOS" platform, - // verify that the Mac Catalyst platform's name (including a space) is rendered correctly - do { - let identifier = ResolvedTopicReference(bundleIdentifier: "org.swift.docc.example", path: "/documentation/MyKit", fragment: nil, sourceLanguage: .swift) - let node = try context.entity(with: identifier) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) - let renderNode = translator.visit(node.semantic) as! RenderNode - - XCTAssertEqual(renderNode.metadata.platforms?.map({ "\($0.name ?? "") \($0.introduced ?? "")\($0.isBeta == true ? "(beta)" : "")" }).sorted(), [ - "Mac Catalyst 13.5(beta)", - "macOS 10.15.1(beta)", - ]) - } + ], equal: [ + "Mac Catalyst 13.5(beta)", + "macOS 10.15.1(beta)", + ]) } - private func assertRenderedPlatforms(for reference: ResolvedTopicReference, equal expected: [String], bundle: DocumentationBundle, context: DocumentationContext, file: StaticString = #file, line: UInt = #line) throws { + private func assertRenderedPlatformsFor(currentPlatforms: [String : PlatformVersion], equal expected: [String], file: StaticString = #file, line: UInt = #line) throws { + var configuration = DocumentationContext.Configuration() + configuration.externalMetadata.currentPlatforms = currentPlatforms + + let catalog = Folder(name: "unit-test.docc", content: [ + CopyOfFile(original: self.infoPlistAvailabilityURL, newName: "Info.plist"), + // This module name needs to match what's specified in the Info.plist + JSONFile(name: "MyKit.symbols.json", content: makeSymbolGraph(moduleName: "MyKit")), + ]) + + let (_, bundle, context) = try loadBundle(from: createTempFolder(content: [catalog]), configuration: configuration) + let reference = try XCTUnwrap(context.soleRootModuleReference, file: file, line: line) + // Test whether we: // 1) Fallback on iOS when Mac Catalyst availability is missing // 2) Render [Beta] or not for Mac Catalyst's inherited iOS availability @@ -162,59 +147,42 @@ class DefaultAvailabilityTests: XCTestCase { // Test whether when Mac Catalyst availability is missing we fall back on // Mac Catalyst info.plist availability and not on iOS availability. func testBundleWithMissingCatalystAvailability() throws { - // Copy an Info.plist with default availability - let (_, bundle, context) = try testBundleAndContext(copying: "TestBundle", excludingPaths: []) { (url) in - do { - try FileManager.default.removeItem(at: url.appendingPathComponent("Info.plist")) - try String(contentsOf: self.infoPlistAvailabilityURL).write(to: url.appendingPathComponent("Info.plist"), atomically: true, encoding: .utf8) - } catch { - XCTFail("Could not copy Info.plist with custom availability in the test bundle") - } - } - - let identifier = ResolvedTopicReference(bundleIdentifier: "org.swift.docc.example", path: "/documentation/MyKit", fragment: nil, sourceLanguage: .swift) - - // Set a beta status for both iOS and Mac Catalyst - context.externalMetadata.currentPlatforms = [ + // Beta status for both iOS and Mac Catalyst + try assertRenderedPlatformsFor(currentPlatforms: [ "iOS": PlatformVersion(VersionTriplet(13, 5, 0), beta: true), "Mac Catalyst": PlatformVersion(VersionTriplet(13, 5, 0), beta: true), - ] - - try assertRenderedPlatforms(for: identifier, equal: [ + ], equal: [ "Mac Catalyst 13.5(beta)", "macOS 10.15.1", - ], bundle: bundle, context: context) + ]) - // Set a public status for Mac Catalyst - context.externalMetadata.currentPlatforms = [ + // Public status for Mac Catalyst + try assertRenderedPlatformsFor(currentPlatforms: [ "Mac Catalyst": PlatformVersion(VersionTriplet(13, 5, 0), beta: false), - ] - - try assertRenderedPlatforms(for: identifier, equal: [ + ], equal: [ "Mac Catalyst 13.5", "macOS 10.15.1", - ], bundle: bundle, context: context) + ]) - // Verify that a bug rendering availability as beta when - // no platforms are provided is fixed. - context.externalMetadata.currentPlatforms = [:] - try assertRenderedPlatforms(for: identifier, equal: [ + // Verify that a bug rendering availability as beta when no platforms are provided is fixed. + try assertRenderedPlatformsFor(currentPlatforms: [:], equal: [ "Mac Catalyst 13.5", "macOS 10.15.1", - ], bundle: bundle, context: context) + ]) } // Test whether the default availability is not beta when not matching current target platform func testBundleWithDefaultAvailabilityNotInBetaDocs() throws { - // Copy an Info.plist with default availability of macOS 10.15.1 - let (_, bundle, context) = try testBundleAndContext(copying: "TestBundle", excludingPaths: []) { (url) in + var configuration = DocumentationContext.Configuration() + // Set a beta status for the docs (which would normally be set via command line argument) + configuration.externalMetadata.currentPlatforms = ["macOS": PlatformVersion(VersionTriplet(10, 16, 0), beta: true)] + + let (_, bundle, context) = try testBundleAndContext(copying: "TestBundle", configuration: configuration) { (url) in + // Copy an Info.plist with default availability of macOS 10.15.1 try? FileManager.default.removeItem(at: url.appendingPathComponent("Info.plist")) try? FileManager.default.copyItem(at: self.infoPlistAvailabilityURL, to: url.appendingPathComponent("Info.plist")) } - // Set a beta status for the docs (which would normally be set via command line argument) - context.externalMetadata.currentPlatforms = ["macOS": PlatformVersion(VersionTriplet(10, 16, 0), beta: true)] - // Test if the module availability is not "beta" for the "macOS" platform (since 10.15.1 != 10.16) do { let identifier = ResolvedTopicReference(bundleIdentifier: "org.swift.docc.example", path: "/documentation/MyKit", fragment: nil, sourceLanguage: .swift) @@ -231,10 +199,10 @@ class DefaultAvailabilityTests: XCTestCase { // Test that a symbol is unavailable and default availability does not precede the "unavailable" attribute. func testUnavailableAvailability() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "TestBundle", excludingPaths: []) { _ in } - + var configuration = DocumentationContext.Configuration() // Set a beta status for the docs (which would normally be set via command line argument) - context.externalMetadata.currentPlatforms = ["iOS": PlatformVersion(VersionTriplet(14, 0, 0), beta: true)] + configuration.externalMetadata.currentPlatforms = ["iOS": PlatformVersion(VersionTriplet(14, 0, 0), beta: true)] + let (_, bundle, context) = try testBundleAndContext(copying: "TestBundle", configuration: configuration) do { let identifier = ResolvedTopicReference(bundleIdentifier: "org.swift.docc.example", path: "/documentation/MyKit/MyClass/myFunction()", fragment: nil, sourceLanguage: .swift) @@ -577,6 +545,5 @@ class DefaultAvailabilityTests: XCTestCase { module.filter({ $0.platformName.displayName == "iPadOS" }).first?.versionInformation, .available(version: "10.0") ) - } } diff --git a/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift b/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift index e146122001..c5d52129d3 100644 --- a/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift @@ -239,9 +239,9 @@ class PlatformAvailabilityTests: XCTestCase { func testBundleWithConfiguredPlatforms(named testBundleName: String, platformMetadata: [String : PlatformVersion]) throws -> (DocumentationBundle, DocumentationContext) { let bundleURL = try XCTUnwrap(Bundle.module.url(forResource: testBundleName, withExtension: "docc", subdirectory: "Test Bundles")) - let (_, bundle, context) = try loadBundle(from: bundleURL) { context in - context.externalMetadata.currentPlatforms = platformMetadata - } + var configuration = DocumentationContext.Configuration() + configuration.externalMetadata.currentPlatforms = platformMetadata + let (_, bundle, context) = try loadBundle(from: bundleURL, configuration: configuration) return (bundle, context) } diff --git a/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift b/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift index 7ab377e9e6..9936a1d4ad 100644 --- a/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift +++ b/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift @@ -17,19 +17,24 @@ import SwiftDocCTestUtilities extension XCTestCase { /// Loads a documentation bundle from the given source URL and creates a documentation context. - func loadBundle(from bundleURL: URL, - externalResolvers: [String: ExternalDocumentationSource] = [:], - externalSymbolResolver: GlobalExternalSymbolResolver? = nil, - fallbackResolver: ConvertServiceFallbackResolver? = nil, - diagnosticFilterLevel: DiagnosticSeverity = .hint, - configureContext: ((DocumentationContext) throws -> Void)? = nil + func loadBundle( + from bundleURL: URL, + externalResolvers: [String: ExternalDocumentationSource] = [:], + externalSymbolResolver: GlobalExternalSymbolResolver? = nil, + fallbackResolver: ConvertServiceFallbackResolver? = nil, + diagnosticFilterLevel: DiagnosticSeverity = .hint, + configuration: DocumentationContext.Configuration = .init(), + configureContext: ((DocumentationContext) throws -> Void)? = nil ) throws -> (URL, DocumentationBundle, DocumentationContext) { let workspace = DocumentationWorkspace() - let context = try DocumentationContext(dataProvider: workspace, diagnosticEngine: DiagnosticEngine(filterLevel: diagnosticFilterLevel)) - context.externalDocumentationSources = externalResolvers - context.globalExternalSymbolResolver = externalSymbolResolver - context.convertServiceFallbackResolver = fallbackResolver - context.externalMetadata.diagnosticLevel = diagnosticFilterLevel + + var configuration = configuration + configuration.externalDocumentationConfiguration.sources = externalResolvers + configuration.externalDocumentationConfiguration.globalSymbolResolver = externalSymbolResolver + configuration.convertServiceConfiguration.fallbackResolver = fallbackResolver + configuration.externalMetadata.diagnosticLevel = diagnosticFilterLevel + + let context = try DocumentationContext(dataProvider: workspace, diagnosticEngine: DiagnosticEngine(filterLevel: diagnosticFilterLevel), configuration: configuration) try configureContext?(context) // Load the bundle using automatic discovery let automaticDataProvider = try LocalFileSystemDataProvider(rootURL: bundleURL) @@ -45,16 +50,15 @@ extension XCTestCase { /// - Parameters: /// - catalog: The directory structure of the documentation catalog /// - otherFileSystemDirectories: Any other directories in the test file system. - /// - configureContext: A closure where the caller can configure the context before registering the data provider with the context. + /// - configuration: Configuration for the created context. /// - Returns: The loaded documentation bundle and context for the given catalog input. func loadBundle( catalog: Folder, otherFileSystemDirectories: [Folder] = [], - configureContext: (DocumentationContext) throws -> Void = { _ in } + configuration: DocumentationContext.Configuration = .init() ) throws -> (DocumentationBundle, DocumentationContext) { let workspace = DocumentationWorkspace() - let context = try DocumentationContext(dataProvider: workspace) - try configureContext(context) + let context = try DocumentationContext(dataProvider: workspace, configuration: configuration) let fileSystem = try TestFileSystem(folders: [catalog] + otherFileSystemDirectories) context.linkResolver.fileManager = fileSystem @@ -64,12 +68,14 @@ extension XCTestCase { return (bundle, context) } - func testBundleAndContext(copying name: String, - excludingPaths excludedPaths: [String] = [], - externalResolvers: [BundleIdentifier : ExternalDocumentationSource] = [:], - externalSymbolResolver: GlobalExternalSymbolResolver? = nil, - fallbackResolver: ConvertServiceFallbackResolver? = nil, - configureBundle: ((URL) throws -> Void)? = nil + func testBundleAndContext( + copying name: String, + excludingPaths excludedPaths: [String] = [], + externalResolvers: [BundleIdentifier : ExternalDocumentationSource] = [:], + externalSymbolResolver: GlobalExternalSymbolResolver? = nil, + fallbackResolver: ConvertServiceFallbackResolver? = nil, + configuration: DocumentationContext.Configuration = .init(), + configureBundle: ((URL) throws -> Void)? = nil ) throws -> (URL, DocumentationBundle, DocumentationContext) { let sourceURL = try XCTUnwrap(Bundle.module.url( forResource: name, withExtension: "docc", subdirectory: "Test Bundles")) @@ -94,14 +100,19 @@ extension XCTestCase { from: bundleURL, externalResolvers: externalResolvers, externalSymbolResolver: externalSymbolResolver, - fallbackResolver: fallbackResolver + fallbackResolver: fallbackResolver, + configuration: configuration ) } - func testBundleAndContext(named name: String, externalResolvers: [String: ExternalDocumentationSource] = [:]) throws -> (URL, DocumentationBundle, DocumentationContext) { + func testBundleAndContext( + named name: String, + externalResolvers: [String: ExternalDocumentationSource] = [:], + fallbackResolver: ConvertServiceFallbackResolver? = nil + ) throws -> (URL, DocumentationBundle, DocumentationContext) { let bundleURL = try XCTUnwrap(Bundle.module.url( forResource: name, withExtension: "docc", subdirectory: "Test Bundles")) - return try loadBundle(from: bundleURL, externalResolvers: externalResolvers) + return try loadBundle(from: bundleURL, externalResolvers: externalResolvers, fallbackResolver: fallbackResolver) } func testBundleAndContext(named name: String, externalResolvers: [String: ExternalDocumentationSource] = [:]) throws -> (DocumentationBundle, DocumentationContext) { diff --git a/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift b/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift index 9c4813c296..ae137ea0d6 100644 --- a/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift @@ -1699,7 +1699,7 @@ class ConvertActionTests: XCTestCase { temporaryDirectory: testDataProvider.uniqueTemporaryDirectory() ) - XCTAssertEqual(action.context.externalMetadata.currentPlatforms, [ + XCTAssertEqual(action.context.configuration.externalMetadata.currentPlatforms, [ "platform1" : PlatformVersion(.init(10, 11, 12), beta: false), "platform2" : PlatformVersion(.init(11, 12, 13), beta: false), ]) @@ -1734,7 +1734,7 @@ class ConvertActionTests: XCTestCase { // Test whether the missing platforms copy the availability information from the fallback platform. var action = try generateConvertAction(currentPlatforms: ["iOS": PlatformVersion(.init(10, 0, 0), beta: true)]) - XCTAssertEqual(action.context.externalMetadata.currentPlatforms, [ + XCTAssertEqual(action.context.configuration.externalMetadata.currentPlatforms, [ "iOS" : PlatformVersion(.init(10, 0, 0), beta: true), "Mac Catalyst" : PlatformVersion(.init(10, 0, 0), beta: true), "iPadOS" : PlatformVersion(.init(10, 0, 0), beta: true), @@ -1744,7 +1744,7 @@ class ConvertActionTests: XCTestCase { "iOS": PlatformVersion(.init(10, 0, 0), beta: true), "Mac Catalyst": PlatformVersion(.init(11, 0, 0), beta: false) ]) - XCTAssertEqual(action.context.externalMetadata.currentPlatforms, [ + XCTAssertEqual(action.context.configuration.externalMetadata.currentPlatforms, [ "iOS" : PlatformVersion(.init(10, 0, 0), beta: true), "Mac Catalyst" : PlatformVersion(.init(11, 0, 0), beta: false), "iPadOS" : PlatformVersion(.init(10, 0, 0), beta: true) @@ -1755,7 +1755,7 @@ class ConvertActionTests: XCTestCase { "iPadOS": PlatformVersion(.init(12, 0, 0), beta: false), ]) - XCTAssertEqual(action.context.externalMetadata.currentPlatforms, [ + XCTAssertEqual(action.context.configuration.externalMetadata.currentPlatforms, [ "iOS" : PlatformVersion(.init(10, 0, 0), beta: true), "Mac Catalyst" : PlatformVersion(.init(11, 0, 0), beta: true), "iPadOS" : PlatformVersion(.init(12, 0, 0), beta: false), @@ -1765,7 +1765,7 @@ class ConvertActionTests: XCTestCase { "tvOS": PlatformVersion(.init(13, 0, 0), beta: true) ]) - XCTAssertEqual(action.context.externalMetadata.currentPlatforms, [ + XCTAssertEqual(action.context.configuration.externalMetadata.currentPlatforms, [ "tvOS": PlatformVersion(.init(13, 0, 0), beta: true) ]) } @@ -1864,6 +1864,13 @@ class ConvertActionTests: XCTestCase { func convertTestBundle(batchSize: Int, emitDigest: Bool, targetURL: URL, testDataProvider: DocumentationWorkspaceDataProvider & FileManagerProtocol) throws -> ActionResult { // Run the create ConvertAction + + var configuration = DocumentationContext.Configuration() + configuration.externalDocumentationConfiguration.sources["com.example.test"] = TestReferenceResolver() + + let workspace = DocumentationWorkspace() + let context = try DocumentationContext(dataProvider: workspace, configuration: configuration) + var action = try ConvertAction( documentationBundleURL: bundle.absoluteURL, outOfProcessResolver: nil, @@ -1872,6 +1879,8 @@ class ConvertActionTests: XCTestCase { htmlTemplateDirectory: Folder.emptyHTMLTemplateDirectory.absoluteURL, emitDigest: emitDigest, currentPlatforms: nil, + workspace: workspace, + context: context, dataProvider: testDataProvider, fileManager: testDataProvider, temporaryDirectory: testDataProvider.uniqueTemporaryDirectory() @@ -1879,8 +1888,6 @@ class ConvertActionTests: XCTestCase { action.converter.batchNodeCount = batchSize - action.context.externalDocumentationSources["com.example.test"] = TestReferenceResolver() - return try action.perform(logHandle: .none) } @@ -2437,7 +2444,7 @@ class ConvertActionTests: XCTestCase { fileManager: testDataProvider, temporaryDirectory: testDataProvider.uniqueTemporaryDirectory(), inheritDocs: flag) - XCTAssertEqual(action.context.externalMetadata.inheritDocs, flag) + XCTAssertEqual(action.context.configuration.externalMetadata.inheritDocs, flag) } // Verify implicit value @@ -2452,7 +2459,7 @@ class ConvertActionTests: XCTestCase { dataProvider: testDataProvider, fileManager: testDataProvider, temporaryDirectory: testDataProvider.uniqueTemporaryDirectory()) - XCTAssertEqual(action.context.externalMetadata.inheritDocs, false) + XCTAssertEqual(action.context.configuration.externalMetadata.inheritDocs, false) } func testEmitsDigest() throws {