Skip to content

Commit 626a05f

Browse files
authored
Fix a crash when a protocol with a default implementation is aliased with the same name (#534) (#536)
* Fix crash when exported import protocol is aliased with same name rdar://107623749 * Remove incorrect memberOf relationships in handcrafted test symbol data
1 parent fb643e3 commit 626a05f

File tree

5 files changed

+453
-12
lines changed

5 files changed

+453
-12
lines changed

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

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,26 @@ struct PathHierarchy {
135135
}
136136
}
137137

138+
for relationship in graph.relationships where relationship.kind == .defaultImplementationOf {
139+
guard let sourceNode = nodes[relationship.source] else {
140+
continue
141+
}
142+
143+
let targetNodes = nodes[relationship.target].map { [$0] } ?? allNodes[relationship.target] ?? []
144+
guard !targetNodes.isEmpty else {
145+
continue
146+
}
147+
148+
for requirementTarget in targetNodes {
149+
assert(
150+
requirementTarget.parent != nil,
151+
"The 'defaultImplementationOf' symbol should be a 'memberOf' a known protocol symbol but didn't have a parent relationship in the hierarchy."
152+
)
153+
requirementTarget.parent?.add(symbolChild: sourceNode)
154+
}
155+
topLevelCandidates.removeValue(forKey: relationship.source)
156+
}
157+
138158
// The hierarchy doesn't contain any non-symbol nodes yet. It's OK to unwrap the `symbol` property.
139159
for topLevelNode in topLevelCandidates.values where topLevelNode.symbol!.pathComponents.count == 1 {
140160
moduleNode.add(symbolChild: topLevelNode)
@@ -155,6 +175,10 @@ struct PathHierarchy {
155175
components = components.dropFirst()
156176
}
157177
for component in components {
178+
assert(
179+
parent.children[components.first!] == nil,
180+
"Shouldn't create a new sparse node when symbol node already exist. This is an indication that a symbol is missing a relationship."
181+
)
158182
let component = Self.parse(pathComponent: component[...])
159183
let nodeWithoutSymbol = Node(name: component.name)
160184
parent.add(child: nodeWithoutSymbol, kind: component.kind, hash: component.hash)
@@ -164,6 +188,10 @@ struct PathHierarchy {
164188
}
165189
}
166190

191+
assert(
192+
allNodes.allSatisfy({ $0.value[0].parent != nil || roots[$0.key] != nil }),
193+
"Every node should either have a parent node or be a root node. This wasn't true for \(allNodes.filter({ $0.value[0].parent != nil || roots[$0.key] != nil }).map(\.key).sorted())"
194+
)
167195
allNodes.removeAll()
168196

169197
// build the lookup list by traversing the hierarchy and adding identifiers to each node
@@ -199,12 +227,15 @@ struct PathHierarchy {
199227
self.tutorialContainer = newNode(bundleName)
200228
self.tutorialOverviewContainer = newNode("tutorials")
201229

202-
assert(lookup.allSatisfy({ $0.key == $0.value.identifier}))
230+
assert(
231+
lookup.allSatisfy({ $0.key == $0.value.identifier }),
232+
"Every node lookup should match a node with that identifier."
233+
)
203234

204235
self.modules = roots
205236
self.lookup = lookup
206237

207-
assert(topLevelSymbols().allSatisfy({ lookup[$0] != nil}))
238+
assert(topLevelSymbols().allSatisfy({ lookup[$0] != nil }))
208239
}
209240

210241
/// Adds an article to the path hierarchy.

Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,39 @@ class PathHierarchyTests: XCTestCase {
674674
try assertFindsPath("/MixedFramework-module-9r7pl/myTopLevelVariable-var-520ez", in: tree, asSymbolID: "s:14MixedFramework18myTopLevelVariableSbvp")
675675
}
676676

677+
func testDefaultImplementationWithCollidingTargetSymbol() throws {
678+
try XCTSkipUnless(LinkResolutionMigrationConfiguration.shouldUseHierarchyBasedLinkResolver)
679+
680+
// ---- Inner
681+
// public protocol Something {
682+
// func doSomething()
683+
// }
684+
// public extension Something {
685+
// func doSomething() {}
686+
// }
687+
//
688+
// ---- Outer
689+
// @_exported import Inner
690+
// public typealias Something = Inner.Something
691+
let (_, context) = try testBundleAndContext(named: "DefaultImplementationsWithExportedImport")
692+
let tree = try XCTUnwrap(context.hierarchyBasedLinkResolver?.pathHierarchy)
693+
694+
// The @_export imported protocol can be found
695+
try assertFindsPath("/DefaultImplementationsWithExportedImport/Something-protocol", in: tree, asSymbolID: "s:5Inner9SomethingP")
696+
// The wrapping type alias can be found
697+
try assertFindsPath("/DefaultImplementationsWithExportedImport/Something-typealias", in: tree, asSymbolID: "s:40DefaultImplementationsWithExportedImport9Somethinga")
698+
699+
// The protocol requirement and the default implementation both exist at the @_export imported Something protocol.
700+
try assertPathRaisesErrorMessage("DefaultImplementationsWithExportedImport/Something-protocol/doSomething()", in: tree, context: context, expectedErrorMessage: """
701+
'doSomething()' is ambiguous at '/DefaultImplementationsWithExportedImport/Something'
702+
""") { error in
703+
XCTAssertEqual(error.solutions, [
704+
.init(summary: "Insert '8skxc' for\n'func doSomething()'", replacements: [("-8skxc", 73, 73)]),
705+
.init(summary: "Insert 'scj9' for\n'func doSomething()'", replacements: [("-scj9", 73, 73)]),
706+
])
707+
}
708+
}
709+
677710
func testDisambiguatedPaths() throws {
678711
try XCTSkipUnless(LinkResolutionMigrationConfiguration.shouldUseHierarchyBasedLinkResolver)
679712
let (_, context) = try testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements")

0 commit comments

Comments
 (0)