Skip to content

Commit 50a487f

Browse files
authored
Emit language specific topic sections in the markdown representation of automatic curation (#856)
* Simplify creation of disambiguated paths * Generate language specific curation markdown rdar://124527905 * Stop using the topic graph to determine automatic curation * Conform SourceLanguage to Comparable
1 parent 2196c3d commit 50a487f

File tree

8 files changed

+323
-108
lines changed

8 files changed

+323
-108
lines changed

Sources/SwiftDocC/Catalog Processing/GeneratedCurationWriter.swift

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,25 +39,15 @@ public struct GeneratedCurationWriter {
3939
func defaultCurationText(for reference: ResolvedTopicReference) -> String? {
4040
guard let node = context.documentationCache[reference],
4141
let symbol = node.semantic as? Symbol,
42-
let automaticTopics = try? AutomaticCuration.topics(for: node, withTraits: [], context: context),
43-
!automaticTopics.isEmpty
42+
let taskGroups = taskGroupsToWrite(for: node)
4443
else {
4544
return nil
4645
}
4746

4847
let relativeLinks = linkResolver.disambiguatedRelativeLinksForDescendants(of: reference)
4948

50-
// Top-level curation has a few special behaviors regarding symbols with different representations in multiple languages.
51-
let isForTopLevelCuration = symbol.kind.identifier == .module
52-
5349
var text = ""
54-
for taskGroup in automaticTopics {
55-
if isForTopLevelCuration, let firstReference = taskGroup.references.first, context.documentationCache[firstReference]?.symbol?.kind.identifier == .typeProperty {
56-
// Skip type properties in top-level curation. It's not clear what's the right place for these symbols are since they exist in
57-
// different places in different source languages (which documentation extensions don't yet have a way of representing).
58-
continue
59-
}
60-
50+
for taskGroup in taskGroups {
6151
let links: [(link: String, comment: String?)] = taskGroup.references.compactMap { (curatedReference: ResolvedTopicReference) -> (String, String?)? in
6252
guard let linkInfo = relativeLinks[curatedReference] else { return nil }
6353
// If this link contains disambiguation, include a comment with the full symbol declaration to make it easier to know which symbol the link refers to.
@@ -73,7 +63,10 @@ public struct GeneratedCurationWriter {
7363

7464
guard !links.isEmpty else { continue }
7565

76-
text.append("\n\n### \(taskGroup.title ?? "<!-- This auto-generated topic has no title -->")\n")
66+
text.append("\n\n### \(taskGroup.title)\n")
67+
if let language = taskGroup.languageFilter {
68+
text.append("\n@SupportedLanguage(\(language.taskGroupID))\n")
69+
}
7770

7871
// Calculate the longest link to nicely align all the comments
7972
let longestLink = links.map(\.link.count).max()! // `links` are non-empty so it's safe to force-unwrap `.max()` here
@@ -180,4 +173,95 @@ public struct GeneratedCurationWriter {
180173

181174
return contentsToWrite
182175
}
176+
177+
private struct GeneratedTaskGroup: Equatable {
178+
var title: String
179+
var references: [ResolvedTopicReference]
180+
var languageFilter: SourceLanguage?
181+
}
182+
183+
private func taskGroupsToWrite(for node: DocumentationNode) -> [GeneratedTaskGroup]? {
184+
// Perform source-language specific curation in a stable order
185+
let languagesToCurate = node.availableSourceLanguages.sorted()
186+
var topicsByLanguage = [SourceLanguage: [AutomaticCuration.TaskGroup]]()
187+
for language in languagesToCurate {
188+
topicsByLanguage[language] = try? AutomaticCuration.topics(for: node, withTraits: [.init(interfaceLanguage: language.id)], context: context)
189+
}
190+
191+
guard topicsByLanguage.count > 1 else {
192+
// If this node doesn't have curation in more than one language, return that curation _without_ a language filter.
193+
return topicsByLanguage.first?.value.map { .init(title: $0.title! /* Automatically-generated task groups always have a title. */, references: $0.references) }
194+
}
195+
196+
// Checks if a node for the given reference is only available in the given source language.
197+
func isOnlyAvailableIn(_ reference: ResolvedTopicReference, _ language: SourceLanguage) -> Bool {
198+
context.documentationCache[reference]?.availableSourceLanguages == [language]
199+
}
200+
201+
// This is defined as an inner function so that the taskGroups-loop can return to finish processing task groups for that symbol kind.
202+
func addProcessedTaskGroups(_ symbolKind: SymbolGraph.Symbol.KindIdentifier, into result: inout [GeneratedTaskGroup]) {
203+
let title = AutomaticCuration.groupTitle(for: symbolKind)
204+
let taskGroups: [GeneratedTaskGroup] = languagesToCurate.compactMap { language in
205+
topicsByLanguage[language]?
206+
.first(where: { $0.title == title })
207+
.map { (title, references) in
208+
// Automatically-generated task groups always have a title.
209+
GeneratedTaskGroup(title: title!, references: references, languageFilter: language)
210+
}
211+
}
212+
guard taskGroups.count > 1 else {
213+
if let taskGroup = taskGroups.first {
214+
if taskGroup.references.allSatisfy({ isOnlyAvailableIn($0, taskGroup.languageFilter!) }) {
215+
// These symbols are all only available in one source language. We can omit the language filter.
216+
result.append(.init(title: title, references: taskGroup.references))
217+
} else {
218+
// Add the task group as-is, with its language filter.
219+
result.append(taskGroup)
220+
}
221+
}
222+
return
223+
}
224+
225+
// Check if the source-language specific task groups need to be emitted individually
226+
for taskGroup in taskGroups {
227+
// Gather all the references for the other task groups
228+
let otherReferences = taskGroups.reduce(into: Set(), { accumulator, group in
229+
guard group != taskGroup else { return }
230+
accumulator.formUnion(group.references)
231+
})
232+
let uniqueReferences = Set(taskGroup.references).subtracting(otherReferences)
233+
234+
guard uniqueReferences.isEmpty || uniqueReferences.allSatisfy({ isOnlyAvailableIn($0, taskGroup.languageFilter!) }) else {
235+
// If at least one of the task groups contains a a language-refined symbol that's not in the other task groups, then emit all task groups individually
236+
result.append(contentsOf: taskGroups)
237+
return
238+
}
239+
}
240+
241+
// Otherwise, if the task groups all contain the same symbols or only contain single-language symbols, emit a combined task group without a language filter.
242+
// When DocC renders this task group it will filter out the single-language symbols, producing correct and consistent results in all language variants without
243+
// needing to repeat the task groups with individual language filters.
244+
result.append(.init(
245+
title: title,
246+
references: taskGroups.reduce(into: Set(), { $0.formUnion($1.references) }).sorted(by: \.path)
247+
))
248+
}
249+
250+
var result = [GeneratedTaskGroup]()
251+
for symbolKind in AutomaticCuration.groupKindOrder {
252+
addProcessedTaskGroups(symbolKind, into: &result)
253+
}
254+
return result
255+
}
256+
}
257+
258+
private extension SourceLanguage {
259+
var taskGroupID: String {
260+
switch self {
261+
case .objectiveC:
262+
return "objc"
263+
default:
264+
return self.linkDisambiguationID
265+
}
266+
}
183267
}

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

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -44,23 +44,31 @@ extension PathHierarchy {
4444
let node = lookup[nodeID]!
4545

4646
var gathered = [(symbolID: String, (link: String, hasDisambiguation: Bool, id: ResolvedIdentifier, isSwift: Bool))]()
47-
for (_, tree) in node.children {
48-
let disambiguatedChildren = tree.disambiguatedValuesWithCollapsedUniqueSymbols(includeLanguage: false)
49-
50-
for (node, disambiguation) in disambiguatedChildren {
51-
guard let id = node.identifier, let symbolID = node.symbol?.identifier.precise else { continue }
52-
let suffix = disambiguation.makeSuffix()
53-
gathered.append((
54-
symbolID: symbolID, (
55-
link: node.name + suffix,
56-
hasDisambiguation: !suffix.isEmpty,
57-
id: id,
58-
isSwift: node.symbol?.identifier.interfaceLanguage == "swift"
59-
)
60-
))
47+
48+
func gatherLinksFrom(_ containers: some Sequence<DisambiguationContainer>) {
49+
for container in containers {
50+
let disambiguatedChildren = container.disambiguatedValuesWithCollapsedUniqueSymbols(includeLanguage: false)
51+
52+
for (node, disambiguation) in disambiguatedChildren {
53+
guard let id = node.identifier, let symbolID = node.symbol?.identifier.precise else { continue }
54+
let suffix = disambiguation.makeSuffix()
55+
gathered.append((
56+
symbolID: symbolID, (
57+
link: node.name + suffix,
58+
hasDisambiguation: !suffix.isEmpty,
59+
id: id,
60+
isSwift: node.symbol?.identifier.interfaceLanguage == "swift"
61+
)
62+
))
63+
}
6164
}
6265
}
6366

67+
gatherLinksFrom(node.children.values)
68+
if let counterpart = node.counterpart {
69+
gatherLinksFrom(counterpart.children.values)
70+
}
71+
6472
// If a symbol node exist in multiple languages, prioritize the Swift variant.
6573
let uniqueSymbolValues = Dictionary(gathered, uniquingKeysWith: { lhs, rhs in lhs.isSwift ? lhs : rhs })
6674
.values.map({ ($0.id, ($0.link, $0.hasDisambiguation)) })
@@ -92,8 +100,8 @@ extension PathHierarchy {
92100
nameTransform = { $0 }
93101
}
94102

95-
func descend(_ node: Node, accumulatedPath: String) -> [(String, (String, Bool))] {
96-
var results: [(String, (String, Bool))] = []
103+
func descend(_ node: Node, accumulatedPath: String) -> [String: String] {
104+
var innerPathsByUSR: [String: String] = [:]
97105
let children = [String: DisambiguationContainer](node.children.map {
98106
var name = $0.key
99107
if !caseSensitive {
@@ -102,8 +110,8 @@ extension PathHierarchy {
102110
return (nameTransform(name), $0.value)
103111
}, uniquingKeysWith: { $0.merge(with: $1) })
104112

105-
for (_, tree) in children {
106-
let disambiguatedChildren = tree.disambiguatedValuesWithCollapsedUniqueSymbols(includeLanguage: includeLanguage)
113+
for (_, container) in children {
114+
let disambiguatedChildren = container.disambiguatedValuesWithCollapsedUniqueSymbols(includeLanguage: includeLanguage)
107115
let uniqueNodesWithChildren = Set(disambiguatedChildren.filter { $0.disambiguation.value() != nil && !$0.value.children.isEmpty }.map { $0.value.symbol?.identifier.precise })
108116

109117
for (node, disambiguation) in disambiguatedChildren {
@@ -112,7 +120,7 @@ extension PathHierarchy {
112120
// When descending through placeholder nodes, we trust that the known disambiguation
113121
// that they were created with is necessary.
114122
var knownDisambiguation = ""
115-
let element = tree.storage.first!
123+
let element = container.storage.first!
116124
if let kind = element.kind {
117125
knownDisambiguation += "-\(kind)"
118126
}
@@ -123,37 +131,39 @@ extension PathHierarchy {
123131
} else {
124132
path = accumulatedPath + "/" + nameTransform(node.name)
125133
}
126-
if let symbol = node.symbol {
127-
results.append(
128-
(symbol.identifier.precise, (path + disambiguation.makeSuffix(), symbol.identifier.interfaceLanguage == "swift"))
129-
)
134+
if let symbol = node.symbol,
135+
// If a symbol node exist in multiple languages, prioritize the Swift variant.
136+
node.counterpart == nil || symbol.identifier.interfaceLanguage == "swift"
137+
{
138+
innerPathsByUSR[symbol.identifier.precise] = path + disambiguation.makeSuffix()
130139
}
131140
if includeDisambiguationForUnambiguousChildren || uniqueNodesWithChildren.count > 1 {
132141
path += disambiguation.makeSuffix()
133142
}
134-
results += descend(node, accumulatedPath: path)
143+
innerPathsByUSR.merge(descend(node, accumulatedPath: path), uniquingKeysWith: { currentPath, newPath in
144+
assertionFailure("Should only have gathered one path per symbol ID. Found \(currentPath) and \(newPath) for the same USR.")
145+
return currentPath
146+
})
135147
}
136148
}
137-
return results
149+
return innerPathsByUSR
138150
}
139151

140-
var gathered: [(String, (String, Bool))] = []
152+
var pathsByUSR: [String: String] = [:]
141153

142154
for node in modules {
143-
let path = "/" + node.name
144-
gathered.append(
145-
(node.name, (path, node.symbol == nil || node.symbol!.identifier.interfaceLanguage == "swift"))
146-
)
147-
gathered += descend(node, accumulatedPath: path)
155+
let modulePath = "/" + node.name
156+
pathsByUSR[node.name] = modulePath
157+
pathsByUSR.merge(descend(node, accumulatedPath: modulePath), uniquingKeysWith: { currentPath, newPath in
158+
assertionFailure("Should only have gathered one path per symbol ID. Found \(currentPath) and \(newPath) for the same USR.")
159+
return currentPath
160+
})
148161
}
149162

150-
// If a symbol node exist in multiple languages, prioritize the Swift variant.
151-
let result = [String: (String, Bool)](gathered, uniquingKeysWith: { lhs, rhs in lhs.1 ? lhs : rhs }).mapValues({ $0.0 })
152-
153163
assert(
154-
Set(result.values).count == result.keys.count,
164+
Set(pathsByUSR.values).count == pathsByUSR.keys.count,
155165
{
156-
let collisionDescriptions = result
166+
let collisionDescriptions = pathsByUSR
157167
.reduce(into: [String: [String]](), { $0[$1.value, default: []].append($1.key) })
158168
.filter({ $0.value.count > 1 })
159169
.map { "\($0.key)\n\($0.value.map({ " " + $0 }).joined(separator: "\n"))" }
@@ -164,7 +174,7 @@ extension PathHierarchy {
164174
}()
165175
)
166176

167-
return result
177+
return pathsByUSR
168178
}
169179
}
170180

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

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -175,18 +175,7 @@ enum GeneratedDocumentationTopics {
175175
)
176176
}
177177

178-
// Create a temp node in order to generate the automatic curation
179-
let temporaryCollectionNode = DocumentationNode(
180-
reference: collectionReference,
181-
kind: .collectionGroup,
182-
sourceLanguage: automaticCurationSourceLanguage,
183-
availableSourceLanguages: automaticCurationSourceLanguages,
184-
name: DocumentationNode.Name.conceptual(title: title),
185-
markup: Document(parsing: ""),
186-
semantic: Article(markup: nil, metadata: nil, redirects: nil, options: [:])
187-
)
188-
189-
let collectionTaskGroups = try AutomaticCuration.topics(for: temporaryCollectionNode, withTraits: [], context: context)
178+
let collectionTaskGroups = try AutomaticCuration.topics(for: identifiers, inInheritedSymbolsAPICollection: true, withTraits: [], context: context)
190179
.map { taskGroup in
191180
AutomaticTaskGroupSection(
192181
// Force-unwrapping the title since automatically-generated task groups always have a title.

0 commit comments

Comments
 (0)