Skip to content

Commit

Permalink
Workaround issues with empty titles for some unnamed C symbols (#1100) (
Browse files Browse the repository at this point in the history
#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
  • Loading branch information
d-ronnqvist authored Nov 20, 2024
1 parent 3cfeeb6 commit b86e2bf
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ struct SymbolGraphLoader {
symbolGraph = try SymbolGraphConcurrentDecoder.decode(data)
}

Self.applyWorkaroundFor139305015(to: &symbolGraph)

symbolGraphTransformer?(&symbolGraph)

let (moduleName, isMainSymbolGraph) = Self.moduleNameFor(symbolGraph, at: symbolGraphURL)
Expand Down Expand Up @@ -362,6 +364,62 @@ struct SymbolGraphLoader {
}
return (moduleName, isMainSymbolGraph)
}

private static func applyWorkaroundFor139305015(to symbolGraph: inout SymbolGraph) {
guard symbolGraph.symbols.values.mapFirst(where: { SourceLanguage(id: $0.identifier.interfaceLanguage) }) == .objectiveC else {
return
}

// Clang emits anonymous structs and unions differently than anonymous enums (rdar://139305015).
//
// The anonymous structs, with empty names, causes issues in a few different places for DocC:
// - The IndexingRecords (one of the `--emit-digest` files) throws an error about the empty name.
// - The NavigatorIndex.Builder may throw an error about the empty name.
// - Their pages can't be navigated to because their URL path end with a leading slash.
// The corresponding static hosting 'index.html' copy also overrides the container's index.html file because
// its file path has two slashes, for example "/documentation/ModuleName/ContainerName//index.html".
//
// To avoid all those issues without handling empty names throughout the code,
// we fill in titles and navigator titles for these symbols using the same format as Clang uses for anonymous enums.

let relationshipsByTarget = [String: [SymbolGraph.Relationship]](grouping: symbolGraph.relationships, by: \.target)

for (usr, symbol) in symbolGraph.symbols {
guard symbol.names.title.isEmpty,
symbol.names.navigator?.map(\.spelling).joined().isEmpty == true,
symbol.pathComponents.last?.isEmpty == true
else {
continue
}

// This symbol has an empty title and an empty navigator title.
var modified = symbol
let fallbackTitle = "\(symbol.kind.identifier.identifier) (unnamed)"
modified.names.title = fallbackTitle
// Clang uses a single `identifier` fragment for anonymous enums.
modified.names.navigator = [.init(kind: .identifier, spelling: fallbackTitle, preciseIdentifier: nil)]
// 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.

// Clang uses the "enum (unnamed)" in the path components of anonymous enums so we follow that format for anonymous structs.
modified.pathComponents[modified.pathComponents.count - 1] = fallbackTitle
symbolGraph.symbols[usr] = modified

// Also update all the members whose path components start with the container's path components so that they're consistent.
if let relationships = relationshipsByTarget[usr] {
let containerPathComponents = modified.pathComponents

for memberRelationship in relationships where memberRelationship.kind == .memberOf {
guard var modifiedMember = symbolGraph.symbols.removeValue(forKey: memberRelationship.source) else { continue }
// Only update the member's path components if it starts with the original container's components.
guard modifiedMember.pathComponents.starts(with: symbol.pathComponents) else { continue }

modifiedMember.pathComponents.replaceSubrange(containerPathComponents.indices, with: containerPathComponents)

symbolGraph.symbols[memberRelationship.source] = modifiedMember
}
}
}
}
}

extension SymbolGraph.SemanticVersion {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftDocCTestUtilities/SymbolGraphCreation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ extension XCTestCase {

return SymbolGraph.Symbol(
identifier: SymbolGraph.Symbol.Identifier(precise: id, interfaceLanguage: language.id),
names: makeSymbolNames(name: pathComponents.first!),
names: makeSymbolNames(name: pathComponents.last!),
pathComponents: pathComponents,
docComment: docComment.map {
makeLineList(
Expand Down
41 changes: 41 additions & 0 deletions Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3605,4 +3605,45 @@ Document

XCTAssertEqual(expectedContent, renderContent)
}

func testSymbolWithEmptyName() throws {
// Symbols _should_ have names, but due to bugs there's cases when anonymous C structs don't.
let catalog = Folder(name: "unit-test.docc", content: [
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
moduleName: "ModuleName",
symbols: [
makeSymbol(id: "some-container", language: .objectiveC, kind: .class, pathComponents: ["SomeContainer"]),
makeSymbol(id: "some-unnamed-struct", language: .objectiveC, kind: .struct, pathComponents: ["SomeContainer", ""]),
makeSymbol(id: "some-inner-member", language: .objectiveC, kind: .var, pathComponents: ["SomeContainer", "", "someMember"]),

makeSymbol(id: "some-named-struct", language: .objectiveC, kind: .struct, pathComponents: ["SomeContainer", "NamedInnerContainer"]),
],
relationships: [
.init(source: "some-unnamed-struct", target: "some-container", kind: .memberOf, targetFallback: nil),
.init(source: "some-inner-member", target: "some-unnamed-struct", kind: .memberOf, targetFallback: nil),

.init(source: "some-named-struct", target: "some-container", kind: .memberOf, targetFallback: nil),
]
))
])

let (bundle, context) = try loadBundle(catalog: catalog)

XCTAssertEqual(context.knownPages.map(\.path).sorted(), [
"/documentation/ModuleName",
"/documentation/ModuleName/SomeContainer",
"/documentation/ModuleName/SomeContainer/NamedInnerContainer",
"/documentation/ModuleName/SomeContainer/struct_(unnamed)",
"/documentation/ModuleName/SomeContainer/struct_(unnamed)/someMember"
], "The reference paths shouldn't have any empty components")

let unnamedStructReference = try XCTUnwrap(context.soleRootModuleReference).appendingPath("SomeContainer/struct_(unnamed)")
let node = try context.entity(with: unnamedStructReference)

let converter = DocumentationNodeConverter(bundle: bundle, context: context)
let renderNode = try converter.convert(node)

XCTAssertEqual(renderNode.metadata.title, "struct (unnamed)")
XCTAssertEqual(renderNode.metadata.navigatorTitle?.map(\.text).joined(), "struct (unnamed)")
}
}

0 comments on commit b86e2bf

Please sign in to comment.