Skip to content

Commit 18561d4

Browse files
authored
Prevent a manual technology root from overriding the module node with the same name (#770)
* Don't override the module node with a same-name manual technology root rdar://119389007 * Preserve symbol kinds when deserializing a path hierarchy from another archive * Add comments to describe why symbol kinds are filled in after creation
1 parent 61775b4 commit 18561d4

8 files changed

+73
-25
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ public class LinkResolver {
3232
try ExternalPathHierarchyResolver(dependencyArchive: $0)
3333
}
3434
for resolver in resolvers {
35-
for moduleName in resolver.pathHierarchy.modules.keys {
36-
self.externalResolvers[moduleName] = resolver
35+
for moduleNode in resolver.pathHierarchy.modules {
36+
self.externalResolvers[moduleNode.name] = resolver
3737
}
3838
}
3939
}

Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+DisambiguatedPaths.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ extension PathHierarchy {
6868

6969
var gathered: [(String, (String, Bool))] = []
7070

71-
for (moduleName, node) in modules {
72-
let path = "/" + moduleName
71+
for node in modules {
72+
let path = "/" + node.name
7373
gathered.append(
74-
(moduleName, (path, node.symbol == nil || node.symbol!.identifier.interfaceLanguage == "swift"))
74+
(node.name, (path, node.symbol == nil || node.symbol!.identifier.interfaceLanguage == "swift"))
7575
)
7676
gathered += descend(node, accumulatedPath: path)
7777
}

Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Dump.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ private extension PathHierarchy.Node {
4747
extension PathHierarchy {
4848
/// Creates a visual representation or the path hierarchy for debugging.
4949
func dump() -> String {
50-
var children = modules.sorted(by: \.key).map { $0.value.dumpableNode() }
50+
var children = modules.sorted(by: \.name).map { $0.dumpableNode() }
5151
if articlesContainer.symbol == nil {
5252
children.append(articlesContainer.dumpableNode()) // The article parent can be the same node as the module
5353
}

Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Find.swift

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,12 @@ extension PathHierarchy {
8585
// A function to avoid repeating the
8686
func searchForNodeInModules() throws -> Node {
8787
// Note: This captures `parentID`, `remaining`, and `rawPathForError`.
88-
if let moduleMatch = modules[firstComponent.full] ?? modules[String(firstComponent.name)] {
88+
if let moduleMatch = modules.first(where: { $0.matches(firstComponent) }) {
8989
return try searchForNode(descendingFrom: moduleMatch, pathComponents: remaining.dropFirst(), onlyFindSymbols: onlyFindSymbols, rawPathForError: rawPath)
9090
}
9191
if modules.count == 1 {
9292
do {
93-
return try searchForNode(descendingFrom: modules.first!.value, pathComponents: remaining, onlyFindSymbols: onlyFindSymbols, rawPathForError: rawPath)
93+
return try searchForNode(descendingFrom: modules.first!, pathComponents: remaining, onlyFindSymbols: onlyFindSymbols, rawPathForError: rawPath)
9494
} catch let error as PathHierarchy.Error {
9595
switch error {
9696
case .notFound:
@@ -115,13 +115,13 @@ extension PathHierarchy {
115115
}
116116
}
117117
}
118-
let topLevelNames = Set(modules.keys + [articlesContainer.name, tutorialContainer.name])
118+
let topLevelNames = Set(modules.map(\.name) + [articlesContainer.name, tutorialContainer.name])
119119

120120
if isAbsolute, FeatureFlags.current.isExperimentalLinkHierarchySerializationEnabled {
121121
throw Error.moduleNotFound(
122122
pathPrefix: pathForError(of: rawPath, droppingLast: remaining.count),
123123
remaining: Array(remaining),
124-
availableChildren: Set(modules.keys)
124+
availableChildren: Set(modules.map(\.name))
125125
)
126126
} else {
127127
throw Error.notFound(
@@ -475,7 +475,12 @@ private extension Sequence {
475475

476476
private extension PathHierarchy.Node {
477477
func matches(_ component: PathHierarchy.PathComponent) -> Bool {
478-
if let symbol = symbol {
478+
// Check the full path component first in case the node's name has a suffix that could be mistaken for a hash disambiguation.
479+
if name == component.full {
480+
return true
481+
}
482+
// Otherwise, check if the node's symbol matches the provided disambiguation
483+
else if let symbol = symbol {
479484
guard name == component.name else {
480485
return false
481486
}
@@ -486,9 +491,9 @@ private extension PathHierarchy.Node {
486491
return false
487492
}
488493
return true
489-
} else {
490-
return name == component.full
491494
}
495+
496+
return false
492497
}
493498

494499
func anyChildMatches(_ component: PathHierarchy.PathComponent) -> Bool {

Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Serialization.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ extension PathHierarchy.FileRepresentation {
6060
}
6161

6262
self.nodes = nodes
63-
self.modules = pathHierarchy.modules.mapValues({ identifierMap[$0.identifier]! })
63+
self.modules = pathHierarchy.modules.map({ identifierMap[$0.identifier]! })
6464
self.articlesContainer = identifierMap[pathHierarchy.articlesContainer.identifier]!
6565
self.tutorialContainer = identifierMap[pathHierarchy.tutorialContainer.identifier]!
6666
self.tutorialOverviewContainer = identifierMap[pathHierarchy.tutorialOverviewContainer.identifier]!
@@ -93,7 +93,7 @@ extension PathHierarchy {
9393
var nodes: [Node]
9494

9595
/// The module nodes in this hierarchy.
96-
var modules: [String: Int]
96+
var modules: [Int]
9797
/// The container for articles and reference documentation.
9898
var articlesContainer: Int
9999
/// The container of tutorials.
@@ -174,7 +174,7 @@ extension PathHierarchyBasedLinkResolver {
174174
}
175175

176176
return SerializableLinkResolutionInformation(
177-
version: .init(major: 0, minor: 0, patch: 1), // This is still in development
177+
version: .init(major: 0, minor: 1, patch: 0), // This is still in development
178178
bundleID: bundleID,
179179
pathHierarchy: hierarchyFileRepresentation,
180180
nonSymbolPaths: nonSymbolPaths

Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ struct ResolvedIdentifier: Equatable, Hashable {
3737
/// After a path hierarchy has been fully created — with both symbols and non-symbols — it can be used to find elements in the hierarchy and to determine the least disambiguated paths for all elements.
3838
struct PathHierarchy {
3939

40-
/// A map of module names to module nodes.
41-
private(set) var modules: [String: Node]
40+
/// The list of module nodes.
41+
private(set) var modules: [Node]
4242
/// The container of top-level articles in the documentation hierarchy.
4343
let articlesContainer: Node
4444
/// The container of tutorials in the documentation hierarchy.
@@ -295,7 +295,7 @@ struct PathHierarchy {
295295
"""
296296
)
297297

298-
self.modules = roots
298+
self.modules = Array(roots.values)
299299
self.lookup = lookup
300300

301301
assert(topLevelSymbols().allSatisfy({ lookup[$0] != nil }))
@@ -352,7 +352,7 @@ struct PathHierarchy {
352352
newNode.identifier = newReference
353353
self.lookup[newReference] = newNode
354354

355-
modules[name] = newNode
355+
modules.append(newNode)
356356

357357
return newReference
358358
}
@@ -377,7 +377,7 @@ extension PathHierarchy {
377377

378378
fileprivate(set) unowned var parent: Node?
379379
/// The symbol, if a node has one.
380-
private(set) var symbol: SymbolGraph.Symbol?
380+
fileprivate(set) var symbol: SymbolGraph.Symbol?
381381

382382
/// If the path hierarchy should disfavor this node in a link collision.
383383
///
@@ -452,7 +452,7 @@ extension PathHierarchy {
452452
func topLevelSymbols() -> [ResolvedIdentifier] {
453453
var result: Set<ResolvedIdentifier> = []
454454
// Roots represent modules and only have direct symbol descendants.
455-
for root in modules.values {
455+
for root in modules {
456456
for (_, tree) in root.children {
457457
for subtree in tree.storage.values {
458458
for node in subtree.values where node.symbol != nil {
@@ -461,7 +461,7 @@ extension PathHierarchy {
461461
}
462462
}
463463
}
464-
return Array(result) + modules.values.map { $0.identifier }
464+
return Array(result) + modules.map { $0.identifier }
465465
}
466466
}
467467

@@ -566,6 +566,8 @@ extension PathHierarchy {
566566
pathComponents: [],
567567
docComment: nil,
568568
accessLevel: .public,
569+
// To make the file format smaller we don't store the symbol kind identifiers with each node. Instead, the kind identifier is stored
570+
// as disambiguation and is filled in while building up the hierarchy below.
569571
kind: SymbolGraph.Symbol.Kind(rawIdentifier: "", displayName: ""),
570572
mixins: [:]
571573
)
@@ -584,11 +586,21 @@ extension PathHierarchy {
584586
let childNode = lookup[identifiers[child.nodeID]]!
585587
// Even if this is a symbol node, explicitly pass the kind and hash disambiguation.
586588
node.add(child: childNode, kind: child.kind, hash: child.hash)
589+
if let kind = child.kind {
590+
// Since the symbol was created with an empty symbol kind, fill in its kind identifier here.
591+
childNode.symbol?.kind.identifier = .init(identifier: kind)
592+
}
587593
}
588594
}
589595

590596
self.lookup = lookup
591-
self.modules = fileRepresentation.modules.mapValues({ lookup[identifiers[$0]]! })
597+
let modules = fileRepresentation.modules.map({ lookup[identifiers[$0]]! })
598+
// Fill in the symbol kind of all modules. This is needed since the modules were created with empty symbol kinds and since no other symbol has a
599+
// module as its child, so the modules didn't get their symbol kind set when building up the hierarchy above.
600+
for node in modules {
601+
node.symbol?.kind.identifier = .module
602+
}
603+
self.modules = modules
592604
self.articlesContainer = lookup[identifiers[fileRepresentation.articlesContainer]]!
593605
self.tutorialContainer = lookup[identifiers[fileRepresentation.tutorialContainer]]!
594606
self.tutorialOverviewContainer = lookup[identifiers[fileRepresentation.tutorialOverviewContainer]]!

Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ final class PathHierarchyBasedLinkResolver {
6565

6666
/// Returns a list of all module symbols.
6767
func modules() -> [ResolvedTopicReference] {
68-
return pathHierarchy.modules.values.map { resolvedReferenceMap[$0.identifier]! }
68+
return pathHierarchy.modules.map { resolvedReferenceMap[$0.identifier]! }
6969
}
7070

7171
// MARK: - Adding non-symbols

Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1755,6 +1755,37 @@ class PathHierarchyTests: XCTestCase {
17551755
XCTAssertEqual(paths[memberID], "/ModuleName/ContainerName-qwwf/MemberName1")
17561756
}
17571757

1758+
func testModuleAndCollidingTechnologyRootHasPathsForItsSymbols() throws {
1759+
let symbolID = "some-symbol-id"
1760+
1761+
let exampleDocumentation = Folder(name: "unit-test.docc", content: [
1762+
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
1763+
moduleName: "ModuleName",
1764+
symbols: [
1765+
(symbolID, .swift, ["SymbolName"]),
1766+
],
1767+
relationships: []
1768+
)),
1769+
1770+
TextFile(name: "ModuleName.md", utf8Content: """
1771+
# Manual Technology Root
1772+
1773+
@Metadata {
1774+
@TechnologyRoot
1775+
}
1776+
1777+
A technology root with the same file name as the module name.
1778+
""")
1779+
])
1780+
1781+
let tempURL = try createTempFolder(content: [exampleDocumentation])
1782+
let (_, _, context) = try loadBundle(from: tempURL)
1783+
let tree = try XCTUnwrap(context.linkResolver.localResolver?.pathHierarchy)
1784+
1785+
let paths = tree.caseInsensitiveDisambiguatedPaths(includeDisambiguationForUnambiguousChildren: true)
1786+
XCTAssertEqual(paths[symbolID], "/ModuleName/SymbolName")
1787+
}
1788+
17581789
func testMultiPlatformModuleWithExtension() throws {
17591790
let (_, context) = try testBundleAndContext(named: "MultiPlatformModuleWithExtension")
17601791
let tree = try XCTUnwrap(context.linkResolver.localResolver?.pathHierarchy)

0 commit comments

Comments
 (0)