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
}
}
}
|