Skip to content

Commit 7cdca7f

Browse files
sofiaromoralesfranklinschd-ronnqvist
authored
Include manually curated non-symbol nodes in the Topics section regardless of the source language of the symbol (#757)
If the node to which the reference belongs is a non symbol node, render the topic in the topic section independently of the source language of the curation. By default, Articles and other non-symbol nodes have the source language set to 'Swift.' Prior to this fix, when curating an Article within a symbol with ObjC availability, the curated article was being omitted from the Topics section. With this fix, we check whether it's an article and maintain the curation, regardless of whether we're linking to a node with a different source language. rdar://118461894 * Added multiple tests to validate that the correct filter behaviour happens when linking to external articles with the link resolver. * Refactored tests to validate filtering functionality for all available language variants. --------- Co-authored-by: Franklin Schrans <[email protected]> Co-authored-by: David Rönnqvist <[email protected]>
1 parent 02718ca commit 7cdca7f

File tree

4 files changed

+211
-8
lines changed

4 files changed

+211
-8
lines changed

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2547,25 +2547,46 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
25472547
throw ContextError.notFound(reference.url)
25482548
}
25492549

2550-
/// Returns the set of languages the entity corresponding to the given reference is available in.
2551-
///
2552-
/// - Precondition: The entity associated with the given reference must be registered in the context.
2553-
public func sourceLanguages(for reference: ResolvedTopicReference) -> Set<SourceLanguage> {
2550+
private func knownEntityValue<Result>(
2551+
reference: ResolvedTopicReference,
2552+
valueInLocalEntity: (DocumentationNode) -> Result,
2553+
valueInExternalEntity: (LinkResolver.ExternalEntity) -> Result
2554+
) -> Result {
25542555
do {
25552556
// Look up the entity without its fragment. The documentation context does not keep track of page sections
25562557
// as nodes, and page sections are considered to be available in the same languages as the page they're
25572558
// defined in.
25582559
let referenceWithoutFragment = reference.withFragment(nil)
2559-
return try entity(with: referenceWithoutFragment).availableSourceLanguages
2560+
return try valueInLocalEntity(entity(with: referenceWithoutFragment))
25602561
} catch ContextError.notFound {
25612562
if let externalEntity = externalCache[reference] {
2562-
return externalEntity.sourceLanguages
2563+
return valueInExternalEntity(externalEntity)
25632564
}
25642565
preconditionFailure("Reference does not have an associated documentation node.")
25652566
} catch {
2566-
fatalError("Unexpected error when retrieving source languages: \(error)")
2567+
fatalError("Unexpected error when retrieving entity: \(error)")
25672568
}
25682569
}
2570+
2571+
/// Returns the set of languages the entity corresponding to the given reference is available in.
2572+
///
2573+
/// - Precondition: The entity associated with the given reference must be registered in the context.
2574+
public func sourceLanguages(for reference: ResolvedTopicReference) -> Set<SourceLanguage> {
2575+
knownEntityValue(
2576+
reference: reference,
2577+
valueInLocalEntity: \.availableSourceLanguages,
2578+
valueInExternalEntity: \.sourceLanguages
2579+
)
2580+
}
2581+
2582+
/// Returns whether the given reference corresponds to a symbol.
2583+
func isSymbol(reference: ResolvedTopicReference) -> Bool {
2584+
knownEntityValue(
2585+
reference: reference,
2586+
valueInLocalEntity: { node in node.kind.isSymbol },
2587+
valueInExternalEntity: { entity in entity.topicRenderReference.kind == .symbol }
2588+
)
2589+
}
25692590

25702591
// MARK: - Relationship queries
25712592

Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,13 @@ public struct RenderNodeTranslator: SemanticVisitor {
10481048
return true
10491049
}
10501050

1051+
guard context.isSymbol(reference: reference) else {
1052+
// If the reference corresponds to any kind except Symbol
1053+
// (e.g., Article, Tutorial, SampleCode...), allow the topic
1054+
// to appear independently of the source language it belongs to.
1055+
return true
1056+
}
1057+
10511058
let referenceSourceLanguageIDs = Set(context.sourceLanguages(for: reference).map(\.id))
10521059

10531060
let availableSourceLanguageTraits = Set(availableTraits.compactMap(\.interfaceLanguage))
@@ -1491,7 +1498,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
14911498

14921499
var sections = [TaskGroupRenderSection]()
14931500
if let topics = topics, !topics.taskGroups.isEmpty {
1494-
// Allowed traits should be all traits except the reverse of the objc/swift pairing
1501+
// Allowed symbol traits should be all traits except the reverse of the objc/swift pairing
14951502
sections.append(
14961503
contentsOf: renderGroups(
14971504
topics,

Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,4 +912,97 @@ Document @1:1-1:35
912912
let finalURL = urlGenerator.presentationURLForReference(linkReference)
913913
XCTAssertEqual(finalURL.absoluteString, "https://example.com/example/externally/resolved/path#67890")
914914
}
915+
916+
func testExternalArticlesAreIncludedInAllVariantsTopicsSection() throws {
917+
let externalResolver = TestMultiResultExternalReferenceResolver()
918+
externalResolver.bundleIdentifier = "com.test.external"
919+
920+
externalResolver.entitiesToReturn["/path/to/external/swiftArticle"] = .success(
921+
.init(
922+
referencePath: "/path/to/external/swiftArticle",
923+
title: "SwiftArticle",
924+
kind: .article,
925+
language: .swift
926+
)
927+
)
928+
929+
externalResolver.entitiesToReturn["/path/to/external/objCArticle"] = .success(
930+
.init(
931+
referencePath: "/path/to/external/objCArticle",
932+
title: "ObjCArticle",
933+
kind: .article,
934+
language: .objectiveC
935+
)
936+
)
937+
938+
externalResolver.entitiesToReturn["/path/to/external/swiftSymbol"] = .success(
939+
.init(
940+
referencePath: "/path/to/external/swiftSymbol",
941+
title: "SwiftSymbol",
942+
kind: .class,
943+
language: .swift
944+
)
945+
)
946+
947+
externalResolver.entitiesToReturn["/path/to/external/objCSymbol"] = .success(
948+
.init(
949+
referencePath: "/path/to/external/objCSymbol",
950+
title: "ObjCSymbol",
951+
kind: .class,
952+
language: .objectiveC
953+
)
954+
)
955+
956+
let (_, bundle, context) = try testBundleAndContext(
957+
copying: "MixedLanguageFramework",
958+
externalResolvers: [externalResolver.bundleIdentifier: externalResolver]
959+
) { url in
960+
let mixedLanguageFrameworkExtension = """
961+
# ``MixedLanguageFramework``
962+
963+
This symbol has a Swift and Objective-C variant.
964+
965+
## Topics
966+
967+
### External Reference
968+
969+
- <doc://com.test.external/path/to/external/swiftArticle>
970+
- <doc://com.test.external/path/to/external/swiftSymbol>
971+
- <doc://com.test.external/path/to/external/objCArticle>
972+
- <doc://com.test.external/path/to/external/objCSymbol>
973+
"""
974+
try mixedLanguageFrameworkExtension.write(to: url.appendingPathComponent("/MixedLanguageFramework.md"), atomically: true, encoding: .utf8)
975+
}
976+
let converter = DocumentationNodeConverter(bundle: bundle, context: context)
977+
let mixedLanguageFrameworkReference = ResolvedTopicReference(
978+
bundleIdentifier: bundle.identifier,
979+
path: "/documentation/MixedLanguageFramework",
980+
sourceLanguage: .swift
981+
)
982+
let node = try context.entity(with: mixedLanguageFrameworkReference)
983+
let fileURL = try XCTUnwrap(context.documentURL(for: node.reference))
984+
let renderNode = try converter.convert(node, at: fileURL)
985+
// Topic identifiers in the Swift variant of the `MixedLanguageFramework` symbol
986+
let swiftTopicIDs = renderNode.topicSections.flatMap(\.identifiers)
987+
988+
let data = try renderNode.encodeToJSON()
989+
let variantRenderNode = try RenderNodeVariantOverridesApplier()
990+
.applyVariantOverrides(in: data, for: [.interfaceLanguage("occ")])
991+
let objCRenderNode = try RenderJSONDecoder.makeDecoder().decode(RenderNode.self, from: variantRenderNode)
992+
// Topic identifiers in the ObjC variant of the `MixedLanguageFramework` symbol
993+
let objCTopicIDs = objCRenderNode.topicSections.flatMap(\.identifiers)
994+
995+
// Verify that external articles are included in the Topics section of both symbol
996+
// variants regardless of their perceived language.
997+
XCTAssertTrue(swiftTopicIDs.contains("doc://com.test.external/path/to/external/swiftArticle"))
998+
XCTAssertTrue(swiftTopicIDs.contains("doc://com.test.external/path/to/external/objCArticle"))
999+
XCTAssertTrue(objCTopicIDs.contains("doc://com.test.external/path/to/external/swiftArticle"))
1000+
XCTAssertTrue(objCTopicIDs.contains("doc://com.test.external/path/to/external/objCArticle"))
1001+
// Verify that external language specific symbols are dropped from the Topics section in the
1002+
// variants for languages where the symbol isn't available.
1003+
XCTAssertFalse(swiftTopicIDs.contains("doc://com.test.external/path/to/external/objCSymbol"))
1004+
XCTAssertTrue(swiftTopicIDs.contains("doc://com.test.external/path/to/external/swiftSymbol"))
1005+
XCTAssertTrue(objCTopicIDs.contains("doc://com.test.external/path/to/external/objCSymbol"))
1006+
XCTAssertFalse(objCTopicIDs.contains("doc://com.test.external/path/to/external/swiftSymbol"))
1007+
}
9151008
}

Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,88 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase {
888888
)
889889
}
890890

891+
func testArticlesAreIncludedInAllVariantsTopicsSection() throws {
892+
let outputConsumer = try renderNodeConsumer(
893+
for: "MixedLanguageFramework",
894+
configureBundle: { bundleURL in
895+
try """
896+
# ObjCArticle
897+
898+
@Metadata {
899+
@SupportedLanguage(objc)
900+
}
901+
902+
This article has Objective-C as the source language.
903+
904+
## Topics
905+
""".write(to: bundleURL.appendingPathComponent("ObjCArticle.md"), atomically: true, encoding: .utf8)
906+
try """
907+
# SwiftArticle
908+
909+
@Metadata {
910+
@SupportedLanguage(swift)
911+
}
912+
913+
This article has Swift as the source language.
914+
""".write(to: bundleURL.appendingPathComponent("SwiftArticle.md"), atomically: true, encoding: .utf8)
915+
try """
916+
# ``MixedLanguageFramework``
917+
918+
This symbol has a Swift and Objective-C variant.
919+
920+
## Topics
921+
922+
- <doc:ObjCArticle>
923+
- <doc:SwiftArticle>
924+
- ``_MixedLanguageFrameworkVersionNumber``
925+
- ``SwiftOnlyStruct``
926+
927+
""".write(to: bundleURL.appendingPathComponent("MixedLanguageFramework.md"), atomically: true, encoding: .utf8)
928+
}
929+
)
930+
assertIsAvailableInLanguages(
931+
try outputConsumer.renderNode(
932+
withTitle: "ObjCArticle"
933+
),
934+
languages: ["occ"],
935+
defaultLanguage: .objectiveC
936+
)
937+
assertIsAvailableInLanguages(
938+
try outputConsumer.renderNode(
939+
withTitle: "_MixedLanguageFrameworkVersionNumber"
940+
),
941+
languages: ["occ"],
942+
defaultLanguage: .objectiveC
943+
)
944+
945+
let renderNode = try outputConsumer.renderNode(withIdentifier: "MixedLanguageFramework")
946+
947+
// Topic identifiers in the Swift variant of the `MixedLanguageFramework` symbol
948+
let swiftTopicIDs = renderNode.topicSections.flatMap(\.identifiers)
949+
950+
let data = try renderNode.encodeToJSON()
951+
let variantRenderNode = try RenderNodeVariantOverridesApplier()
952+
.applyVariantOverrides(in: data, for: [.interfaceLanguage("occ")])
953+
let objCRenderNode = try RenderJSONDecoder.makeDecoder().decode(RenderNode.self, from: variantRenderNode)
954+
// Topic identifiers in the ObjC variant of the `MixedLanguageFramework` symbol
955+
let objCTopicIDs = objCRenderNode.topicSections.flatMap(\.identifiers)
956+
957+
958+
// Verify that articles are included in the Topics section of both symbol
959+
// variants regardless of their perceived language.
960+
XCTAssertTrue(swiftTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/ObjCArticle"))
961+
XCTAssertTrue(swiftTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/SwiftArticle"))
962+
XCTAssertTrue(objCTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/SwiftArticle"))
963+
XCTAssertTrue(objCTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/ObjCArticle"))
964+
965+
// Verify that language specific symbols are dropped from the Topics section in the
966+
// variants for languages where the symbol isn't available.
967+
XCTAssertTrue(swiftTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/SwiftOnlyStruct"))
968+
XCTAssertFalse(swiftTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/_MixedLanguageFrameworkVersionNumber"))
969+
XCTAssertTrue(objCTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/_MixedLanguageFrameworkVersionNumber"))
970+
XCTAssertFalse(objCTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/SwiftOnlyStruct"))
971+
}
972+
891973
func renderNodeApplyingObjectiveCVariantOverrides(to renderNode: RenderNode) throws -> RenderNode {
892974
return try renderNodeApplying(variant: "occ", to: renderNode)
893975
}

0 commit comments

Comments
 (0)