Skip to content

Commit 2cdc04c

Browse files
committed
Try to repair symbol graphs with deep hierarchies but no actual memberOf relationships
1 parent 600630b commit 2cdc04c

File tree

2 files changed

+88
-4
lines changed

2 files changed

+88
-4
lines changed

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -299,12 +299,13 @@ struct PathHierarchy {
299299
// FIXME:
300300
// This code path is both expected (when `knownDisambiguatedPathComponents` is non-nil) and unexpected (when the symbol graph is missing data or contains extra relationships).
301301
// It would be good to restructure this code to better distinguish what's supported behavior and what's a best-effort attempt at gracefully handle invalid symbol graphs.
302-
if let existing = parent.children[components.first!] {
303-
//
302+
if let existing = parent.children[component] {
303+
// This code tries to repair incomplete symbol graph files by guessing that the symbol with the most overlapping languages is the intended container.
304+
// Valid symbol graph files we should never end up here.
304305
var bestLanguageMatch: (node: Node, count: Int)?
305306
for element in existing.storage {
306307
let numberOfMatchingLanguages = node.languages.intersection(element.node.languages).count
307-
if numberOfMatchingLanguages < (bestLanguageMatch?.count ?? .max) {
308+
if (bestLanguageMatch?.count ?? .min) < numberOfMatchingLanguages {
308309
bestLanguageMatch = (node: element.node, count: numberOfMatchingLanguages)
309310
}
310311
}
@@ -316,7 +317,7 @@ struct PathHierarchy {
316317
}
317318

318319
assert(
319-
parent.children[components.first!] == nil,
320+
parent.children[component] == nil,
320321
"Shouldn't create a new sparse node when symbol node already exist. This is an indication that a symbol is missing a relationship."
321322
)
322323

Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3200,6 +3200,89 @@ class PathHierarchyTests: XCTestCase {
32003200
try assertFindsPath("/ModuleName/ContainerName", in: tree, asSymbolID: containerID)
32013201
}
32023202

3203+
func testInvalidSymbolGraphWithNoMemberOfRelationshipsDesptiteDeepHierarchyAcrossManyPlatforms() throws {
3204+
// We make a best-effort attempt to create a valid path hierarchy, even if the symbol graph inputs are not valid.
3205+
3206+
// If the symbol graph files define a deep hierarchy, with the same symbol names but different symbol kinds across different, we try to match them up by language.
3207+
3208+
// Repeat the same symbols in both languages for many platforms.
3209+
let platforms = (1...10).map {
3210+
let name = "Platform\($0)"
3211+
return (name: name, availability: [makeAvailabilityItem(domainName: name)])
3212+
}
3213+
3214+
let catalog = Folder(name: "unit-test.docc", content: [
3215+
Folder(name: "clang", content: platforms.map { platform in
3216+
return JSONFile(name: "ModuleName-\(platform.name).symbols.json", content: makeSymbolGraph(
3217+
moduleName: "ModuleName",
3218+
symbols: [
3219+
makeSymbol(id: "some-outer-container-id", language: .objectiveC, kind: .class, pathComponents: ["OuterContainerName"]),
3220+
makeSymbol(id: "some-middle-container-id", language: .objectiveC, kind: .class, pathComponents: ["OuterContainerName", "MiddleContainerName"]),
3221+
makeSymbol(id: "some-inner-container-id", language: .objectiveC, kind: .class, pathComponents: ["OuterContainerName", "MiddleContainerName", "InnerContainerName"]),
3222+
makeSymbol(id: "some-objc-specific-member-id", language: .objectiveC, kind: .property, pathComponents: ["OuterContainerName", "MiddleContainerName", "InnerContainerName", "objcSpecificMember"]),
3223+
],
3224+
relationships: [/* all required memberOf relationships all missing */]
3225+
))
3226+
}),
3227+
3228+
Folder(name: "swift", content: platforms.map { platform in
3229+
return JSONFile(name: "ModuleName-\(platform.name).symbols.json", content: makeSymbolGraph(
3230+
moduleName: "ModuleName",
3231+
symbols: [
3232+
makeSymbol(id: "some-outer-container-id", kind: .struct, pathComponents: ["OuterContainerName"]),
3233+
makeSymbol(id: "some-middle-container-id", kind: .struct, pathComponents: ["OuterContainerName", "MiddleContainerName"]),
3234+
makeSymbol(id: "some-inner-container-id", kind: .struct, pathComponents: ["OuterContainerName", "MiddleContainerName", "InnerContainerName"]),
3235+
makeSymbol(id: "some-swift-specific-member-id", kind: .method, pathComponents: ["OuterContainerName", "MiddleContainerName", "InnerContainerName", "swiftSpecificMember()"]),
3236+
],
3237+
relationships: [/* all required memberOf relationships all missing */]
3238+
))
3239+
})
3240+
])
3241+
3242+
let (_, context) = try loadBundle(catalog: catalog)
3243+
let tree = context.linkResolver.localResolver.pathHierarchy
3244+
3245+
let swiftSpecificNode = try tree.findNode(path: "/ModuleName/OuterContainerName-struct/MiddleContainerName-struct/InnerContainerName-struct/swiftSpecificMember()", onlyFindSymbols: true, parent: nil)
3246+
XCTAssertEqual(swiftSpecificNode.symbol?.identifier.precise, "some-swift-specific-member-id")
3247+
// Trace up and check that each node is represented by a symbol
3248+
XCTAssertEqual(swiftSpecificNode.parent?.symbol?.identifier.precise, "some-inner-container-id")
3249+
XCTAssertEqual(swiftSpecificNode.parent?.parent?.symbol?.identifier.precise, "some-middle-container-id")
3250+
XCTAssertEqual(swiftSpecificNode.parent?.parent?.parent?.symbol?.identifier.precise, "some-outer-container-id")
3251+
3252+
let objcSpecificNode = try tree.findNode(path: "/ModuleName/OuterContainerName-class/MiddleContainerName-class/InnerContainerName-class/objcSpecificMember", onlyFindSymbols: true, parent: nil)
3253+
XCTAssertEqual(objcSpecificNode.symbol?.identifier.precise, "some-objc-specific-member-id")
3254+
// Trace up and check that each node is represented by a symbol
3255+
XCTAssertEqual(objcSpecificNode.parent?.symbol?.identifier.precise, "some-inner-container-id")
3256+
XCTAssertEqual(objcSpecificNode.parent?.parent?.symbol?.identifier.precise, "some-middle-container-id")
3257+
XCTAssertEqual(objcSpecificNode.parent?.parent?.parent?.symbol?.identifier.precise, "some-outer-container-id")
3258+
3259+
// Check that each language has different nodes
3260+
XCTAssertNotEqual(swiftSpecificNode.parent?.identifier, objcSpecificNode.parent?.identifier)
3261+
XCTAssertNotEqual(swiftSpecificNode.parent?.parent?.identifier, objcSpecificNode.parent?.parent?.identifier)
3262+
XCTAssertNotEqual(swiftSpecificNode.parent?.parent?.parent?.identifier, objcSpecificNode.parent?.parent?.parent?.identifier)
3263+
3264+
// Check that neither path require disambiguation
3265+
let paths = tree.caseInsensitiveDisambiguatedPaths()
3266+
3267+
XCTAssertEqual(paths["some-outer-container-id"], "/ModuleName/OuterContainerName")
3268+
XCTAssertEqual(paths["some-middle-container-id"], "/ModuleName/OuterContainerName/MiddleContainerName")
3269+
XCTAssertEqual(paths["some-inner-container-id"], "/ModuleName/OuterContainerName/MiddleContainerName/InnerContainerName")
3270+
XCTAssertEqual(paths["some-swift-specific-member-id"], "/ModuleName/OuterContainerName/MiddleContainerName/InnerContainerName/swiftSpecificMember()")
3271+
XCTAssertEqual(paths["some-objc-specific-member-id"], "/ModuleName/OuterContainerName/MiddleContainerName/InnerContainerName/objcSpecificMember")
3272+
3273+
// Check that the hierarchy doesn't contain any sparse nodes
3274+
var remaining = tree.modules[...]
3275+
XCTAssertFalse(remaining.isEmpty)
3276+
3277+
while let node = remaining.popFirst() {
3278+
XCTAssertNotNil(node.symbol, "Unexpected sparse node named '\(node.name)' in hierarchy")
3279+
3280+
for container in node.children.values {
3281+
remaining.append(contentsOf: container.storage.map(\.node))
3282+
}
3283+
}
3284+
}
3285+
32033286
func testMissingReferencedContainerSymbolOnSomePlatforms() throws {
32043287
// We make a best-effort attempt to create a valid path hierarchy, even if the symbol graph inputs are not valid.
32053288

0 commit comments

Comments
 (0)