Skip to content

Commit 70a2fe3

Browse files
use SymbolKit's overload groups implementation to group overloads (#846)
rdar://116550528 Co-authored-by: David Rönnqvist <[email protected]>
1 parent 73e7adc commit 70a2fe3

File tree

11 files changed

+179
-93
lines changed

11 files changed

+179
-93
lines changed

Package.resolved

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,6 +1156,8 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
11561156

11571157
// For inherited symbols we remove the source docs (if inheriting docs is disabled) before creating their documentation nodes.
11581158
for (_, relationships) in unifiedSymbolGraph.relationshipsByLanguage {
1159+
var overloadGroups = [String: [String]]()
1160+
11591161
for relationship in relationships {
11601162
// Check for an origin key.
11611163
if let sourceOrigin = relationship[mixin: SymbolGraph.Relationship.SourceOrigin.self],
@@ -1169,8 +1171,13 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
11691171
localCache: documentationCache,
11701172
moduleName: moduleName
11711173
)
1174+
} else if relationship.kind == .overloadOf {
1175+
// An 'overloadOf' relationship points from symbol -> group
1176+
overloadGroups[relationship.target, default: []].append(relationship.source)
11721177
}
11731178
}
1179+
1180+
addOverloadGroupReferences(overloadGroups: overloadGroups)
11741181
}
11751182

11761183
if let rootURL = symbolGraphLoader.mainModuleURL(forModule: moduleName), let rootModule = unifiedSymbolGraph.moduleData[rootURL] {
@@ -1285,8 +1292,6 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
12851292
}
12861293
emitWarningsForSymbolsMatchedInMultipleDocumentationExtensions(with: symbolsWithMultipleDocumentationExtensionMatches)
12871294
symbolsWithMultipleDocumentationExtensionMatches.removeAll()
1288-
1289-
try groupOverloadedSymbols(with: linkResolver.localResolver)
12901295

12911296
// Create inherited API collections
12921297
try GeneratedDocumentationTopics.createInheritedSymbolsAPICollections(
@@ -2330,34 +2335,78 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
23302335
return automaticallyCuratedSymbols
23312336
}
23322337

2333-
/// Handles overloaded symbols by grouping them together into one page.
2334-
private func groupOverloadedSymbols(with linkResolver: PathHierarchyBasedLinkResolver) throws {
2338+
private func addOverloadGroupReferences(overloadGroups: [String: [String]]) {
23352339
guard FeatureFlags.current.isExperimentalOverloadedSymbolPresentationEnabled else {
23362340
return
23372341
}
23382342

2339-
try linkResolver.traverseOverloadedSymbols { overloadedSymbolReferences in
2343+
for (overloadGroupID, overloadSymbolIDs) in overloadGroups {
2344+
guard overloadSymbolIDs.count > 1 else {
2345+
assertionFailure("Overload group \(overloadGroupID) contained \(overloadSymbolIDs.count) symbols, but should have more than one symbol to be valid.")
2346+
continue
2347+
}
2348+
guard let overloadGroupNode = documentationCache[overloadGroupID] else {
2349+
preconditionFailure("Overload group \(overloadGroupID) doesn't have a local entity")
2350+
}
23402351

2341-
// Tell each symbol what other symbols overload it.
2342-
for (index, symbolReference) in overloadedSymbolReferences.indexed() {
2343-
let documentationNode = try entity(with: symbolReference)
2352+
var overloadSymbolNodes = overloadSymbolIDs.map {
2353+
guard let node = documentationCache[$0] else {
2354+
preconditionFailure("Overloaded symbol \($0) doesn't have a local entity")
2355+
}
2356+
return node
2357+
}
2358+
if overloadSymbolNodes.allSatisfy({ node in
2359+
(node.semantic as? Symbol)?.overloadsVariants.firstValue != nil
2360+
}) {
2361+
// If SymbolKit saved its sort of the symbols, use that ordering here
2362+
overloadSymbolNodes.sort(by: { lhs, rhs in
2363+
let lhsIndex = (lhs.semantic as! Symbol).overloadsVariants.firstValue!.displayIndex
2364+
let rhsIndex = (rhs.semantic as! Symbol).overloadsVariants.firstValue!.displayIndex
2365+
return lhsIndex < rhsIndex
2366+
})
2367+
} else {
2368+
assertionFailure("""
2369+
Overload group \(overloadGroupNode.reference.absoluteString.singleQuoted) was not properly initialized with overload data from SymbolKit.
2370+
Symbols without overload data: \(Array(overloadSymbolNodes.filter({ ($0.semantic as? Symbol)?.overloadsVariants.firstValue == nil }).map(\.reference.absoluteString.singleQuoted)))
2371+
""")
2372+
return
2373+
}
23442374

2375+
func addOverloadReferences(
2376+
to documentationNode: DocumentationNode,
2377+
at index: Int,
2378+
overloadSymbolReferences: [DocumentationNode]
2379+
) {
23452380
guard let symbol = documentationNode.semantic as? Symbol else {
23462381
preconditionFailure("""
2347-
Only symbols can be overloads. Found non-symbol overload for \(symbolReference.absoluteString.singleQuoted).
2348-
Non-symbols should already have been filtered out in `PathHierarchyBasedLinkResolver.traverseOverloadedSymbols(_:)`.
2382+
Only symbols can be overloads. Found non-symbol overload for \(documentationNode.reference.absoluteString.singleQuoted).
2383+
Non-symbols should already have been filtered out by SymbolKit.
23492384
""")
23502385
}
2351-
guard symbolReference.sourceLanguage == .swift else {
2352-
assertionFailure("Overload groups is only supported for Swift symbols.")
2353-
continue
2386+
guard documentationNode.reference.sourceLanguage == .swift else {
2387+
assertionFailure("""
2388+
Overload groups are only supported for Swift symbols.
2389+
The symbol at \(documentationNode.reference.absoluteString.singleQuoted) is listed as \(documentationNode.reference.sourceLanguage.name).
2390+
""")
2391+
return
23542392
}
23552393

2356-
var otherOverloadedSymbolReferences = overloadedSymbolReferences
2394+
var otherOverloadedSymbolReferences = overloadSymbolReferences.map(\.reference)
23572395
otherOverloadedSymbolReferences.remove(at: index)
2358-
let overloads = Symbol.Overloads(references: otherOverloadedSymbolReferences, displayIndex: index)
2396+
let displayIndex = symbol.overloadsVariants.firstValue?.displayIndex ?? index
2397+
let overloads = Symbol.Overloads(references: otherOverloadedSymbolReferences, displayIndex: displayIndex)
23592398
symbol.overloadsVariants = .init(swiftVariant: overloads)
23602399
}
2400+
2401+
// The overload group node itself is a clone of the first symbol, so the code above can
2402+
// swap out the first element in the overload references to create the alternate
2403+
// declarations section properly. However, it is also a distinct symbol, node, and page,
2404+
// so the first overload itself should also be handled separately in the loop below.
2405+
addOverloadReferences(to: overloadGroupNode, at: 0, overloadSymbolReferences: overloadSymbolNodes)
2406+
2407+
for (index, node) in overloadSymbolNodes.indexed() {
2408+
addOverloadReferences(to: node, at: index, overloadSymbolReferences: overloadSymbolNodes)
2409+
}
23612410
}
23622411
}
23632412

Sources/SwiftDocC/Infrastructure/Extensions/KindIdentifier+Overloads.swift

Lines changed: 0 additions & 31 deletions
This file was deleted.

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

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,14 @@ struct PathHierarchy {
136136
allNodes[id, default: []].append(node)
137137
}
138138
}
139-
139+
140+
for relationship in graph.relationships where relationship.kind == .overloadOf {
141+
// An 'overloadOf' relationship points from symbol -> group. We want to disfavor the
142+
// individual overload symbols in favor of resolving links to their overload group
143+
// symbol.
144+
nodes[relationship.source]?.specialBehaviors.insert(.disfavorInLinkCollision)
145+
}
146+
140147
// If there are multiple symbol graphs (for example for different source languages or platforms) then the nodes may have already been added to the hierarchy.
141148
var topLevelCandidates = nodes.filter { _, node in node.parent == nil }
142149
for relationship in graph.relationships where relationship.kind.formsHierarchy {
@@ -192,7 +199,7 @@ struct PathHierarchy {
192199
}
193200
topLevelCandidates.removeValue(forKey: relationship.source)
194201
}
195-
202+
196203
// The hierarchy doesn't contain any non-symbol nodes yet. It's OK to unwrap the `symbol` property.
197204
for topLevelNode in topLevelCandidates.values where topLevelNode.symbol!.pathComponents.count == 1 {
198205
moduleNode.add(symbolChild: topLevelNode)
@@ -520,23 +527,6 @@ extension PathHierarchy {
520527
}
521528
return Array(result) + modules.map { $0.identifier }
522529
}
523-
524-
func traverseOverloadedSymbolGroups(observe: (_ overloadedSymbols: [ResolvedIdentifier]) throws -> Void) rethrows {
525-
for node in lookup.values where node.symbol != nil {
526-
for disambiguation in node.children.values {
527-
var overloadGroups = [SymbolGraph.Symbol.KindIdentifier: [ResolvedIdentifier]]()
528-
for element in disambiguation.storage {
529-
guard let kind = element.node.symbol?.kind.identifier, kind.isOverloadableKind else {
530-
continue
531-
}
532-
overloadGroups[kind, default: []].append(element.node.identifier)
533-
}
534-
for overloads in overloadGroups.values where overloads.count > 1 {
535-
try observe(overloads)
536-
}
537-
}
538-
}
539-
}
540530
}
541531

542532
// MARK: Removing nodes

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ final class PathHierarchyBasedLinkResolver {
5656
observe(reference, parentReference)
5757
}
5858
}
59-
59+
6060
/// Returns the direct descendants of the given page that match the given source language filter.
6161
///
6262
/// A descendant is included if it has a language representation in at least one of the languages in the given language filter or if the language filter is empty.
@@ -92,14 +92,7 @@ final class PathHierarchyBasedLinkResolver {
9292
}
9393
return results
9494
}
95-
96-
/// Traverse all symbols of the same kind that have collisions.
97-
func traverseOverloadedSymbols(_ observe: (_ overloadedSymbols: [ResolvedTopicReference]) throws -> Void) rethrows {
98-
try pathHierarchy.traverseOverloadedSymbolGroups() { overloadedSymbols in
99-
try observe(overloadedSymbols.map { resolvedReferenceMap[$0]! })
100-
}
101-
}
102-
95+
10396
/// Returns a list of all the top level symbols.
10497
func topLevelSymbols() -> [ResolvedTopicReference] {
10598
return pathHierarchy.topLevelSymbols().map { resolvedReferenceMap[$0]! }

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ struct SymbolGraphLoader {
7878

7979
configureSymbolGraph?(&symbolGraph)
8080

81+
if FeatureFlags.current.isExperimentalOverloadedSymbolPresentationEnabled {
82+
symbolGraph.createOverloadGroupSymbols()
83+
}
84+
8185
let (moduleName, isMainSymbolGraph) = Self.moduleNameFor(symbolGraph, at: symbolGraphURL)
8286
// If the bundle provides availability defaults add symbol availability data.
8387
self.addDefaultAvailability(to: &symbolGraph, moduleName: moduleName)

Sources/SwiftDocC/Model/DocumentationNode.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,17 @@ public struct DocumentationNode {
223223
}
224224
return nil
225225
}
226-
226+
227+
let overloadVariants = DocumentationDataVariants(
228+
symbolData: unifiedSymbol.mixins,
229+
platformName: platformName
230+
) { mixins -> Symbol.Overloads? in
231+
guard let overloadData = mixins[SymbolGraph.Symbol.OverloadData.mixinKey] as? SymbolGraph.Symbol.OverloadData else {
232+
return nil
233+
}
234+
return .init(references: [], displayIndex: overloadData.overloadGroupIndex)
235+
}
236+
227237
var languages = Set([reference.sourceLanguage])
228238
var operatingSystemName = platformName.map({ Set([$0]) }) ?? []
229239

@@ -300,7 +310,8 @@ public struct DocumentationNode {
300310
httpParametersSectionVariants: .empty,
301311
httpResponsesSectionVariants: .empty,
302312
redirectsVariants: .empty,
303-
crossImportOverlayModule: moduleData.bystanders.map({ (moduleData.name, $0) })
313+
crossImportOverlayModule: moduleData.bystanders.map({ (moduleData.name, $0) }),
314+
overloadsVariants: overloadVariants
304315
)
305316

306317
try! semanticSymbol.mergeDeclarations(unifiedSymbol: unifiedSymbol)

Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3938,6 +3938,7 @@ let expected = """
39383938

39393939
let symbolReference = try XCTUnwrap(context.documentationCache.reference(symbolID: "s:12Minimal_docs4TestV"))
39403940

3941+
39413942
// Resolve from various locations in the bundle
39423943
for parent in [bundle.rootReference, bundle.documentationRootReference, bundle.tutorialsRootReference, symbolReference] {
39433944
switch context.resolve(unresolved, in: parent) {
@@ -4356,14 +4357,31 @@ let expected = """
43564357
))
43574358
])
43584359
])
4359-
let (_, _, context) = try loadBundle(from: tempURL)
4360-
4360+
let (_, bundle, context) = try loadBundle(from: tempURL)
4361+
let moduleReference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/ModuleName", sourceLanguage: .swift)
4362+
43614363
for kindID in overloadableKindIDs {
43624364
var seenIndices = Set<Int>()
4363-
// Find the 4 symbols of this specific kind
4364-
let overloadedReferences = try symbols.filter { $0.kind.identifier == kindID }
4365+
// Find the 4 symbols of this specific kind. SymbolKit will have assigned a display
4366+
// index based on their sorted USRs, so sort them ahead of time based on that
4367+
let overloadedReferences = try symbols.filter { $0.kind.identifier == kindID }.sorted(by: \.identifier.precise)
43654368
.map { try XCTUnwrap(context.documentationCache.reference(symbolID: $0.identifier.precise)) }
4366-
4369+
4370+
let overloadGroupNode: DocumentationNode
4371+
let overloadGroupSymbol: Symbol
4372+
let overloadGroupReferences: Symbol.Overloads
4373+
switch context.resolve(.unresolved(.init(topicURL: .init(symbolPath: "SymbolName-\(kindID.identifier)"))), in: moduleReference, fromSymbolLink: true) {
4374+
case let .failure(_, errorMessage):
4375+
XCTFail("Could not resolve overload group page for \(kindID.identifier). Error message: \(errorMessage)")
4376+
continue
4377+
case let .success(overloadGroupReference):
4378+
overloadGroupNode = try context.entity(with: overloadGroupReference)
4379+
overloadGroupSymbol = try XCTUnwrap(overloadGroupNode.semantic as? Symbol)
4380+
overloadGroupReferences = try XCTUnwrap(overloadGroupSymbol.overloadsVariants.firstValue)
4381+
4382+
XCTAssertEqual(overloadGroupReferences.displayIndex, 0)
4383+
}
4384+
43674385
// Check that each symbol lists the other 3 overloads
43684386
for (index, reference) in overloadedReferences.indexed() {
43694387
let overloadedDocumentationNode = try XCTUnwrap(context.documentationCache[reference])
@@ -4378,9 +4396,18 @@ let expected = """
43784396
}
43794397

43804398
// Each symbol needs to tell the renderer where it belongs in the array of overloaded declarations.
4381-
let displayIndex = try XCTUnwrap(overloads.displayIndex)
4382-
XCTAssertFalse(seenIndices.contains(displayIndex))
4383-
seenIndices.insert(displayIndex)
4399+
XCTAssertFalse(seenIndices.contains(overloads.displayIndex))
4400+
XCTAssertEqual(overloads.displayIndex, index)
4401+
seenIndices.insert(overloads.displayIndex)
4402+
4403+
if overloads.displayIndex == 0 {
4404+
// The first declaration in the display list should be the same declaration as
4405+
// the overload group page
4406+
XCTAssertEqual(overloadedSymbol.declaration.first?.value.declarationFragments, overloadGroupSymbol.declaration.first?.value.declarationFragments)
4407+
} else {
4408+
// Otherwise, this reference should also be referenced by the overload group
4409+
XCTAssert(overloadGroupReferences.references.contains(reference))
4410+
}
43844411
}
43854412
// Check that all the overloads was encountered
43864413
for index in overloadedReferences.indices {
@@ -4425,13 +4452,17 @@ let expected = """
44254452
}
44264453
}
44274454
}
4428-
4455+
44294456
// A test helper that creates a symbol with a given identifier and kind.
4430-
private func makeSymbol(identifier: String, kind: SymbolGraph.Symbol.KindIdentifier) -> SymbolGraph.Symbol {
4457+
private func makeSymbol(
4458+
name: String = "SymbolName",
4459+
identifier: String,
4460+
kind: SymbolGraph.Symbol.KindIdentifier
4461+
) -> SymbolGraph.Symbol {
44314462
return SymbolGraph.Symbol(
44324463
identifier: .init(precise: identifier, interfaceLanguage: SourceLanguage.swift.id),
4433-
names: .init(title: "SymbolName", navigator: nil, subHeading: nil, prose: nil),
4434-
pathComponents: ["SymbolName"],
4464+
names: .init(title: name, navigator: nil, subHeading: nil, prose: nil),
4465+
pathComponents: [name],
44354466
docComment: nil,
44364467
accessLevel: .public,
44374468
kind: .init(parsedIdentifier: kind, displayName: "Kind Display Name"),

0 commit comments

Comments
 (0)