diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift index 6dc11c7a5..32dca1151 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift @@ -1877,7 +1877,10 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { let reference = ResolvedTopicReference( bundleIdentifier: bundle.identifier, path: path, - sourceLanguages: availableSourceLanguages ?? [.swift] + sourceLanguages: availableSourceLanguages + // FIXME: Pages in article-only catalogs should not be inferred as "Swift" as a fallback + // (github.com/apple/swift-docc/issues/240). + ?? [.swift] ) let title = article.topicGraphNode.title diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift index d0a6a6827..f5fd78b60 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift @@ -601,7 +601,14 @@ public struct RenderNodeTranslator: SemanticVisitor { collectedTopicReferences.append(contentsOf: hierarchyTranslator.collectedTopicReferences) node.hierarchy = hierarchy - node.variants = variants(for: documentationNode) + // Emit variants only if we're not compiling an article-only catalog to prevent renderers from + // advertising the page as "Swift", which is the language DocC assigns to pages in article only pages. + // (github.com/apple/swift-docc/issues/240). + if let topLevelModule = context.soleRootModuleReference, + try! context.entity(with: topLevelModule).kind.isSymbol + { + node.variants = variants(for: documentationNode) + } if let abstract = article.abstractSection, let abstractContent = visitMarkup(abstract.content) as? [RenderInlineContent] { diff --git a/Tests/SwiftDocCTests/Model/SemaToRenderNodeArticleOnlyCatalogTests.swift b/Tests/SwiftDocCTests/Model/SemaToRenderNodeArticleOnlyCatalogTests.swift new file mode 100644 index 000000000..0dcdb8052 --- /dev/null +++ b/Tests/SwiftDocCTests/Model/SemaToRenderNodeArticleOnlyCatalogTests.swift @@ -0,0 +1,20 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 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 XCTest +@testable import SwiftDocC + +class SemaToRenderNodeArticleOnlyCatalogTests: XCTestCase { + func testDoesNotEmitVariantsForPagesInArticleOnlyCatalog() throws { + for renderNode in try renderNodeConsumer(for: "BundleWithTechnologyRoot").allRenderNodes() { + XCTAssertNil(renderNode.variants) + } + } +} diff --git a/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift b/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift index 26c279591..23b1d1c15 100644 --- a/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift +++ b/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift @@ -78,7 +78,7 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { } func assertOutputsMultiLanguageRenderNodes(variantInterfaceLanguage: String) throws { - let outputConsumer = try mixedLanguageFrameworkConsumer { bundleURL in + let outputConsumer = try renderNodeConsumer(for: "MixedLanguageFramework") { bundleURL in // Update the clang symbol graph with the Objective-C identifier given in variantInterfaceLanguage. let clangSymbolGraphLocation = bundleURL @@ -207,7 +207,7 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { } func testFrameworkRenderNodeHasExpectedContentAcrossLanguages() throws { - let outputConsumer = try mixedLanguageFrameworkConsumer() + let outputConsumer = try renderNodeConsumer(for: "MixedLanguageFramework") let mixedLanguageFrameworkRenderNode = try outputConsumer.renderNode( withIdentifier: "MixedLanguageFramework" ) @@ -322,7 +322,7 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { } func testObjectiveCAuthoredRenderNodeHasExpectedContentAcrossLanguages() throws { - let outputConsumer = try mixedLanguageFrameworkConsumer() + let outputConsumer = try renderNodeConsumer(for: "MixedLanguageFramework") let fooRenderNode = try outputConsumer.renderNode(withIdentifier: "c:@E@Foo") assertExpectedContent( @@ -476,7 +476,7 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { } func testArticleInMixedLanguageFramework() throws { - let outputConsumer = try mixedLanguageFrameworkConsumer() { url in + let outputConsumer = try renderNodeConsumer(for: "MixedLanguageFramework") { url in try """ # MyArticle @@ -539,7 +539,7 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { } func testAPICollectionInMixedLanguageFramework() throws { - let outputConsumer = try mixedLanguageFrameworkConsumer() + let outputConsumer = try renderNodeConsumer(for: "MixedLanguageFramework") let articleRenderNode = try outputConsumer.renderNode(withTitle: "APICollection") @@ -604,7 +604,7 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { } func testGeneratedImplementationsCollectionIsCuratedInAllAvailableLanguages() throws { - let outputConsumer = try mixedLanguageFrameworkConsumer() + let outputConsumer = try renderNodeConsumer(for: "MixedLanguageFramework") let protocolRenderNode = try outputConsumer.renderNode(withTitle: "MixedLanguageClassConformingToProtocol") @@ -628,7 +628,7 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { } func testGeneratedImplementationsCollectionDoesNotCurateInAllUnavailableLanguages() throws { - let outputConsumer = try mixedLanguageFrameworkConsumer { bundleURL in + let outputConsumer = try renderNodeConsumer(for: "MixedLanguageFramework") { bundleURL in // Update the clang symbol graph to remove the protocol method requirement, so that it's effectively // available in Swift only. @@ -668,7 +668,7 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { } func testAutomaticSeeAlsoOnlyShowsAPIsAvailableInParentsLanguageForSymbol() throws { - let outputConsumer = try mixedLanguageFrameworkConsumer() + let outputConsumer = try renderNodeConsumer(for: "MixedLanguageFramework") // Swift-only symbol. XCTAssertEqual( @@ -738,8 +738,8 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { } func testMultiLanguageChildOfSingleParentSymbolIsCuratedInMultiLanguage() throws { - let outputConsumer = try mixedLanguageFrameworkConsumer( - bundleName: "MixedLanguageFrameworkSingleLanguageParent" + let outputConsumer = try renderNodeConsumer( + for: "MixedLanguageFrameworkSingleLanguageParent" ) let topLevelFrameworkPage = try outputConsumer.renderNode(withTitle: "MixedLanguageFramework") @@ -891,97 +891,3 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { ) } } - -private class TestRenderNodeOutputConsumer: ConvertOutputConsumer { - var renderNodes = Synchronized<[RenderNode]>([]) - - func consume(renderNode: RenderNode) throws { - renderNodes.sync { renderNodes in - renderNodes.append(renderNode) - } - } - - func consume(problems: [Problem]) throws { } - func consume(assetsInBundle bundle: DocumentationBundle) throws { } - func consume(linkableElementSummaries: [LinkDestinationSummary]) throws { } - func consume(indexingRecords: [IndexingRecord]) throws { } - func consume(assets: [RenderReferenceType: [RenderReference]]) throws { } - func consume(benchmarks: Benchmark) throws { } - func consume(documentationCoverageInfo: [CoverageDataEntry]) throws { } - func consume(renderReferenceStore: RenderReferenceStore) throws { } - func consume(buildMetadata: BuildMetadata) throws { } -} - -extension TestRenderNodeOutputConsumer { - func renderNodes(withInterfaceLanguages interfaceLanguages: Set?) -> [RenderNode] { - renderNodes.sync { renderNodes in - renderNodes.filter { renderNode in - guard let interfaceLanguages = interfaceLanguages else { - // If there are no interface languages set, return the nodes with no variants. - return renderNode.variants == nil - } - - guard let variants = renderNode.variants else { - return false - } - - let actualInterfaceLanguages: [String] = variants.flatMap { variant in - variant.traits.compactMap { trait in - guard case .interfaceLanguage(let interfaceLanguage) = trait else { - return nil - } - return interfaceLanguage - } - } - - return Set(actualInterfaceLanguages) == interfaceLanguages - } - } - } - - func renderNode(withIdentifier identifier: String) throws -> RenderNode { - try renderNode(where: { renderNode in renderNode.metadata.externalID == identifier }) - } - - func renderNode(withTitle title: String) throws -> RenderNode { - try renderNode(where: { renderNode in renderNode.metadata.title == title }) - } - - private func renderNode(where predicate: (RenderNode) -> Bool) throws -> RenderNode { - let renderNode = renderNodes.sync { renderNodes in - renderNodes.first { renderNode in - predicate(renderNode) - } - } - - return try XCTUnwrap(renderNode) - } -} - -fileprivate extension SemaToRenderNodeMixedLanguageTests { - func mixedLanguageFrameworkConsumer( - bundleName: String = "MixedLanguageFramework", - configureBundle: ((URL) throws -> Void)? = nil - ) throws -> TestRenderNodeOutputConsumer { - let (bundleURL, _, context) = try testBundleAndContext( - copying: bundleName, - configureBundle: configureBundle - ) - - var converter = DocumentationConverter( - documentationBundleURL: bundleURL, - emitDigest: false, - documentationCoverageOptions: .noCoverage, - currentPlatforms: nil, - workspace: context.dataProvider as! DocumentationWorkspace, - context: context, - dataProvider: try LocalFileSystemDataProvider(rootURL: bundleURL), - bundleDiscoveryOptions: BundleDiscoveryOptions() - ) - - let outputConsumer = TestRenderNodeOutputConsumer() - let (_, _) = try converter.convert(outputConsumer: outputConsumer) - - return outputConsumer - } -} diff --git a/Tests/SwiftDocCTests/TestRenderNodeOutputConsumer.swift b/Tests/SwiftDocCTests/TestRenderNodeOutputConsumer.swift new file mode 100644 index 000000000..6526f17dd --- /dev/null +++ b/Tests/SwiftDocCTests/TestRenderNodeOutputConsumer.swift @@ -0,0 +1,111 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 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 +@testable import SwiftDocC +import XCTest + +class TestRenderNodeOutputConsumer: ConvertOutputConsumer { + var renderNodes = Synchronized<[RenderNode]>([]) + + func consume(renderNode: RenderNode) throws { + renderNodes.sync { renderNodes in + renderNodes.append(renderNode) + } + } + + func consume(problems: [Problem]) throws { } + func consume(assetsInBundle bundle: DocumentationBundle) throws { } + func consume(linkableElementSummaries: [LinkDestinationSummary]) throws { } + func consume(indexingRecords: [IndexingRecord]) throws { } + func consume(assets: [RenderReferenceType: [RenderReference]]) throws { } + func consume(benchmarks: Benchmark) throws { } + func consume(documentationCoverageInfo: [CoverageDataEntry]) throws { } + func consume(renderReferenceStore: RenderReferenceStore) throws { } + func consume(buildMetadata: BuildMetadata) throws { } +} + +extension TestRenderNodeOutputConsumer { + func allRenderNodes() -> [RenderNode] { + renderNodes.sync { $0 } + } + + func renderNodes(withInterfaceLanguages interfaceLanguages: Set?) -> [RenderNode] { + renderNodes.sync { renderNodes in + renderNodes.filter { renderNode in + guard let interfaceLanguages = interfaceLanguages else { + // If there are no interface languages set, return the nodes with no variants. + return renderNode.variants == nil + } + + guard let variants = renderNode.variants else { + return false + } + + let actualInterfaceLanguages: [String] = variants.flatMap { variant in + variant.traits.compactMap { trait in + guard case .interfaceLanguage(let interfaceLanguage) = trait else { + return nil + } + return interfaceLanguage + } + } + + return Set(actualInterfaceLanguages) == interfaceLanguages + } + } + } + + func renderNode(withIdentifier identifier: String) throws -> RenderNode { + try renderNode(where: { renderNode in renderNode.metadata.externalID == identifier }) + } + + func renderNode(withTitle title: String) throws -> RenderNode { + try renderNode(where: { renderNode in renderNode.metadata.title == title }) + } + + func renderNode(where predicate: (RenderNode) -> Bool) throws -> RenderNode { + let renderNode = renderNodes.sync { renderNodes in + renderNodes.first { renderNode in + predicate(renderNode) + } + } + + return try XCTUnwrap(renderNode) + } +} + +extension XCTestCase { + func renderNodeConsumer( + for bundleName: String, + configureBundle: ((URL) throws -> Void)? = nil + ) throws -> TestRenderNodeOutputConsumer { + let (bundleURL, _, context) = try testBundleAndContext( + copying: bundleName, + configureBundle: configureBundle + ) + + var converter = DocumentationConverter( + documentationBundleURL: bundleURL, + emitDigest: false, + documentationCoverageOptions: .noCoverage, + currentPlatforms: nil, + workspace: context.dataProvider as! DocumentationWorkspace, + context: context, + dataProvider: try LocalFileSystemDataProvider(rootURL: bundleURL), + bundleDiscoveryOptions: BundleDiscoveryOptions() + ) + + let outputConsumer = TestRenderNodeOutputConsumer() + let (_, _) = try converter.convert(outputConsumer: outputConsumer) + + return outputConsumer + } +}