Skip to content

Commit 04be889

Browse files
When documenting members of extensions to external protocols, display the external protocol's name (#732)
rdar://112219546 When documenting members of extensions to external protocols, display the external protocol's name. This is helpful because these pages don’t have the context of being curated inside their “owning” type or protocol. Expand the section that we use for conditional constraints to display the external protocol name, for example: "Self is ExternalProtocol". The new "Self is ExternalProtocol" message will be combined with any other existing generic constraints. Do not do this for extended structs, classes or other extended types, since this generic constraint syntax wouldn't be accurate for them. Also as a related bug fix remove the extendedModule property of the Semanic Symbol class, and instead calculate this based on swiftExtensionVariants. Same for constraintsVariants. Fix the initializer of Semantic Symbol, which previously allowed a nil default value for the extendedModule property, allowing callers to accidentally leave it nil.
1 parent da16aa9 commit 04be889

File tree

14 files changed

+448
-30
lines changed

14 files changed

+448
-30
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: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1411,7 +1411,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
14111411
}
14121412
}
14131413

1414-
1414+
14151415
/// Builds in-memory relationships between symbols based on the relationship information in a given symbol graph file.
14161416
///
14171417
/// - Parameters:
@@ -1427,8 +1427,22 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
14271427
bundle: DocumentationBundle,
14281428
engine: DiagnosticEngine
14291429
) {
1430+
1431+
// Find all of the relationships which refer to an extended module.
1432+
let extendedModuleRelationships = ExtendedTypeFormatTransformation.collapsedExtendedModuleRelationships(from: relationships)
1433+
14301434
for edge in relationships {
14311435
switch edge.kind {
1436+
case .memberOf, .optionalMemberOf:
1437+
// Add a "Self is" constraint for members of protocol extensions that
1438+
// extend a protocol from extended modules.
1439+
SymbolGraphRelationshipsBuilder.addProtocolExtensionMemberConstraint(
1440+
edge: edge,
1441+
selector: selector,
1442+
extendedModuleRelationships: extendedModuleRelationships,
1443+
symbolIndex: &symbolIndex,
1444+
documentationCache: documentationCache
1445+
)
14321446
case .conformsTo:
14331447
// Build conformant type <-> protocol relationships
14341448
SymbolGraphRelationshipsBuilder.addConformanceRelationship(

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,50 @@ extension ExtendedTypeFormatTransformation {
184184
return true
185185
}
186186

187+
// Collapse inContextOf relationships into their target declaredIn
188+
// relationships. See the diagram above. Return a dictionary of source
189+
// identifiers to the target identifiers.
190+
typealias SourceIdentifier = String
191+
typealias TargetIdentifier = String
192+
static func collapsedExtendedModuleRelationships(from relationships: Set<SymbolGraph.Relationship>) -> [ SourceIdentifier : TargetIdentifier] {
193+
194+
// Filter all the relationships down to only declaredIn and inContextOf.
195+
let filtered = relationships.filter { $0.kind == .declaredIn || $0.kind == .inContextOf }
196+
197+
// Create a dictionary of relationships by their source identifier, to
198+
// speed up lookup below.
199+
let relsBySource = filtered.reduce(into: [:]) { result, rel in
200+
result[rel.source] = rel
201+
}
202+
203+
// Reduce the filtered list and build up a dictionary of:
204+
// - key = original source symbol
205+
// - value = target of declaredIn = which external module the symbol was
206+
// declared in.
207+
return filtered.reduce(into: [:]) { result, relationship in
208+
var rel = relationship
209+
var seen: Set<String> = []
210+
// Walk the target->relationship hierarchy until we find declaredIn
211+
while rel.kind != .declaredIn {
212+
// Stop if we encounter a cycle in the hierarchy
213+
guard !seen.contains(rel.source) else {
214+
return
215+
}
216+
seen.insert(rel.source)
217+
// Look up the target relationship in the dictionary created
218+
// above. Stop if not found.
219+
guard let r = relsBySource[rel.target] else {
220+
return
221+
}
222+
// Step to the target relationship looking for
223+
// declaredIn.
224+
rel = r
225+
}
226+
// Save the original source as the key to the declaredIn target
227+
result[relationship.source] = rel.target
228+
}
229+
}
230+
187231
/// Tries to obtain `docComment`s for all `targets` and copies the documentation from sources to the target.
188232
///
189233
/// Iterates over all `targets` calling the `source` method to obtain a list of symbols that should serve as sources for the target's `docComment`.

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

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,5 +396,80 @@ struct SymbolGraphRelationshipsBuilder {
396396
setAsInheritedSymbol(origin: origin, for: &context.documentationCache[reference]!, originNode: symbolIndex[origin.identifier].flatMap { context.documentationCache[$0] })
397397
}
398398
}
399-
}
400399

400+
/// Add a new generic constraint: "Self is SomeProtocol" to members of
401+
/// protocol extensions of protocols from external modules. When a protocol
402+
/// is defined in a different module it's not clear which protocol the
403+
/// extension is for since we don't otherwise display that, unless implied
404+
/// by curation.
405+
///
406+
/// - Parameters:
407+
/// - edge: A symbol graph relationship with a source and a target.
408+
/// - selector: The symbol graph selector in which the relationship is
409+
/// relevant.
410+
/// - extendedModuleRelationships: Source->target dictionary for external
411+
/// module relationships.
412+
/// - symbolIndex: A symbol lookup map by precise identifier.
413+
/// - documentationCache: A documentation node lookup by the node's resolved reference.
414+
415+
static func addProtocolExtensionMemberConstraint(
416+
edge: SymbolGraph.Relationship,
417+
selector: UnifiedSymbolGraph.Selector,
418+
extendedModuleRelationships: [String : String],
419+
symbolIndex: inout [String: ResolvedTopicReference],
420+
documentationCache: [ResolvedTopicReference: DocumentationNode]
421+
) {
422+
423+
// Utility function to look up a symbol identifier in the
424+
// symbol index, returning its documentation node and semantic symbol
425+
func nodeAndSymbolFor(identifier: String) -> (DocumentationNode, Symbol)? {
426+
if let node = symbolIndex[identifier].flatMap({documentationCache[$0]}),
427+
let symbol = node.semantic as? Symbol {
428+
return (node, symbol)
429+
}
430+
return nil
431+
}
432+
433+
// Is this symbol a member of some type from an extended module?
434+
guard let extendedModuleRelationship = extendedModuleRelationships[edge.target] else {
435+
return
436+
}
437+
438+
// Return unless the target symbol is a protocol. The "Self is ..."
439+
// constraint only makes sense for protocol extensions.
440+
guard let (targetNode, targetSymbol) = nodeAndSymbolFor(identifier: edge.target) else {
441+
return
442+
}
443+
guard targetNode.kind == .extendedProtocol else {
444+
return
445+
}
446+
447+
// Obtain the source symbol
448+
guard let (_, sourceSymbol) = nodeAndSymbolFor(identifier: edge.source) else {
449+
return
450+
}
451+
452+
// Obtain the extended module documentation node.
453+
guard let (_, extendedModuleSymbol) = nodeAndSymbolFor(identifier: extendedModuleRelationship) else {
454+
return
455+
}
456+
457+
// Create a new generic constraint: "Self is SomeProtocol" to show which
458+
// protocol this function's extension is extending. When the protocol is
459+
// defined in a different module it's not clear at all which protocol it
460+
// is, especially if the curation doesn't indicate that.
461+
let newConstraint = SymbolGraph.Symbol.Swift.GenericConstraint(
462+
kind: SymbolGraph.Symbol.Swift.GenericConstraint.Kind.sameType,
463+
leftTypeName: "Self",
464+
rightTypeName: targetSymbol.title
465+
)
466+
467+
// Add the constraint to the source symbol, the member of the protocol
468+
// extension.
469+
sourceSymbol.addSwiftExtensionConstraint(
470+
extendedModule: extendedModuleSymbol.title,
471+
extendedSymbolKind: .protocol,
472+
constraint: newConstraint
473+
)
474+
}
475+
}

Sources/SwiftDocC/Model/DocumentationNode.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,6 @@ public struct DocumentationNode {
239239

240240
self.availableSourceLanguages = reference.sourceLanguages
241241

242-
let extendedModule: String? = unifiedSymbol.mixins
243-
.first(where: { $0.0.platform == platformName })?.value
244-
.getValueIfPresent(for: SymbolGraph.Symbol.Swift.Extension.self)?.extendedModule
245-
246242
let semanticSymbol = Symbol(
247243
kindVariants: DocumentationDataVariants(
248244
symbolData: unifiedSymbol.kind,
@@ -272,7 +268,6 @@ public struct DocumentationNode {
272268
defaultVariantValue: platformName.map(PlatformName.init(operatingSystemName:))
273269
),
274270
moduleReference: moduleReference,
275-
extendedModule: extendedModule,
276271
externalIDVariants: DocumentationDataVariants(defaultVariantValue: unifiedSymbol.uniqueIdentifier),
277272
accessLevelVariants: DocumentationDataVariants(
278273
symbolData: unifiedSymbol.accessLevel,

Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,13 +1198,27 @@ public struct RenderNodeTranslator: SemanticVisitor {
11981198

11991199
if let crossImportOverlayModule = symbol.crossImportOverlayModule {
12001200
node.metadata.modulesVariants = VariantCollection(defaultValue: [RenderMetadata.Module(name: crossImportOverlayModule.declaringModule, relatedModules: crossImportOverlayModule.bystanderModules)])
1201-
} else if let extendedModule = symbol.extendedModule, extendedModule != moduleName.displayName {
1202-
node.metadata.modulesVariants = VariantCollection(defaultValue: [RenderMetadata.Module(name: moduleName.displayName, relatedModules: [extendedModule])])
1201+
1202+
} else if let moduleVariants = VariantCollection<[RenderMetadata.Module]?>(
1203+
from: symbol.extendedModuleVariants,
1204+
transform: { (_, value) in
1205+
let relatedModules: [String]?
1206+
if value != moduleName.displayName {
1207+
relatedModules = [value]
1208+
} else {
1209+
relatedModules = nil
1210+
}
1211+
return [
1212+
RenderMetadata.Module(name: moduleName.displayName, relatedModules: relatedModules)
1213+
]
1214+
}
1215+
) {
1216+
node.metadata.modulesVariants = moduleVariants
12031217
} else {
12041218
node.metadata.modulesVariants = VariantCollection(defaultValue: [RenderMetadata.Module(name: moduleName.displayName, relatedModules: nil)])
12051219
}
1206-
1207-
node.metadata.extendedModuleVariants = VariantCollection<String?>(defaultValue: symbol.extendedModule)
1220+
1221+
node.metadata.extendedModuleVariants = VariantCollection<String?>(from: symbol.extendedModuleVariants)
12081222

12091223
node.metadata.platformsVariants = VariantCollection<[AvailabilityRenderItem]?>(from: symbol.availabilityVariants) { _, availability in
12101224
availability.availability

Sources/SwiftDocC/Semantics/ReferenceResolver.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,6 @@ struct ReferenceResolver: SemanticVisitor {
480480
roleHeadingVariants: symbol.roleHeadingVariants,
481481
platformNameVariants: symbol.platformNameVariants,
482482
moduleReference: symbol.moduleReference,
483-
extendedModule: symbol.extendedModule,
484483
requiredVariants: symbol.isRequiredVariants,
485484
externalIDVariants: symbol.externalIDVariants,
486485
accessLevelVariants: symbol.accessLevelVariants,

Sources/SwiftDocC/Semantics/Symbol/Symbol.swift

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import SymbolKit
2929
/// - ``kindVariants``
3030
/// - ``platformNameVariants``
3131
/// - ``moduleReference``
32-
/// - ``extendedModule``
32+
/// - ``extendedModuleVariants``
3333
/// - ``bystanderModuleNames``
3434
/// - ``isRequiredVariants``
3535
/// - ``externalIDVariants``
@@ -115,9 +115,15 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups
115115

116116
/// The reference to the documentation node that represents this symbol's module symbol.
117117
internal(set) public var moduleReference: ResolvedTopicReference
118-
119-
/// The name of the module extension in which the symbol is defined, if applicable.
120-
internal(set) public var extendedModule: String?
118+
119+
/// The name of the module extension in which the symbol is defined, in each language variant the symbol is available in
120+
public var extendedModuleVariants: DocumentationDataVariants<String> {
121+
var variants = DocumentationDataVariants<String>()
122+
for (trait, swiftExtension) in swiftExtensionVariants() {
123+
variants[trait] = swiftExtension.extendedModule
124+
}
125+
return variants
126+
}
121127

122128
/// The names of any "bystander" modules required for this symbol, if it came from a cross-import overlay.
123129
@available(*, deprecated, message: "Use crossImportOverlayModule instead")
@@ -146,10 +152,16 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups
146152
)
147153

148154
public var locationVariants = DocumentationDataVariants<SymbolGraph.Symbol.Location>()
149-
155+
150156
/// The symbol's availability or conformance constraints, in each language variant the symbol is available in.
151-
public var constraintsVariants = DocumentationDataVariants<[SymbolGraph.Symbol.Swift.GenericConstraint]>()
152-
157+
public var constraintsVariants: DocumentationDataVariants<[SymbolGraph.Symbol.Swift.GenericConstraint]> {
158+
var variants = DocumentationDataVariants<[SymbolGraph.Symbol.Swift.GenericConstraint]>()
159+
for (trait, swiftExtension) in swiftExtensionVariants() {
160+
variants[trait] = swiftExtension.constraints
161+
}
162+
return variants
163+
}
164+
153165
/// The inheritance information for the symbol in each language variant the symbol is available in.
154166
public var originVariants: DocumentationDataVariants<SymbolGraph.Relationship.SourceOrigin>
155167

@@ -236,7 +248,6 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups
236248
roleHeadingVariants: DocumentationDataVariants<String>,
237249
platformNameVariants: DocumentationDataVariants<PlatformName>,
238250
moduleReference: ResolvedTopicReference,
239-
extendedModule: String? = nil,
240251
requiredVariants: DocumentationDataVariants<Bool> = .init(defaultVariantValue: false),
241252
externalIDVariants: DocumentationDataVariants<String>,
242253
accessLevelVariants: DocumentationDataVariants<String>,
@@ -292,8 +303,6 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups
292303
if !self.declarationVariants.hasVariant(for: trait) {
293304
self.declarationVariants[trait] = [[platformNameVariants[trait]]: declaration]
294305
}
295-
case let extensionConstraints as SymbolGraph.Symbol.Swift.Extension where !extensionConstraints.constraints.isEmpty:
296-
self.constraintsVariants[trait] = extensionConstraints.constraints
297306
case let location as SymbolGraph.Symbol.Location:
298307
self.locationVariants[trait] = location
299308
case let spi as SymbolGraph.Symbol.SPI:
@@ -323,12 +332,69 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups
323332
self.redirectsVariants = redirectsVariants
324333
self.originVariants = originVariants
325334
self.automaticTaskGroupsVariants = automaticTaskGroupsVariants
326-
self.extendedModule = extendedModule
327335
}
328336

329337
public override func accept<V: SemanticVisitor>(_ visitor: inout V) -> V.Result {
330338
return visitor.visitSymbol(self)
331339
}
340+
341+
/// Append a new generic constraint for the given extended module
342+
/// - Parameters:
343+
/// - extendedModule: The name of the extended module.
344+
/// - extendedSymbolKind: The kind of the extended symbol.
345+
/// - constraint: The new generic constraints to add.
346+
public func addSwiftExtensionConstraint(
347+
extendedModule: String,
348+
extendedSymbolKind: SymbolGraph.Symbol.KindIdentifier? = nil,
349+
constraint newConstraint: SymbolGraph.Symbol.Swift.GenericConstraint
350+
) {
351+
352+
var swiftExtension: SymbolGraph.Symbol.Swift.Extension
353+
354+
// Does this symbol already have a swift extension variant for the swift trait?
355+
356+
// Yes: Create a new copy of the existing extension with
357+
// the new constraint appended to the existing list
358+
let trait = DocumentationDataVariantsTrait.swift
359+
if let existing = swiftExtensionVariants()[trait] {
360+
// Double check the existing extension uses the same module and type. If it does not,
361+
// we must have a tooling or data consistency problem.
362+
assert(
363+
existing.extendedModule == extendedModule && existing.typeKind == extendedSymbolKind,
364+
"New constraint's module and type kind do not match symbol's existing constraints."
365+
)
366+
swiftExtension = existing
367+
swiftExtension.constraints = swiftExtension.constraints + [newConstraint]
368+
369+
// No: Create a new extension with the specified module, type and
370+
// new constraint
371+
} else {
372+
swiftExtension = SymbolGraph.Symbol.Swift.Extension(
373+
extendedModule: extendedModule,
374+
typeKind: extendedSymbolKind,
375+
constraints: [newConstraint]
376+
)
377+
}
378+
379+
// Save the new or updated extension
380+
self.mixinsVariants[
381+
trait,
382+
default: [:]
383+
][SymbolGraph.Symbol.Swift.Extension.mixinKey] = swiftExtension
384+
}
385+
386+
// MARK: - Private helpers
387+
388+
/// Return all of this symbol's Swift extension variants.
389+
private func swiftExtensionVariants() -> [DocumentationDataVariantsTrait : SymbolGraph.Symbol.Swift.Extension] {
390+
var variants: [DocumentationDataVariantsTrait : SymbolGraph.Symbol.Swift.Extension] = [:]
391+
for (trait, mixins) in mixinsVariants.allValues {
392+
if let swiftExtension = mixins[SymbolGraph.Symbol.Swift.Extension.mixinKey] as? SymbolGraph.Symbol.Swift.Extension {
393+
variants[trait] = swiftExtension
394+
}
395+
}
396+
return variants
397+
}
332398
}
333399

334400
extension Symbol {
@@ -432,6 +498,10 @@ extension Symbol {
432498
/// The first variant of the symbol's platform, if available.
433499
public var platformName: PlatformName? { platformNameVariants.firstValue }
434500

501+
/// The first variant of the symbol's extended module, if available
502+
@available(*, deprecated, message: "Please use extendedModuleVariants instead.")
503+
public var extendedModule: String? { extendedModuleVariants.firstValue }
504+
435505
/// Whether the first variant of the symbol is required in its context.
436506
public var isRequired: Bool {
437507
get { isRequiredVariants.firstValue! }
@@ -462,10 +532,7 @@ extension Symbol {
462532
}
463533

464534
/// The first variant of the symbol's availability or conformance constraints.
465-
public var constraints: [SymbolGraph.Symbol.Swift.GenericConstraint]? {
466-
get { constraintsVariants.firstValue }
467-
set { constraintsVariants.firstValue = newValue }
468-
}
535+
public var constraints: [SymbolGraph.Symbol.Swift.GenericConstraint]? { constraintsVariants.firstValue }
469536

470537
/// The inheritance information for the first variant of the symbol.
471538
public var origin: SymbolGraph.Relationship.SourceOrigin? {

0 commit comments

Comments
 (0)