File: AutomaticCuration.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (317 lines) | stat: -rw-r--r-- 14,256 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
/*
 This source file is part of the Swift.org open source project

 Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
 Licensed under Apache License v2.0 with Runtime Library Exception

 See https://swift.org/LICENSE.txt for license information
 See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation
import Markdown
import SymbolKit


private let automaticSeeAlsoLimit: Int = {
    ProcessInfo.processInfo.environment["DOCC_AUTOMATIC_SEE_ALSO_LIMIT"].flatMap { Int($0) } ?? 15
}()

/// A set of functions that add automatic symbol curation to a topic graph.
public struct AutomaticCuration {
    /// A value type to store an automatically curated task group and its sorting index.
    struct ReferenceGroup {
        let title: String
        let sortOrder: Int
        var references = [ResolvedTopicReference]()
        
        init(title: String, sortOrder: Int = 0, references: [ResolvedTopicReference] = []) {
            self.title = title
            self.sortOrder = sortOrder
            self.references = references
        }
    }
    
    /// A mapping between a symbol kind and its matching group.
    typealias ReferenceGroupIndex = [SymbolGraph.Symbol.KindIdentifier: ReferenceGroup]
    
    /// A static list of predefined groups for each supported kind of symbol.
    static var groups: ReferenceGroupIndex {
        return groupKindOrder.enumerated().reduce(into: ReferenceGroupIndex()) { (result, next) in
            result[next.element] = ReferenceGroup(title: AutomaticCuration.groupTitle(for: next.element), sortOrder: next.offset)
        }
    }
    
    /// Automatic curation task group.
    typealias TaskGroup = (title: String?, references: [ResolvedTopicReference])
    
    /// Returns a list of "automatic curation" task groups, organized by their symbol kind or page kind, with the given traits for the given documentation node.
    /// - Parameters:
    ///   - node: The node to generate "automatic curation" task groups for.
    ///   - variantsTraits: The variant traits to filter the automatic curation task groups for.
    ///   - context: The context to lookup entities and topic graph edges in.
    /// - Returns: A list of title and references pairs.
    static func topics(
        for node: DocumentationNode,
        withTraits variantsTraits: Set<DocumentationDataVariantsTrait>,
        context: DocumentationContext
    ) throws -> [TaskGroup] {
        let languagesFilter = Set(variantsTraits.compactMap {
            $0.interfaceLanguage.map { SourceLanguage(id: $0) }
        })
        
        // Because the `TopicGraph` uses the same nodes for both language representations and doesn't have awareness of language specific edges,
        // it can't correctly determine language specific automatic curation. Instead we ask the `PathHierarchy` which is source-language-aware.
        let children = context.linkResolver.localResolver.directDescendants(of: node.reference, languagesFilter: languagesFilter)
            .sorted(by: \.path)
        
        return try topics(
            for: children,
            inInheritedSymbolsAPICollection: GeneratedDocumentationTopics.isInheritedSymbolsAPICollectionNode(node.reference, in: context.topicGraph),
            withTraits: variantsTraits,
            context: context
        )
    }
    
    /// Organizes the given list of references into "automatic curation" task groups based on their symbol kind or page kind.
    /// - Parameters:
    ///   - references: The list of references to organize into "automatic curation" task groups.
    ///   - inInheritedSymbolsAPICollection: Whether or not this automatic curation is for a "inherited symbols" API collection.
    ///   - variantsTraits: The variant traits to filter the automatic curation task groups for.
    ///   - context: The context to lookup entities and topic graph edges in.
    /// - Returns: A list of title and references pairs.
    static func topics(
        for references: [ResolvedTopicReference],
        inInheritedSymbolsAPICollection: Bool,
        withTraits variantsTraits: Set<DocumentationDataVariantsTrait>,
        context: DocumentationContext
    ) throws -> [TaskGroup] {
        try references
            .reduce(into: AutomaticCuration.groups) { groupsIndex, reference in
                guard let topicNode = context.topicGraph.nodeWithReference(reference),
                      !topicNode.isEmptyExtension,
                      topicNode.shouldAutoCurateInCanonicalLocation
                else {
                    return
                }
                
                // Skip members of "inherited" API collections unless the automatic curation is for an Inherited API collection.
                guard inInheritedSymbolsAPICollection
                   || !(context.topicGraph.reverseEdges[reference] ?? []).contains(where: { GeneratedDocumentationTopics.isInheritedSymbolsAPICollectionNode($0, in: context.topicGraph) })
                else {
                    return
                }

                // If this symbol is an overload group and all its overloaded children were manually
                // curated elsewhere, skip it so it doesn't clutter the curation hierarchy with a
                // duplicate symbol.
                if let overloads = context.linkResolver.localResolver.overloads(ofGroup: reference), overloads.isEmpty {
                    return
                }

                let childNode = try context.entity(with: reference)
                guard let childSymbol = childNode.semantic as? Symbol else {
                    return
                }
                
                // If we have a specific trait to collect topics for, we only want
                // to include children that have a kind available for that trait.
                //
                // Otherwise, we'll fall back to the first kind variant.
                let childSymbolKindIdentifier: SymbolGraph.Symbol.KindIdentifier?
                if !variantsTraits.isEmpty {
                    if let matchingTrait = variantsTraits.first(where: { childSymbol.kindVariants[$0] != nil }) {
                        childSymbolKindIdentifier = childSymbol.kindVariants[matchingTrait]?.identifier
                    } else {
                        childSymbolKindIdentifier = nil
                    }
                } else {
                    childSymbolKindIdentifier = childSymbol.kindVariants.firstValue?.identifier
                }
                
                if let childSymbolKindIdentifier {
                    groupsIndex[childSymbolKindIdentifier]?.references.append(reference)
                }
            }
            .lazy
            // Sort the groups in the order intended for rendering
            .sorted(by: \.value.sortOrder)
            // Map to sorted tuples
            .compactMap { groupIndex in
                let group = groupIndex.value
                guard !group.references.isEmpty else { return nil }
                return (title: group.title, references: group.references.sorted(by: \.path))
            }
    }
    
    /// Returns a list of automatically curated See Also task groups for the given documentation node.
    /// - Parameters:
    ///   - node: A node for which to generate a See Also group.
    ///   - context: A documentation context.
    ///   - bundle: A documentation bundle.
    /// - Returns: A group title and the group's references or links.
    ///   `nil` if the method can't find any relevant links to automatically generate a See Also content.
    static func seeAlso(
        for node: DocumentationNode,
        withTraits variantsTraits: Set<DocumentationDataVariantsTrait>,
        context: DocumentationContext,
        bundle: DocumentationBundle,
        renderContext: RenderContext?,
        renderer: DocumentationContentRenderer
    ) -> TaskGroup? {
        if (node.options?.automaticSeeAlsoEnabled ?? context.options?.automaticSeeAlsoEnabled) == false {
            return nil
        }
        
        // FIXME: The shortest path to the reference may not be applicable to the given variants traits.
        // First try getting the canonical path from a render context, default to the documentation context
        guard let canonicalPath = renderContext?.store.content(for: node.reference)?.canonicalPath ?? context.shortestFinitePath(to: node.reference),
              let parentReference = canonicalPath.last
        else {
            // If the symbol is not curated or is a root symbol, no See Also please.
            return nil
        }
        
        let variantLanguages = Set(variantsTraits.compactMap { traits in
            traits.interfaceLanguage.map { SourceLanguage(id: $0) }
        })
        
        func isRelevant(_ filteredGroup: DocumentationContentRenderer.ReferenceGroup) -> Bool {
            // Check if the task group is filtered to a subset of languages
            if let languageFilter = filteredGroup.languageFilter,
               languageFilter.isDisjoint(with: variantLanguages)
            {
                // This group is only applicable to other languages than the given variant traits.
                return false
            }
            
            // Otherwise, check that the group contains the this reference.
            return filteredGroup.references.contains(node.reference)
        }
        
        func filterReferences(_ references: [ResolvedTopicReference]) -> [ResolvedTopicReference] {
            Array(
                references
                .filter { reference in
                    // Don't include the current node.
                    reference != node.reference
                    &&
                    // Don't include nodes that aren't available in any of the given traits.
                    !context.sourceLanguages(for: reference).isDisjoint(with: variantLanguages)
                }
                // Don't create too long See Also sections
                .prefix(automaticSeeAlsoLimit)
            )
        }
        
        // Look up the render context first
        if let taskGroups = renderContext?.store.content(for: parentReference)?.taskGroups,
           let linkingGroup = taskGroups.first(where: isRelevant)
        {
            // Group match in render context, verify if there are any other references besides the current one.
            guard linkingGroup.references.count > 1 else { return nil }
            return (title: linkingGroup.title, references: filterReferences(linkingGroup.references))
        }
        
        // Get the parent's task groups
        guard let taskGroups = renderer.taskGroups(for: parentReference) else {
            return nil
        }
        
        // Find the group where the current symbol is curated
        let linkingGroup = taskGroups.first(where: isRelevant)
        
        // Verify there is a matching linking group and more references than just the current one.
        guard let group = linkingGroup, group.references.count > 1 else {
            return nil
        }
        
        return (title: group.title, references: filterReferences(group.references))
    }
}

extension AutomaticCuration {
    /// Returns a topics group title for the given symbol kind.
    /// - Parameter symbolKind: A symbol kind, such as a protocol or a variable.
    /// - Returns: A group title for symbols of the given kind.
    static func groupTitle(`for` symbolKind: SymbolGraph.Symbol.KindIdentifier) -> String {
        switch symbolKind {
            case .`associatedtype`: return "Associated Types"
            case .`class`: return "Classes"
            case .`deinit`: return "Deinitializers"
            case .`enum`: return "Enumerations"
            case .`case`: return "Enumeration Cases"
            case .dictionary: return "Dictionaries"
            case .extension: return "Extensions"
            case .`func`: return "Functions"
            case .httpRequest: return "Endpoints"
            case .`operator`: return "Operators"
            case .`init`: return "Initializers"
            case .ivar: return "Instance Variables"
            case .macro: return "Macros"
            case .`method`: return "Instance Methods"
            case .namespace: return "Namespaces"
            case .`property`: return "Instance Properties"
            case .`protocol`: return "Protocols"
            case .`struct`: return "Structures"
            case .`subscript`: return "Subscripts"
            case .`typeMethod`: return "Type Methods"
            case .`typeProperty`: return "Type Properties"
            case .`typeSubscript`: return "Type Subscripts"
            case .`typealias`: return "Type Aliases"
            case .union: return "Unions"
            case .`var`: return "Variables"
            case .module: return "Modules"
            case .extendedModule: return "Extended Modules"
            case .extendedClass: return "Extended Classes"
            case .extendedStructure: return "Extended Structures"
            case .extendedEnumeration: return "Extended Enumerations"
            case .extendedProtocol: return "Extended Protocols"
            case .unknownExtendedType: return "Extended Types"
            default: return "Symbols"
        }
    }

    /// The order of symbol kinds when grouped automatically.
    ///
    /// Add a symbol kind to `KindIdentifier.noPageKinds` if it should not generate a page in the
    /// documentation hierarchy.
    static let groupKindOrder: [SymbolGraph.Symbol.KindIdentifier] = [
        .namespace,

        .`class`,
        .`protocol`,
        .`struct`,
        .`union`,
        .`httpRequest`,
        .`dictionary`,
        .`var`,
        .`func`,
        .`operator`,
        .`macro`,

        .`associatedtype`,
        .`case`,
        .`init`,
        .`deinit`,
        .`ivar`,
        .`property`,
        .`method`,
        .`subscript`,

        .`typealias`,
        .`typeProperty`,
        .`typeMethod`,
        .`enum`,
        .`typeSubscript`,
        
        .extendedModule,
        .extendedClass,
        .extendedProtocol,
        .extendedStructure,
        .extendedEnumeration,
        .unknownExtendedType,

        .extension,
    ]
}