From 812c56aa1ceb360fb0fd0fea41093ff8c2d2720e Mon Sep 17 00:00:00 2001 From: Prajwal Nadig Date: Mon, 4 Aug 2025 14:24:17 +0100 Subject: [PATCH] Include symbols without common lang in navigator The Topics section has a filtering logic to ensure that it displays the correct variant of a referenced symbol (e.g. the Objective-C variant of the symbol is shown only for the Objective-C version of a page). If the page and symbol have no languages in common, the symbol will be shown anyway in order to not drop the reference completely. For example, if a Swift-only symbol is curated under an Objective-C only page, the symbol will be shown in the Topics section even though their languages don't match. The navigator also has a similar filtering logic to display all references present in the Topics section. However, it drops a reference if the pages don't have any source languages in common. This results in cases where the navigator is incorrectly missing references that are present in the Topics section. This issue can be reproduced with the `MixedLanguageFrameworkSingleLanguageCuration.docc` test bundle by running `xcrun docc preview Tests/SwiftDocCTests/Test\ Bundles/MixedLanguageFrameworkSingleLanguageCuration.docc` and viewing the navigator in the preview. This patch updates the navigator's filtering logic to include references if they do not share a common language with the relevant page. rdar://155522179 --- .../Indexing/Navigator/NavigatorIndex.swift | 66 ++++++++++++++----- .../Indexing/NavigatorIndexTests.swift | 26 ++++++++ .../MixedLanguageFramework.md | 1 - 3 files changed, 77 insertions(+), 16 deletions(-) diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift index 63a427ff87..8b845b6681 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift @@ -230,12 +230,12 @@ public class NavigatorIndex { } /** - Initialize an `NavigatorIndex` from a given path with an empty tree. + Initialize a `NavigatorIndex` from a given path with an empty tree. - Parameter url: The URL pointing to the path from which the index should be read. - Parameter bundleIdentifier: The name of the bundle the index is referring to. - - Note: Don't exposed this initializer as it's used **ONLY** for building an index. + - Note: Don't expose this initializer as it's used **ONLY** for building an index. */ fileprivate init(withEmptyTree url: URL, bundleIdentifier: String) throws { self.url = url @@ -466,6 +466,17 @@ extension NavigatorIndex { self.fragment = fragment self.languageIdentifier = languageIdentifier } + + /// Compare an identifier with another one, ignoring the identifier language. + /// + /// Used when curating cross-language references in multi-language frameworks. + /// + /// - Parameter other: The other identifier to compare with. + func isEquivalentIgnoringLanguage(to other: Identifier) -> Bool { + return self.bundleIdentifier == other.bundleIdentifier && + self.path == other.path && + self.fragment == other.fragment + } } /** @@ -916,7 +927,7 @@ extension NavigatorIndex { /// - emitJSONRepresentation: Whether or not a JSON representation of the index should /// be written to disk. /// - /// Defaults to `false`. + /// Defaults to `true`. /// /// - emitLMDBRepresentation: Whether or not an LMDB representation of the index should /// written to disk. @@ -949,14 +960,28 @@ extension NavigatorIndex { let (nodeID, parent) = nodesMultiCurated[index] let placeholders = identifierToChildren[nodeID]! for reference in placeholders { - if let child = identifierToNode[reference] { + var child = identifierToNode[reference] + var childReference = reference + + // If no child node exists in this language, try to find one across all available languages. + if child == nil { + if let match = identifierToNode.first(where: { (identifier, node) in + identifier.isEquivalentIgnoringLanguage(to: reference) && + PageType(rawValue: node.item.pageType)?.isSymbolKind == true + }) { + childReference = match.key + child = match.value + } + } + + if let child = child { parent.add(child: child) - pendingUncuratedReferences.remove(reference) - if !multiCurated.keys.contains(reference) && reference.fragment == nil { + pendingUncuratedReferences.remove(childReference) + if !multiCurated.keys.contains(childReference) && childReference.fragment == nil { // As the children of a multi-curated node is itself curated multiple times // we need to process it as well, ignoring items with fragments as those are sections. - nodesMultiCuratedChildren.append((reference, child)) - multiCurated[reference] = child + nodesMultiCuratedChildren.append((childReference, child)) + multiCurated[childReference] = child } } } @@ -970,10 +995,24 @@ extension NavigatorIndex { for (nodeIdentifier, placeholders) in identifierToChildren { for reference in placeholders { let parent = identifierToNode[nodeIdentifier]! - if let child = identifierToNode[reference] { - let needsCopy = multiCurated[reference] != nil + var child = identifierToNode[reference] + var childReference = reference + + // If no child node exists in this language, try to find one across all available languages. + if child == nil { + if let match = identifierToNode.first(where: { (identifier, node) in + identifier.isEquivalentIgnoringLanguage(to: reference) && + PageType(rawValue: node.item.pageType)?.isSymbolKind == true + }) { + childReference = match.key + child = match.value + } + } + + if let child = child { + let needsCopy = multiCurated[childReference] != nil parent.add(child: (needsCopy) ? child.copy() : child) - pendingUncuratedReferences.remove(reference) + pendingUncuratedReferences.remove(childReference) } } } @@ -1005,10 +1044,7 @@ extension NavigatorIndex { // If an uncurated page has been curated in another language, don't add it to the top-level. if curatedReferences.contains(where: { curatedNodeID in - // Compare all the identifier's properties for equality, except for its language. - curatedNodeID.bundleIdentifier == nodeID.bundleIdentifier - && curatedNodeID.path == nodeID.path - && curatedNodeID.fragment == nodeID.fragment + curatedNodeID.isEquivalentIgnoringLanguage(to: nodeID) }) { continue } diff --git a/Tests/SwiftDocCTests/Indexing/NavigatorIndexTests.swift b/Tests/SwiftDocCTests/Indexing/NavigatorIndexTests.swift index 9c64514d31..9675305910 100644 --- a/Tests/SwiftDocCTests/Indexing/NavigatorIndexTests.swift +++ b/Tests/SwiftDocCTests/Indexing/NavigatorIndexTests.swift @@ -894,6 +894,32 @@ Root ) } + /// Test the curation of a symbol in a page where the page language is different from the symbol language. + /// This occurs in multi-language frameworks when a symbol in one language is referenced in another, e.g. + /// a Swift-only symbol includes a link to an Objective-C only symbol. + func testCurateSymbolsInPageWithNoCommonLanguage() async throws { + let navigatorIndex = try await generatedNavigatorIndex( + for: "MixedLanguageFrameworkSingleLanguageCuration", + bundleIdentifier: "org.swift.mixedlanguageframework" + ) + + XCTAssertEqual( + navigatorIndex.navigatorTree.root.children + .first { $0.item.title == "Swift" }? + .children + .first { $0.item.title == "MixedLanguageFramework" }? + .children + .first { $0.item.title == "SwiftOnlyStruct2" }? + .children + .contains { $0.item.title == "ObjectiveCOnlyClass" }, + true, + """ + Expected the Objective-C-only node with title "ObjectiveCOnlyClass" to be curated in the Swift \ + navigator tree. + """ + ) + } + func testNavigatorIndexUsingPageTitleGeneration() async throws { let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let renderContext = RenderContext(documentationContext: context, bundle: bundle) diff --git a/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFrameworkSingleLanguageCuration.docc/MixedLanguageFramework.md b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFrameworkSingleLanguageCuration.docc/MixedLanguageFramework.md index 2fcb107d44..1df3d61e93 100644 --- a/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFrameworkSingleLanguageCuration.docc/MixedLanguageFramework.md +++ b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFrameworkSingleLanguageCuration.docc/MixedLanguageFramework.md @@ -6,7 +6,6 @@ These symbols are Objective-C only and curated in multiple places in the catalog. -- ``MultiCuratedObjectiveCOnlyClass1`` - ``MultiCuratedObjectiveCOnlyClass1`` - ``MultiCuratedObjectiveCOnlyClass2``