Skip to content

Commit b86e2bf

Browse files
authored
Workaround issues with empty titles for some unnamed C symbols (#1100) (#1101)
* Workaround issues with empty titles for some unnamed C symbols rdar://139305015 * Extract workaround to private function * Add another named inner container to new test data
1 parent 3cfeeb6 commit b86e2bf

File tree

3 files changed

+100
-1
lines changed

3 files changed

+100
-1
lines changed

Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ struct SymbolGraphLoader {
7676
symbolGraph = try SymbolGraphConcurrentDecoder.decode(data)
7777
}
7878

79+
Self.applyWorkaroundFor139305015(to: &symbolGraph)
80+
7981
symbolGraphTransformer?(&symbolGraph)
8082

8183
let (moduleName, isMainSymbolGraph) = Self.moduleNameFor(symbolGraph, at: symbolGraphURL)
@@ -362,6 +364,62 @@ struct SymbolGraphLoader {
362364
}
363365
return (moduleName, isMainSymbolGraph)
364366
}
367+
368+
private static func applyWorkaroundFor139305015(to symbolGraph: inout SymbolGraph) {
369+
guard symbolGraph.symbols.values.mapFirst(where: { SourceLanguage(id: $0.identifier.interfaceLanguage) }) == .objectiveC else {
370+
return
371+
}
372+
373+
// Clang emits anonymous structs and unions differently than anonymous enums (rdar://139305015).
374+
//
375+
// The anonymous structs, with empty names, causes issues in a few different places for DocC:
376+
// - The IndexingRecords (one of the `--emit-digest` files) throws an error about the empty name.
377+
// - The NavigatorIndex.Builder may throw an error about the empty name.
378+
// - Their pages can't be navigated to because their URL path end with a leading slash.
379+
// The corresponding static hosting 'index.html' copy also overrides the container's index.html file because
380+
// its file path has two slashes, for example "/documentation/ModuleName/ContainerName//index.html".
381+
//
382+
// To avoid all those issues without handling empty names throughout the code,
383+
// we fill in titles and navigator titles for these symbols using the same format as Clang uses for anonymous enums.
384+
385+
let relationshipsByTarget = [String: [SymbolGraph.Relationship]](grouping: symbolGraph.relationships, by: \.target)
386+
387+
for (usr, symbol) in symbolGraph.symbols {
388+
guard symbol.names.title.isEmpty,
389+
symbol.names.navigator?.map(\.spelling).joined().isEmpty == true,
390+
symbol.pathComponents.last?.isEmpty == true
391+
else {
392+
continue
393+
}
394+
395+
// This symbol has an empty title and an empty navigator title.
396+
var modified = symbol
397+
let fallbackTitle = "\(symbol.kind.identifier.identifier) (unnamed)"
398+
modified.names.title = fallbackTitle
399+
// Clang uses a single `identifier` fragment for anonymous enums.
400+
modified.names.navigator = [.init(kind: .identifier, spelling: fallbackTitle, preciseIdentifier: nil)]
401+
// Don't update `modified.names.subHeading`. Clang _doesn't_ use "enum (unnamed)" for the `Symbol/Names/subHeading` so we don't add it here either.
402+
403+
// Clang uses the "enum (unnamed)" in the path components of anonymous enums so we follow that format for anonymous structs.
404+
modified.pathComponents[modified.pathComponents.count - 1] = fallbackTitle
405+
symbolGraph.symbols[usr] = modified
406+
407+
// Also update all the members whose path components start with the container's path components so that they're consistent.
408+
if let relationships = relationshipsByTarget[usr] {
409+
let containerPathComponents = modified.pathComponents
410+
411+
for memberRelationship in relationships where memberRelationship.kind == .memberOf {
412+
guard var modifiedMember = symbolGraph.symbols.removeValue(forKey: memberRelationship.source) else { continue }
413+
// Only update the member's path components if it starts with the original container's components.
414+
guard modifiedMember.pathComponents.starts(with: symbol.pathComponents) else { continue }
415+
416+
modifiedMember.pathComponents.replaceSubrange(containerPathComponents.indices, with: containerPathComponents)
417+
418+
symbolGraph.symbols[memberRelationship.source] = modifiedMember
419+
}
420+
}
421+
}
422+
}
365423
}
366424

367425
extension SymbolGraph.SemanticVersion {

Sources/SwiftDocCTestUtilities/SymbolGraphCreation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ extension XCTestCase {
104104

105105
return SymbolGraph.Symbol(
106106
identifier: SymbolGraph.Symbol.Identifier(precise: id, interfaceLanguage: language.id),
107-
names: makeSymbolNames(name: pathComponents.first!),
107+
names: makeSymbolNames(name: pathComponents.last!),
108108
pathComponents: pathComponents,
109109
docComment: docComment.map {
110110
makeLineList(

Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3605,4 +3605,45 @@ Document
36053605

36063606
XCTAssertEqual(expectedContent, renderContent)
36073607
}
3608+
3609+
func testSymbolWithEmptyName() throws {
3610+
// Symbols _should_ have names, but due to bugs there's cases when anonymous C structs don't.
3611+
let catalog = Folder(name: "unit-test.docc", content: [
3612+
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
3613+
moduleName: "ModuleName",
3614+
symbols: [
3615+
makeSymbol(id: "some-container", language: .objectiveC, kind: .class, pathComponents: ["SomeContainer"]),
3616+
makeSymbol(id: "some-unnamed-struct", language: .objectiveC, kind: .struct, pathComponents: ["SomeContainer", ""]),
3617+
makeSymbol(id: "some-inner-member", language: .objectiveC, kind: .var, pathComponents: ["SomeContainer", "", "someMember"]),
3618+
3619+
makeSymbol(id: "some-named-struct", language: .objectiveC, kind: .struct, pathComponents: ["SomeContainer", "NamedInnerContainer"]),
3620+
],
3621+
relationships: [
3622+
.init(source: "some-unnamed-struct", target: "some-container", kind: .memberOf, targetFallback: nil),
3623+
.init(source: "some-inner-member", target: "some-unnamed-struct", kind: .memberOf, targetFallback: nil),
3624+
3625+
.init(source: "some-named-struct", target: "some-container", kind: .memberOf, targetFallback: nil),
3626+
]
3627+
))
3628+
])
3629+
3630+
let (bundle, context) = try loadBundle(catalog: catalog)
3631+
3632+
XCTAssertEqual(context.knownPages.map(\.path).sorted(), [
3633+
"/documentation/ModuleName",
3634+
"/documentation/ModuleName/SomeContainer",
3635+
"/documentation/ModuleName/SomeContainer/NamedInnerContainer",
3636+
"/documentation/ModuleName/SomeContainer/struct_(unnamed)",
3637+
"/documentation/ModuleName/SomeContainer/struct_(unnamed)/someMember"
3638+
], "The reference paths shouldn't have any empty components")
3639+
3640+
let unnamedStructReference = try XCTUnwrap(context.soleRootModuleReference).appendingPath("SomeContainer/struct_(unnamed)")
3641+
let node = try context.entity(with: unnamedStructReference)
3642+
3643+
let converter = DocumentationNodeConverter(bundle: bundle, context: context)
3644+
let renderNode = try converter.convert(node)
3645+
3646+
XCTAssertEqual(renderNode.metadata.title, "struct (unnamed)")
3647+
XCTAssertEqual(renderNode.metadata.navigatorTitle?.map(\.text).joined(), "struct (unnamed)")
3648+
}
36083649
}

0 commit comments

Comments
 (0)