File: UnifiedSymbolGraph.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (427 lines) | stat: -rw-r--r-- 23,157 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
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
/*
 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

/// A combined ``SymbolGraph`` from multiple languages' views of the same module.
public class UnifiedSymbolGraph {

    /// The module that these combined symbol graphs represent.
    public var moduleName: String

    /// The decoded metadata about the module being represented, indexed by the file path of the symbol graph it was decoded from.
    ///
    /// Some module symbol graphs contain different metadata (e.g. platform it was rendered for, "bystander" modules, etc), and this allows users of unified graph data to load individual graphs' module data.
    public var moduleData: [URL: SymbolGraph.Module]

    /// Metadata about the individual symbol graphs that were combined together, indexed by the file path of each input symbol graph.
    public var metadata: [URL: SymbolGraph.Metadata]

    /// The symbols in the module, indexed by precise identifier.
    public var symbols: [String: UnifiedSymbolGraph.Symbol]

    /// The relationships between symbols.
    @available(*, deprecated, message: "Use unifiedRelationships and orphanRelationships instead")
    public var relationships: [SymbolGraph.Relationship] {
        var allRelations = mergeRelationships(Array(relationshipsByLanguage.values.joined()))
        allRelations.append(contentsOf: self.orphanRelationships)
        return allRelations
    }

    /// The relationships between symbols, separated by the language's view those relationships are
    /// relevant in.
    public var relationshipsByLanguage: [Selector: [SymbolGraph.Relationship]]

    /// A list of relationships between symbols, for which neither the source nor target were able
    /// to be matched with an appropriate symbol in the collected graphs.
    public var orphanRelationships: [SymbolGraph.Relationship]

    /// Overload group symbols that were detected while collecting symbol graphs.
    ///
    /// This may not reflect the overload groups present in the collected graph;
    /// ``GraphCollector/finishLoading(createOverloadGroups:)`` can deduplicate overload groups or
    /// compute new ones depending on the symbol graphs that were loaded.
    public var overloadGroupsFromOriginalGraphs: Set<String>

    /// Overload group symbols that are in the final symbol graph after the graph collector finished.
    ///
    /// These could either be processed from pre-existing overload groups, calculated before
    /// unification, or computed after the fact, when
    /// ``GraphCollector/finishLoading(createOverloadGroups:)`` is given a `createOverloadGroups`
    /// parameter of `true`.
    public var overloadGroupSymbols: Set<String>

    public init?(fromSingleGraph graph: SymbolGraph, at url: URL) {
        let (_, isMainGraph) = GraphCollector.moduleNameFor(graph, at: url)

        self.moduleName = graph.module.name
        self.moduleData = [url: graph.module]
        self.metadata = [url: graph.metadata]
        var overloadedSymbols: Set<String> = []
        self.symbols = graph.symbols.mapValues { symbol in
            if symbol.identifier.precise.hasSuffix(SymbolGraph.Symbol.overloadGroupIdentifierSuffix) {
                overloadedSymbols.insert(symbol.identifier.precise)
            }
            return UnifiedSymbolGraph.Symbol(fromSingleSymbol: symbol, module: graph.module, isMainGraph: isMainGraph)
        }
        self.overloadGroupsFromOriginalGraphs = overloadedSymbols
        self.overloadGroupSymbols = []
        self.relationshipsByLanguage = [:]
        self.orphanRelationships = []
        loadRelationships(fromGraph: graph)
    }
}

extension UnifiedSymbolGraph {
    func loadRelationships(fromGraph graph: SymbolGraph) {
        var newRelations: [Selector: [SymbolGraph.Relationship]] = [:]

        for rel in graph.relationships {
            // associate each relationship with a selector based on the symbol(s) it references
            let selectors: [Selector]
            if let sourceSym = graph.symbols[rel.source] {
                selectors = [Selector(interfaceLanguage: sourceSym.identifier.interfaceLanguage, platform: graph.module.platform.name)]
            } else if let targetSym = graph.symbols[rel.target] {
                selectors = [Selector(interfaceLanguage: targetSym.identifier.interfaceLanguage, platform: graph.module.platform.name)]
            } else if let unifiedSourceSym = self.symbols[rel.source] {
                selectors = unifiedSourceSym.mainGraphSelectors
            } else if let unifiedTargetSym = self.symbols[rel.target] {
                selectors = unifiedTargetSym.mainGraphSelectors
            } else {
                // If we can't find the appropriate selector(s) to use, consider the relationship an
                // orphan and save it for later.
                self.orphanRelationships.append(rel)
                continue
            }

            for selector in selectors {
                if !newRelations.keys.contains(selector) {
                    newRelations[selector] = []
                }

                newRelations[selector]!.append(rel)
            }
        }

        for (key: selector, value: relations) in newRelations {
            self.relationshipsByLanguage[selector] = mergeRelationships(self.relationshipsByLanguage[selector, default: []], relations)
        }
    }

    /// Merge the given lists of ``SymbolGraph/Relationship``s.
    ///
    /// This function will deduplicate relationships based on their source, target, and kind. If it sees a duplicate, it will keep the first one it sees.
    func mergeRelationships(_ relationsList: [SymbolGraph.Relationship]...)
    -> [SymbolGraph.Relationship] {
        struct RelationKey: Hashable {
            let source: String
            let target: String
            let kind: SymbolGraph.Relationship.Kind

            init(fromRelation relationship: SymbolGraph.Relationship) {
                self.source = relationship.source
                self.target = relationship.target
                self.kind = relationship.kind
            }

            static func makePair(fromRelation relationship: SymbolGraph.Relationship) -> (RelationKey, SymbolGraph.Relationship) {
                return (RelationKey(fromRelation: relationship), relationship)
            }
        }

        let allRelations = relationsList.joined()

        // deduplicate the combined relationships array by source/target/kind
        // FIXME: Actually merge relationships if they have different mixins (rdar://84267943)
        let map = [:].merging(allRelations.map({ RelationKey.makePair(fromRelation: $0) }), uniquingKeysWith: { r1, r2 in r1 })

        return Array(map.values)
    }

    /// Scans over ``orphanRelationships`` and sorts any whose source/target symbols were loaded
    /// after the relationship was.
    ///
    /// Since relationships are added to ``relationshipsByLanguage`` based on what symbols are
    /// available when the relationship is being loaded, a relationship can be considered an
    /// "orphan" even when it's not, if the symbol graphs are loaded in a certain order. This
    /// method was added to ensure that these relationships can be properly assigned a language
    /// even if the symbol information isn't in the same symbol graph.
    internal func collectOrphans() {
        var newRelations: [Selector: [SymbolGraph.Relationship]] = [:]
        var remainingOrphans: [SymbolGraph.Relationship] = []
        for rel in self.orphanRelationships {
            let selectors: [Selector]
            if let unifiedSourceSym = self.symbols[rel.source] {
                selectors = unifiedSourceSym.mainGraphSelectors
            } else if let unifiedSourceSym = self.symbols[rel.target] {
                selectors = unifiedSourceSym.mainGraphSelectors
            } else {
                remainingOrphans.append(rel)
                continue
            }

            for selector in selectors {
                if !newRelations.keys.contains(selector) {
                    newRelations[selector] = []
                }

                newRelations[selector]!.append(rel)
            }
        }

        for (key: selector, value: relations) in newRelations {
            self.relationshipsByLanguage[selector] = mergeRelationships(self.relationshipsByLanguage[selector, default: []], relations)
        }

        self.orphanRelationships = remainingOrphans
    }

    /// If there were overload groups created by ``SymbolGraph/createOverloadGroupSymbols()``,
    /// recalculate their ``SymbolGraph/Symbol/OverloadData`` and store it in
    /// ``UnifiedSymbolGraph/Symbol/unifiedMixins``.
    internal func combineOverloadGroups() {
        // There are three main situations where a symbol's overload data would be different:
        //
        // 1. A symbol is not overloaded in one symbol graph, but is overloaded by a sibling symbol
        //    in another symbol graph.
        // 2. A group of overloads contains a different set of symbols between different symbol
        //    graphs, but the same symbol was chosen to be cloned as the overload group symbol.
        // 3. A group of overloads contains a different set of symbols between different symbol
        //    graphs, but different symbols were chosen to be cloned as their respective overload
        //    group symbols.
        //
        // In situation 1, the unified relationships match the most complete picture of all the
        // overloads (in that there is an overload group that exists and there are relationships
        // that point to it), but the symbol that is in both graphs only has an OverloadData mixin
        // connected to the selector for one symbol graph. Depending on how the mixins are loaded
        // later, this could lead to an inconsistent view where an `overloadOf` relationship exists,
        // but the client doesn't load the matching OverloadData mixin. In this case, we should
        // effectively promote the OverloadData to the whole-graph scope.
        //
        // In situation 2, we have symbols that look roughly like this:
        //
        // ┌───────────────────────────┐  ┌───────────────────────────┐
        // │ overloadA (1) ─┬─► groupA │  │ overloadA (1) ─┬─► groupA │
        // │ overloadB (2) ─┤          │  │ overloadC (2) ─┤          │
        // │ overloadC (3) ─┘          │  │ overloadD (3) ─┘          │
        // └───────────────────────────┘  └───────────────────────────┘
        //
        // The unified relationships are still consistent (there's one overload-group symbol and all
        // the overloaded symbols point to it), but `overloadC` has different OverloadData depending
        // on which selector you're using: In one graph it's at index 3, but in the other graph it's
        // at index 2. In this case we need to recalculate the overload indices based on an ordering
        // that takes all of the overloaded symbols into account.
        //
        // Finally, in situation 3 the relationship graph looks like this:
        //
        // ┌───────────────────────────────────────────┐
        // │                 overloadA (1) ─┬─► groupA │
        // │ groupB ◄─┬─ (1) overloadB (2) ─┘          │
        // │          └─ (2) overloadC                 │
        // └───────────────────────────────────────────┘
        //
        // In addition to overloadB having different indices per graph, the unified relationships
        // are also inconsistent: there are two overload groups that refer to different subsets of
        // symbols! In this case, we need to recalculate the overload indices like in situation 2
        // above, but we also need to determine which overload group to keep as the "canonical" one.
        // Since overload group symbols are by definition going to be generated by SymbolKit, when
        // they're introduced into a unified symbol graph we can remove any that don't describe a
        // "unified overload group" in this way, along with any relationships that point to them.
        //
        // In all three situations, we can generalize the solution with the following steps:
        //
        // 1. For each overload group, check the constituent symbols to see if they're part of
        //    another overload group. Collect the symbols and groups as you go.
        //    a. If you find other overload groups, recursively check for other overload groups
        //       until no new groups are found.
        // 2. Sort the collected symbols with the same comparator as
        //    `SymbolGraph.createOverloadGroupSymbols()`.
        // 3. Find the overload group symbol matching the first overload in the resulting order, and
        //    ensure that it contains `overloadOf` relationships to all the overloaded symbols.
        // 4. For each symbol, create a new OverloadData instance referencing the unified overload
        //    group and the computed index in the sort order. Save it in the symbol's `unifiedMixins`.
        // 5. If there were other overload group symbols, remove them from the symbol graph, along
        //    with any relationships that referenced them.

        /// A mapping of overload group symbols to their individual overloads.
        var overloadGroups: [String: Set<String>] = [:]
        /// A mapping of overloaded symbols to the overload groups they are part of.
        var overloadGroupsBySymbol: [String: Set<String>] = [:]
        /// A collection of all the overload groups that have been processed.
        var processedOverloadGroups: Set<String> = []

        for relationship in self.relationshipsByLanguage.flatMap(\.value)
            where relationship.kind == SymbolGraph.Relationship.Kind.overloadOf
        {
            overloadGroups[relationship.target, default: []].insert(relationship.source)
            overloadGroupsBySymbol[relationship.source, default: []].insert(relationship.target)
        }

        for overloadGroup in overloadGroups.keys {
            guard !processedOverloadGroups.contains(overloadGroup) else {
                // If this overload group was a sibling to another overload group, don't try to
                // process it again.
                continue
            }
            var siblingOverloadGroups: Set<String> = []
            var overloadedSymbolIdentifiers: Set<String> = []
            var pendingOverloadGroups: Set<String> = [overloadGroup]

            // 1. Collect all the overloaded symbols and all the overload groups they belong to.
            while let processingOverloadGroup = pendingOverloadGroups.popFirst() {
                pendingOverloadGroups.remove(processingOverloadGroup)
                siblingOverloadGroups.insert(processingOverloadGroup)

                for overloadedSymbol in overloadGroups[processingOverloadGroup]! {
                    overloadedSymbolIdentifiers.insert(overloadedSymbol)
                    for otherOverloadGroup in overloadGroupsBySymbol[overloadedSymbol]! where !siblingOverloadGroups.contains(otherOverloadGroup) {
                            pendingOverloadGroups.insert(otherOverloadGroup)
                    }
                }
            }

            guard !overloadedSymbolIdentifiers.isEmpty else { continue }

            // 2. Sort the overloaded symbols with the same comparator as `SymbolGraph.createOverloadGroupSymbols()`.
            let overloadedSymbols = overloadedSymbolIdentifiers.map({ symbols[$0]! })
            let allSymbolsHaveDeclaration = overloadedSymbols.allSatisfy({ !$0.declarationFragments.isEmpty })
            let sortedOverloads = overloadedSymbols.sorted(by: Symbol.sortForOverloads(orderByDeclaration: allSymbolsHaveDeclaration))
            let firstOverload = sortedOverloads.first!
            let computedOverloadGroup = firstOverload.uniqueIdentifier + SymbolGraph.Symbol.overloadGroupIdentifierSuffix

            guard siblingOverloadGroups.contains(computedOverloadGroup) else {
                assertionFailure(
                """
                UnifiedSymbolGraph.combineOverloadGroups found a candidate overload group that didn't
                match any existing overload group from its constituent symbol graphs.

                The unified graph found that \(firstOverload.uniqueIdentifier) should be the default
                overload, but the constituent symbol graphs contained the following overload groups:
                \(siblingOverloadGroups)
                """
                )
                continue
            }

            self.overloadGroupSymbols.insert(computedOverloadGroup)

            for overloadIndex in sortedOverloads.indices {
                let overloadedSymbol = sortedOverloads[overloadIndex]

                // 4. Create a new OverloadData instance and save it to the overloaded symbol.
                let overloadData = SymbolGraph.Symbol.OverloadData(
                    overloadGroupIdentifier: computedOverloadGroup,
                    overloadGroupIndex: overloadIndex)
                overloadedSymbol.unifiedMixins[SymbolGraph.Symbol.OverloadData.mixinKey] = overloadData
            }

            // 5. If there were any other overload groups that pointed to the same symbols, remove
            // them from the unified graph, along with any relationships that included them.
            processedOverloadGroups.formUnion(siblingOverloadGroups)
            siblingOverloadGroups.remove(computedOverloadGroup)
            for siblingOverloadGroup in siblingOverloadGroups {
                symbols.removeValue(forKey: siblingOverloadGroup)
            }

            // Performance optimization: The relationshipsByLanguage listings can become quite large
            // in some situations, so iterating over it to check membership and remove extraneous
            // relationships can become quite expensive. This loop intends to perform all the checks
            // and modifications we need on the relationships in a single iteration, by pre-caching
            // the things we need to check and inspecting the relationships as we go.

            /// The overloaded symbols that exist in a given selector.
            let overloadsPerSelector: [Selector: Set<String>] = overloadedSymbols.reduce(into: [:], { acc, symbol in
                for selector in symbol.allSelectors {
                    acc[selector, default: []].insert(symbol.uniqueIdentifier)
                }
            })
            for (selector, relationships) in relationshipsByLanguage {
                guard var selectorOverloads = overloadsPerSelector[selector] else {
                    // If there were no overloads that matched this selector, then we can assume
                    // that no overload groups exist in this selector either. There is no need to
                    // continue iterating the relationships here.
                    continue
                }

                relationshipsByLanguage[selector] = relationships.filter({ relationship in
                    // `filter` is already iterating over the collection anyway, so while we're
                    // scanning for other overload groups' relationships to remove, also check to
                    // ensure that all our overloads have relationships to the overload group.
                    if relationship.target == computedOverloadGroup,
                       relationship.kind == .overloadOf {
                        selectorOverloads.remove(relationship.source)
                    }

                    return !siblingOverloadGroups.contains(relationship.source) &&
                        !siblingOverloadGroups.contains(relationship.target)
                })

                // 3. Ensure that all the overloaded symbols have `overloadOf` relationships pointing
                // to the computed overload group.
                relationshipsByLanguage[selector]?.append(contentsOf: selectorOverloads.map({ overloadID in
                        .init(
                            source: overloadID,
                            target: computedOverloadGroup,
                            kind: .overloadOf,
                            targetFallback: nil)
                }))
            }
        }
    }

    /// Merge the given symbol graph with this one.
    public func mergeGraph(graph: SymbolGraph, at url: URL) {
        let (_, isMainGraph) = GraphCollector.moduleNameFor(graph, at: url)

        self.metadata[url] = graph.metadata
        self.moduleData[url] = graph.module

        for (key: precise, value: sym) in graph.symbols {
            if sym.identifier.precise.hasSuffix(SymbolGraph.Symbol.overloadGroupIdentifierSuffix) {
                self.overloadGroupsFromOriginalGraphs.insert(sym.identifier.precise)
            }
            if let existingSymbol = self.symbols[precise] {
                existingSymbol.mergeSymbol(symbol: sym, module: graph.module, isMainGraph: isMainGraph)
            } else {
                self.symbols[precise] = Symbol(fromSingleSymbol: sym, module: graph.module, isMainGraph: isMainGraph)
            }
        }

        loadRelationships(fromGraph: graph)
    }
}

extension UnifiedSymbolGraph {
    /// A combination of interface language and list of platforms that allows a symbol to be distinguished from another when unifying symbol graphs.
    public struct Selector: Equatable, Hashable, Encodable {
        /// The interface language used for the symbol.
        public let interfaceLanguage: String

        /// The platform that the symbol was built for.
        ///
        /// If the symbol graph that the symbol was sourced from does not contain a `module.platform.operatingSystem`, this will be `nil`.
        public let platform: String?

        public init(interfaceLanguage: String, platform: String?) {
            self.interfaceLanguage = interfaceLanguage
            self.platform = platform
        }

        /// Creates a ``UnifiedSymbolGraph/Selector`` for the given symbol graph's language and platform.
        ///
        /// If `graph` has no symbols, this will return `nil`.
        public init?(forSymbolGraph graph: SymbolGraph) {
            guard let lang = graph.symbols.first?.value.identifier.interfaceLanguage else { return nil }

            self.interfaceLanguage = lang
            self.platform = graph.module.platform.name
        }
    }
}