Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Sources/SwiftDocC/Infrastructure/DocumentationContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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] {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
114 changes: 10 additions & 104 deletions Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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")

Expand All @@ -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.

Expand Down Expand Up @@ -668,7 +668,7 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase {
}

func testAutomaticSeeAlsoOnlyShowsAPIsAvailableInParentsLanguageForSymbol() throws {
let outputConsumer = try mixedLanguageFrameworkConsumer()
let outputConsumer = try renderNodeConsumer(for: "MixedLanguageFramework")

// Swift-only symbol.
XCTAssertEqual(
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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<String>?) -> [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
}
}
111 changes: 111 additions & 0 deletions Tests/SwiftDocCTests/TestRenderNodeOutputConsumer.swift
Original file line number Diff line number Diff line change
@@ -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<String>?) -> [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
}
}