Skip to content

Commit 46c0e3d

Browse files
committed
Disambiguate external and local links by bundle ID
External and local links with the same path were colliding when building the navigator due to using the same bundle identifier to represent both, regardless of the real bundle identifier of the external link. [1] This commit updates the navigation indexing logic to respect the real bundle identifiers of the links, which should remove the chance of collisions between local and external links when building the navigator. Fixes rdar://163018922. [1]: https://github.com/swiftlang/swift-docc/blob/e15f4e10e60c1f554e1c27912804ab7272dbfaec/Sources/SwiftDocC/Infrastructure/ConvertActionConverter.swift#L106-L108
1 parent 137a1b2 commit 46c0e3d

File tree

5 files changed

+28
-53
lines changed

5 files changed

+28
-53
lines changed

Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -792,8 +792,8 @@ extension NavigatorIndex {
792792
navigationItem.usrIdentifier = language.name + "-" + ExternalIdentifier.usr(usr).hash // We pair the hash and the language name
793793
}
794794

795-
let navigatorNode = NavigatorTree.Node(item: navigationItem, bundleIdentifier: bundleIdentifier)
796-
795+
let navigatorNode = NavigatorTree.Node(item: navigationItem, bundleIdentifier: renderNode.identifier.bundleID.rawValue)
796+
797797
// Process the children
798798
var children = [Identifier]()
799799
for (index, child) in renderNode.navigatorChildren(for: traits).enumerated() {
@@ -820,8 +820,8 @@ extension NavigatorIndex {
820820

821821
groupItem.path = identifier.path + "#" + fragment
822822

823-
let navigatorGroup = NavigatorTree.Node(item: groupItem, bundleIdentifier: bundleIdentifier)
824-
823+
let navigatorGroup = NavigatorTree.Node(item: groupItem, bundleIdentifier: renderNode.identifier.bundleID.rawValue)
824+
825825
identifierToNode[identifier] = navigatorGroup
826826
children.append(identifier)
827827

@@ -831,8 +831,12 @@ extension NavigatorIndex {
831831
}
832832

833833
let identifiers = child.references.map { reference in
834+
let bundleIdentifier = reference.identifier.identifier
835+
.dropFirst(6) // Drop the `doc://` prefix
836+
.prefix(while: { $0 != "/" }) // Grab just the bundle identifier (the host component)
837+
.lowercased()
834838
return Identifier(
835-
bundleIdentifier: bundleIdentifier.lowercased(),
839+
bundleIdentifier: bundleIdentifier,
836840
path: reference.url.lowercased(),
837841
languageIdentifier: language.mask
838842
)

Sources/SwiftDocC/Infrastructure/ConvertActionConverter.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,8 @@ package enum ConvertActionConverter {
166166

167167
try signposter.withIntervalSignpost("Index external links", id: signposter.makeSignpostID()) {
168168
// Consume external links and add them into the sidebar.
169-
for externalLink in context.externalCache {
170-
// Here we're associating the external node with the **current** bundle's bundle ID.
171-
// This is needed because nodes are only considered children if the parent and child's bundle ID match.
172-
// Otherwise, the node will be considered as a separate root node and displayed separately.
173-
let externalRenderNode = ExternalRenderNode(externalEntity: externalLink.value, bundleIdentifier: context.inputs.id)
169+
for (_, externalLink) in context.externalCache {
170+
let externalRenderNode = ExternalRenderNode(externalEntity: externalLink)
174171
try outputConsumer.consume(externalRenderNode: externalRenderNode)
175172
}
176173
}

Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver+NavigatorIndex.swift

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,16 @@ import SymbolKit
1515
package struct ExternalRenderNode {
1616
private var entity: LinkResolver.ExternalEntity
1717
private var topicRenderReference: TopicRenderReference
18-
19-
/// The bundle identifier for this external node.
20-
private var bundleIdentifier: DocumentationBundle.Identifier
2118

22-
// This type is designed to misrepresent external content as local content to fit in with the navigator.
23-
// This spreads the issue to more code rather than fixing it, which adds technical debt and can be fragile.
24-
//
25-
// At the time of writing this comment, this type and the issues it comes with has spread to 6 files (+ 3 test files).
26-
// Luckily, none of that code is public API so we can modify or even remove it without compatibility restrictions.
27-
init(externalEntity: LinkResolver.ExternalEntity, bundleIdentifier: DocumentationBundle.Identifier) {
19+
init(externalEntity: LinkResolver.ExternalEntity) {
2820
self.entity = externalEntity
29-
self.bundleIdentifier = bundleIdentifier
3021
self.topicRenderReference = externalEntity.makeTopicRenderReference()
3122
}
3223

3324
/// The identifier of the external render node.
3425
package var identifier: ResolvedTopicReference {
3526
ResolvedTopicReference(
36-
bundleID: bundleIdentifier,
27+
bundleID: .init(rawValue: entity.referenceURL.host!),
3728
path: entity.referenceURL.path,
3829
fragment: entity.referenceURL.fragment,
3930
sourceLanguages: entity.availableLanguages

Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -128,32 +128,32 @@ class ExternalRenderNodeTests: XCTestCase {
128128
}
129129

130130
let externalRenderNodes = context.externalCache.valuesByReference.values.map {
131-
ExternalRenderNode(externalEntity: $0, bundleIdentifier: bundle.id)
131+
ExternalRenderNode(externalEntity: $0)
132132
}.sorted(by: \.titleVariants.defaultValue)
133133
XCTAssertEqual(externalRenderNodes.count, 4)
134134

135-
XCTAssertEqual(externalRenderNodes[0].identifier.absoluteString, "doc://org.swift.MixedLanguageFramework/path/to/external/objCArticle")
135+
XCTAssertEqual(externalRenderNodes[0].identifier.absoluteString, "doc://com.test.external/path/to/external/objCArticle")
136136
XCTAssertEqual(externalRenderNodes[0].kind, .article)
137137
XCTAssertEqual(externalRenderNodes[0].symbolKind, nil)
138138
XCTAssertEqual(externalRenderNodes[0].role, "article")
139139
XCTAssertEqual(externalRenderNodes[0].externalIdentifier.identifier, "doc://com.test.external/path/to/external/objCArticle")
140140
XCTAssertTrue(externalRenderNodes[0].isBeta)
141141

142-
XCTAssertEqual(externalRenderNodes[1].identifier.absoluteString, "doc://org.swift.MixedLanguageFramework/path/to/external/objCSymbol")
142+
XCTAssertEqual(externalRenderNodes[1].identifier.absoluteString, "doc://com.test.external/path/to/external/objCSymbol")
143143
XCTAssertEqual(externalRenderNodes[1].kind, .symbol)
144144
XCTAssertEqual(externalRenderNodes[1].symbolKind, .func)
145145
XCTAssertEqual(externalRenderNodes[1].role, "symbol")
146146
XCTAssertEqual(externalRenderNodes[1].externalIdentifier.identifier, "doc://com.test.external/path/to/external/objCSymbol")
147147
XCTAssertFalse(externalRenderNodes[1].isBeta)
148148

149-
XCTAssertEqual(externalRenderNodes[2].identifier.absoluteString, "doc://org.swift.MixedLanguageFramework/path/to/external/swiftArticle")
149+
XCTAssertEqual(externalRenderNodes[2].identifier.absoluteString, "doc://com.test.external/path/to/external/swiftArticle")
150150
XCTAssertEqual(externalRenderNodes[2].kind, .article)
151151
XCTAssertEqual(externalRenderNodes[2].symbolKind, nil)
152152
XCTAssertEqual(externalRenderNodes[2].role, "article")
153153
XCTAssertEqual(externalRenderNodes[2].externalIdentifier.identifier, "doc://com.test.external/path/to/external/swiftArticle")
154154
XCTAssertFalse(externalRenderNodes[2].isBeta)
155155

156-
XCTAssertEqual(externalRenderNodes[3].identifier.absoluteString, "doc://org.swift.MixedLanguageFramework/path/to/external/swiftSymbol")
156+
XCTAssertEqual(externalRenderNodes[3].identifier.absoluteString, "doc://com.test.external/path/to/external/swiftSymbol")
157157
XCTAssertEqual(externalRenderNodes[3].kind, .symbol)
158158
XCTAssertEqual(externalRenderNodes[3].symbolKind, .class)
159159
XCTAssertEqual(externalRenderNodes[3].role, "symbol")
@@ -190,10 +190,7 @@ class ExternalRenderNodeTests: XCTestCase {
190190
)
191191
]
192192
)
193-
let externalRenderNode = ExternalRenderNode(
194-
externalEntity: externalEntity,
195-
bundleIdentifier: "com.test.external"
196-
)
193+
let externalRenderNode = ExternalRenderNode(externalEntity: externalEntity)
197194

198195
let swiftNavigatorExternalRenderNode = try XCTUnwrap(
199196
NavigatorExternalRenderNode(renderNode: externalRenderNode)
@@ -252,8 +249,8 @@ class ExternalRenderNodeTests: XCTestCase {
252249
let targetURL = try createTemporaryDirectory()
253250
let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: context.inputs.id.rawValue, sortRootChildrenByName: true, groupByLanguage: true)
254251
builder.setup()
255-
for externalLink in context.externalCache {
256-
let externalRenderNode = ExternalRenderNode(externalEntity: externalLink.value, bundleIdentifier: context.inputs.id)
252+
for (_, externalLink) in context.externalCache {
253+
let externalRenderNode = ExternalRenderNode(externalEntity: externalLink)
257254
try builder.index(renderNode: externalRenderNode)
258255
}
259256
for identifier in context.knownPages {
@@ -355,8 +352,8 @@ class ExternalRenderNodeTests: XCTestCase {
355352
let targetURL = try createTemporaryDirectory()
356353
let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: context.inputs.id.rawValue, sortRootChildrenByName: true, groupByLanguage: true)
357354
builder.setup()
358-
for externalLink in context.externalCache {
359-
let externalRenderNode = ExternalRenderNode(externalEntity: externalLink.value, bundleIdentifier: context.inputs.id)
355+
for (_, externalLink) in context.externalCache {
356+
let externalRenderNode = ExternalRenderNode(externalEntity: externalLink)
360357
try builder.index(renderNode: externalRenderNode)
361358
}
362359
for identifier in context.knownPages {
@@ -433,8 +430,8 @@ class ExternalRenderNodeTests: XCTestCase {
433430
let targetURL = try createTemporaryDirectory()
434431
let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: context.inputs.id.rawValue, sortRootChildrenByName: true, groupByLanguage: true)
435432
builder.setup()
436-
for externalLink in context.externalCache {
437-
let externalRenderNode = ExternalRenderNode(externalEntity: externalLink.value, bundleIdentifier: context.inputs.id)
433+
for (_, externalLink) in context.externalCache {
434+
let externalRenderNode = ExternalRenderNode(externalEntity: externalLink)
438435
try builder.index(renderNode: externalRenderNode)
439436
}
440437
for identifier in context.knownPages {
@@ -490,10 +487,7 @@ class ExternalRenderNodeTests: XCTestCase {
490487
)
491488
]
492489
)
493-
let externalRenderNode = ExternalRenderNode(
494-
externalEntity: externalEntity,
495-
bundleIdentifier: "com.test.external"
496-
)
490+
let externalRenderNode = ExternalRenderNode(externalEntity: externalEntity)
497491

498492
let swiftNavigatorExternalRenderNode = try XCTUnwrap(
499493
NavigatorExternalRenderNode(renderNode: externalRenderNode)

Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3300,21 +3300,10 @@ class ConvertActionTests: XCTestCase {
33003300
{
33013301
"children": [
33023302
{
3303-
"children": [
3304-
{
3305-
"path": "/documentation/testbundle/childarticlea",
3306-
"title": "ChildArticleA",
3307-
"type": "article"
3308-
},
3309-
{
3310-
"path": "/documentation/testbundle/childarticleb",
3311-
"title": "ChildArticleB",
3312-
"type": "article"
3313-
}
3314-
],
33153303
"path": "/documentation/testbundle/sampleclass",
33163304
"title": "SampleClass",
3317-
"type": "class"
3305+
"type": "class",
3306+
"external": true
33183307
}
33193308
],
33203309
"path": "/documentation/testbundle/article",

0 commit comments

Comments
 (0)