Skip to content

Commit 5ba3de2

Browse files
clone path hierarchy nodes when their parent has a language-specific counterpart (#1203) (#1209)
rdar://144862231
1 parent 10aaa0a commit 5ba3de2

File tree

2 files changed

+141
-3
lines changed

2 files changed

+141
-3
lines changed

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

+62-2
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,51 @@ struct PathHierarchy {
148148
// would require that we redundantly create multiple nodes for the same symbol in many common cases and then merge them. To avoid doing that, we instead check
149149
// the source symbol's path components to find the correct target symbol by matching its name.
150150
if let targetNode = nodes[relationship.target], targetNode.name == expectedContainerName {
151-
targetNode.add(symbolChild: sourceNode)
151+
if sourceNode.parent == nil {
152+
targetNode.add(symbolChild: sourceNode)
153+
} else if sourceNode.parent !== targetNode {
154+
// If the node we have for the child has an existing parent that doesn't
155+
// match the parent from this symbol graph, we need to clone the child to
156+
// ensure that the hierarchy remains consistent.
157+
let clonedSourceNode = Node(
158+
cloning: sourceNode,
159+
symbol: graph.symbols[relationship.source],
160+
children: [:],
161+
languages: [language!]
162+
)
163+
164+
// The original node no longer represents this symbol graph's language,
165+
// so remove that data from there.
166+
sourceNode.languages.remove(language!)
167+
168+
// Make sure that the clone's children can all line up with symbols from this symbol graph.
169+
for (childName, children) in sourceNode.children {
170+
for child in children.storage {
171+
guard let childSymbol = child.node.symbol else {
172+
// We shouldn't come across any non-symbol nodes here,
173+
// but assume they can work as child of both variants.
174+
clonedSourceNode.add(child: child.node, kind: child.kind, hash: child.hash)
175+
continue
176+
}
177+
if nodes[childSymbol.identifier.precise] === child.node {
178+
clonedSourceNode.add(symbolChild: child.node)
179+
}
180+
}
181+
}
182+
183+
// Track the cloned node in the lists of nodes.
184+
nodes[relationship.source] = clonedSourceNode
185+
if let existingNodes = allNodes[relationship.source] {
186+
clonedSourceNode.counterpart = existingNodes.first
187+
for other in existingNodes {
188+
other.counterpart = clonedSourceNode
189+
}
190+
}
191+
allNodes[relationship.source, default: []].append(clonedSourceNode)
192+
193+
// Finally, add the cloned node as a child of its parent.
194+
targetNode.add(symbolChild: clonedSourceNode)
195+
}
152196
topLevelCandidates.removeValue(forKey: relationship.source)
153197
} else if var targetNodes = allNodes[relationship.target] {
154198
// If the source was added in an extension symbol graph file, then its target won't be found in the same symbol graph file (in `nodes`).
@@ -532,7 +576,21 @@ extension PathHierarchy {
532576
self.children = [:]
533577
self.specialBehaviors = []
534578
}
535-
579+
580+
/// Initializes a node with a new identifier but the data from an existing node.
581+
fileprivate init(
582+
cloning source: Node,
583+
symbol: SymbolGraph.Symbol?? = nil,
584+
children: [String: DisambiguationContainer]? = nil,
585+
languages: Set<SourceLanguage>? = nil
586+
) {
587+
self.symbol = symbol ?? source.symbol
588+
self.name = source.name
589+
self.children = children ?? source.children
590+
self.specialBehaviors = source.specialBehaviors
591+
self.languages = languages ?? source.languages
592+
}
593+
536594
/// Adds a descendant to this node, providing disambiguation information from the node's symbol.
537595
fileprivate func add(symbolChild: Node) {
538596
precondition(symbolChild.symbol != nil)
@@ -558,6 +616,8 @@ extension PathHierarchy {
558616
)
559617
return
560618
}
619+
620+
assert(child.parent == nil, "Nodes that already have a parent should not be added to a different parent.")
561621
// If the name was passed explicitly, then the node could have spaces in its name
562622
child.parent = self
563623
children[child.name, default: .init()].add(child, kind: kind, hash: hash, parameterTypes: parameterTypes, returnTypes: returnTypes)

Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift

+79-1
Original file line numberDiff line numberDiff line change
@@ -2820,7 +2820,85 @@ class PathHierarchyTests: XCTestCase {
28202820
XCTAssertEqual(paths[containerID], "/ModuleName/ContainerName")
28212821
XCTAssertEqual(paths[memberID], "/ModuleName/ContainerName/memberName") // The Swift spelling is preferred
28222822
}
2823-
2823+
2824+
func testLanguageRepresentationsWithDifferentParentKinds() throws {
2825+
enableFeatureFlag(\.isExperimentalLinkHierarchySerializationEnabled)
2826+
2827+
let containerID = "some-container-symbol-id"
2828+
let memberID = "some-member-symbol-id"
2829+
2830+
let catalog = Folder(name: "unit-test.docc", content: [
2831+
Folder(name: "clang", content: [
2832+
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
2833+
moduleName: "ModuleName",
2834+
symbols: [
2835+
makeSymbol(id: containerID, language: .objectiveC, kind: .union, pathComponents: ["ContainerName"]),
2836+
makeSymbol(id: memberID, language: .objectiveC, kind: .property, pathComponents: ["ContainerName", "MemberName"]),
2837+
],
2838+
relationships: [
2839+
.init(source: memberID, target: containerID, kind: .memberOf, targetFallback: nil)
2840+
]
2841+
)),
2842+
]),
2843+
2844+
Folder(name: "swift", content: [
2845+
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
2846+
moduleName: "ModuleName",
2847+
symbols: [
2848+
makeSymbol(id: containerID, kind: .struct, pathComponents: ["ContainerName"]),
2849+
makeSymbol(id: memberID, kind: .property, pathComponents: ["ContainerName", "MemberName"]),
2850+
],
2851+
relationships: [
2852+
.init(source: memberID, target: containerID, kind: .memberOf, targetFallback: nil)
2853+
]
2854+
)),
2855+
])
2856+
])
2857+
2858+
let (_, context) = try loadBundle(catalog: catalog)
2859+
let tree = context.linkResolver.localResolver.pathHierarchy
2860+
2861+
let resolvedSwiftContainerID = try tree.find(path: "/ModuleName/ContainerName-struct", onlyFindSymbols: true)
2862+
let resolvedSwiftContainer = try XCTUnwrap(tree.lookup[resolvedSwiftContainerID])
2863+
XCTAssertEqual(resolvedSwiftContainer.name, "ContainerName")
2864+
XCTAssertEqual(resolvedSwiftContainer.symbol?.identifier.precise, containerID)
2865+
XCTAssertEqual(resolvedSwiftContainer.symbol?.kind.identifier, .struct)
2866+
XCTAssertEqual(resolvedSwiftContainer.languages, [.swift])
2867+
2868+
let resolvedObjcContainerID = try tree.find(path: "/ModuleName/ContainerName-union", onlyFindSymbols: true)
2869+
let resolvedObjcContainer = try XCTUnwrap(tree.lookup[resolvedObjcContainerID])
2870+
XCTAssertEqual(resolvedObjcContainer.name, "ContainerName")
2871+
XCTAssertEqual(resolvedObjcContainer.symbol?.identifier.precise, containerID)
2872+
XCTAssertEqual(resolvedObjcContainer.symbol?.kind.identifier, .union)
2873+
XCTAssertEqual(resolvedObjcContainer.languages, [.objectiveC])
2874+
2875+
let resolvedContainerID = try tree.find(path: "/ModuleName/ContainerName", onlyFindSymbols: true)
2876+
XCTAssertEqual(resolvedContainerID, resolvedSwiftContainerID)
2877+
2878+
let resolvedSwiftMemberID = try tree.find(path: "/ModuleName/ContainerName-struct/MemberName", onlyFindSymbols: true)
2879+
let resolvedSwiftMember = try XCTUnwrap(tree.lookup[resolvedSwiftMemberID])
2880+
XCTAssertEqual(resolvedSwiftMember.name, "MemberName")
2881+
XCTAssertEqual(resolvedSwiftMember.parent?.identifier, resolvedSwiftContainerID)
2882+
XCTAssertEqual(resolvedSwiftMember.symbol?.identifier.precise, memberID)
2883+
XCTAssertEqual(resolvedSwiftMember.symbol?.kind.identifier, .property)
2884+
XCTAssertEqual(resolvedSwiftMember.languages, [.swift])
2885+
2886+
let resolvedObjcMemberID = try tree.find(path: "/ModuleName/ContainerName-union/MemberName", onlyFindSymbols: true)
2887+
let resolvedObjcMember = try XCTUnwrap(tree.lookup[resolvedObjcMemberID])
2888+
XCTAssertEqual(resolvedObjcMember.name, "MemberName")
2889+
XCTAssertEqual(resolvedObjcMember.parent?.identifier, resolvedObjcContainerID)
2890+
XCTAssertEqual(resolvedObjcMember.symbol?.identifier.precise, memberID)
2891+
XCTAssertEqual(resolvedObjcMember.symbol?.kind.identifier, .property)
2892+
XCTAssertEqual(resolvedObjcMember.languages, [.objectiveC])
2893+
2894+
let resolvedMemberID = try tree.find(path: "/ModuleName/ContainerName/MemberName", onlyFindSymbols: true)
2895+
XCTAssertEqual(resolvedMemberID, resolvedSwiftMemberID)
2896+
2897+
let paths = tree.caseInsensitiveDisambiguatedPaths()
2898+
XCTAssertEqual(paths[containerID], "/ModuleName/ContainerName")
2899+
XCTAssertEqual(paths[memberID], "/ModuleName/ContainerName/MemberName")
2900+
}
2901+
28242902
func testMixedLanguageSymbolAndItsExtendingModuleWithDifferentContainerNames() throws {
28252903
let containerID = "some-container-symbol-id"
28262904
let memberID = "some-member-symbol-id"

0 commit comments

Comments
 (0)