Skip to content

Commit 4c245d4

Browse files
add a separate overload-group calculation in the unified graph collector (#81)
1 parent 11dd0fa commit 4c245d4

File tree

7 files changed

+1111
-370
lines changed

7 files changed

+1111
-370
lines changed

Sources/SymbolKit/SymbolGraph/Symbol/DeclarationFragments/DeclarationFragments+Simplify.swift

Lines changed: 86 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -28,71 +28,103 @@ fileprivate extension UnifiedSymbolGraph.Symbol {
2828
}
2929
}
3030

31-
internal extension SymbolGraph.Symbol {
32-
func overloadSubheadingFragments() -> [DeclarationFragments.Fragment]? {
33-
guard let sourceFragments = self.declarationFragments ?? self.names.subHeading ?? self.names.navigator, !sourceFragments.isEmpty else {
34-
return nil
35-
}
31+
private func overloadFragments(
32+
declarationFragments: [SymbolGraph.Symbol.DeclarationFragments.Fragment]?,
33+
subHeading: [SymbolGraph.Symbol.DeclarationFragments.Fragment]?,
34+
navigator: [SymbolGraph.Symbol.DeclarationFragments.Fragment]?,
35+
functionSignature: SymbolGraph.Symbol.FunctionSignature?
36+
) -> [SymbolGraph.Symbol.DeclarationFragments.Fragment]? {
37+
guard let sourceFragments = declarationFragments ?? subHeading ?? navigator, !sourceFragments.isEmpty else {
38+
return nil
39+
}
3640

37-
var simplifiedFragments = [DeclarationFragments.Fragment]()
41+
var simplifiedFragments: [SymbolGraph.Symbol.DeclarationFragments.Fragment] = []
3842

39-
// In Swift, methods have a keyword as their first token; if the declaration follows that
40-
// pattern then pull that out
43+
// In Swift, methods have a keyword as their first token; if the declaration follows that
44+
// pattern then pull that out
4145

42-
// Sometimes symbols are decorated with attributes or extra keywords in the full declaration.
43-
// In this case, the sub-heading declaration doesn't include those decorations, so pull that
44-
// keyword if it exists
45-
if let firstFragment = self.names.subHeading?.first, firstFragment.kind == .keyword {
46-
simplifiedFragments.append(firstFragment)
47-
} else if let firstFragment = sourceFragments.first(where: { $0.kind != .attribute && $0.kind != .text }), firstFragment.kind == .keyword {
48-
// If we only have full declaration fragments, still try to skip a leading attribute if possible
49-
simplifiedFragments.append(firstFragment)
50-
}
46+
// Sometimes symbols are decorated with attributes or extra keywords in the full declaration.
47+
// In this case, the sub-heading declaration doesn't include those decorations, so pull that
48+
// keyword if it exists
49+
if let firstFragment = subHeading?.first, firstFragment.kind == .keyword {
50+
simplifiedFragments.append(firstFragment)
51+
} else if let firstFragment = sourceFragments.first(where: { $0.kind != .attribute && $0.kind != .text }), firstFragment.kind == .keyword {
52+
// If we only have full declaration fragments, still try to skip a leading attribute if possible
53+
simplifiedFragments.append(firstFragment)
54+
}
5155

52-
// Then, look for the first identifier, which should contain the symbol's name, and add that
53-
if let firstIdentifier = sourceFragments.first(where: { $0.kind == .identifier }) {
54-
if !simplifiedFragments.isEmpty {
55-
simplifiedFragments.append(.init(textFragment: " "))
56-
}
57-
simplifiedFragments.append(firstIdentifier)
56+
// Then, look for the first identifier, which should contain the symbol's name, and add that
57+
if let firstIdentifier = sourceFragments.first(where: { $0.kind == .identifier }) {
58+
if !simplifiedFragments.isEmpty {
59+
simplifiedFragments.append(.init(textFragment: " "))
5860
}
61+
simplifiedFragments.append(firstIdentifier)
62+
}
5963

60-
// Assumption: All symbols that can be considered "overloads" are written with method
61-
// syntax, including a list of arguments surrounded by parentheses. In Swift symbol graphs,
62-
// method parameters are included in the FunctionSignature mixin, so if that's present we
63-
// use that to parse the data out.
64-
65-
simplifiedFragments.append(.init(textFragment: "("))
66-
67-
if let functionSignature = self.functionSignature {
68-
for parameter in functionSignature.parameters {
69-
// Scan through the declaration fragments to see whether this parameter's name is
70-
// externally-facing or not.
71-
let fragment: SymbolGraph.Symbol.DeclarationFragments.Fragment
72-
let parameterName = parameter.externalName ?? parameter.name
73-
if let paramNameFragment = sourceFragments.first(where: { $0.spelling == parameterName && $0.kind == .externalParameter }) {
74-
fragment = paramNameFragment
75-
} else {
76-
// If not, then insert an underscore for this parameter.
77-
// FIXME: This is a Swift-centric assumption; change this if/when we support C++ overloads
78-
fragment = .init(kind: .externalParameter, spelling: "_", preciseIdentifier: nil)
79-
}
80-
simplifiedFragments.append(fragment)
81-
simplifiedFragments.append(.init(textFragment: ":"))
64+
// Assumption: All symbols that can be considered "overloads" are written with method
65+
// syntax, including a list of arguments surrounded by parentheses. In Swift symbol graphs,
66+
// method parameters are included in the FunctionSignature mixin, so if that's present we
67+
// use that to parse the data out.
68+
69+
simplifiedFragments.append(.init(textFragment: "("))
70+
71+
if let functionSignature = functionSignature {
72+
for parameter in functionSignature.parameters {
73+
// Scan through the declaration fragments to see whether this parameter's name is
74+
// externally-facing or not.
75+
let fragment: SymbolGraph.Symbol.DeclarationFragments.Fragment
76+
let parameterName = parameter.externalName ?? parameter.name
77+
if let paramNameFragment = sourceFragments.first(where: { $0.spelling == parameterName && $0.kind == .externalParameter }) {
78+
fragment = paramNameFragment
79+
} else {
80+
// If not, then insert an underscore for this parameter.
81+
// FIXME: This is a Swift-centric assumption; change this if/when we support C++ overloads
82+
fragment = .init(kind: .externalParameter, spelling: "_", preciseIdentifier: nil)
8283
}
83-
} else {
84-
let parameterFragments = sourceFragments.extractFunctionParameters()
85-
simplifiedFragments.append(contentsOf: parameterFragments)
84+
simplifiedFragments.append(fragment)
85+
simplifiedFragments.append(.init(textFragment: ":"))
8686
}
87+
} else {
88+
let parameterFragments = sourceFragments.extractFunctionParameters()
89+
simplifiedFragments.append(contentsOf: parameterFragments)
90+
}
91+
92+
if simplifiedFragments.last?.kind == .text, var lastFragment = simplifiedFragments.popLast() {
93+
lastFragment.spelling += ")"
94+
simplifiedFragments.append(lastFragment)
95+
} else {
96+
simplifiedFragments.append(.init(textFragment: ")"))
97+
}
8798

88-
if simplifiedFragments.last?.kind == .text, var lastFragment = simplifiedFragments.popLast() {
89-
lastFragment.spelling += ")"
90-
simplifiedFragments.append(lastFragment)
91-
} else {
92-
simplifiedFragments.append(.init(textFragment: ")"))
99+
return simplifiedFragments
100+
}
101+
102+
internal extension SymbolGraph.Symbol {
103+
func overloadSubheadingFragments() -> [DeclarationFragments.Fragment]? {
104+
return overloadFragments(
105+
declarationFragments: self.declarationFragments,
106+
subHeading: self.names.subHeading,
107+
navigator: self.names.navigator,
108+
functionSignature: self.functionSignature)
109+
}
110+
}
111+
112+
internal extension UnifiedSymbolGraph.Symbol {
113+
func overloadSubheadingFragments() -> [UnifiedSymbolGraph.Selector: [SymbolGraph.Symbol.DeclarationFragments.Fragment]] {
114+
var fragmentsMap: [UnifiedSymbolGraph.Selector: [SymbolGraph.Symbol.DeclarationFragments.Fragment]] = [:]
115+
116+
for selector in self.allSelectors {
117+
if let fragments = overloadFragments(
118+
declarationFragments: self.declarationFragments(selector: selector),
119+
subHeading: self.names[selector]?.subHeading,
120+
navigator: self.names[selector]?.navigator,
121+
functionSignature: self.functionSignature(selector: selector)
122+
) {
123+
fragmentsMap[selector] = fragments
124+
}
93125
}
94126

95-
return simplifiedFragments
127+
return fragmentsMap
96128
}
97129
}
98130

Sources/SymbolKit/SymbolGraph/SymbolGraph+Overloads.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ extension SymbolGraph {
3636
/// relationships are created between the colliding symbols and the new overload group symbol.
3737
/// In addition, any existing relationships the original symbol had are also cloned for the
3838
/// overload group.
39+
///
40+
/// > Warning: If you are processing this symbol graph into a ``GraphCollector``, using this
41+
/// > method can lead to incorrectly grouped overloads. Use the `createOverloadGroups`
42+
/// > parameter of ``GraphCollector/finishLoading(createOverloadGroups:)`` instead, which will
43+
/// > perform overload grouping over all the collected graphs.
3944
public mutating func createOverloadGroupSymbols() {
4045
struct OverloadKey: Hashable {
4146
let path: [String]

Sources/SymbolKit/UnifiedSymbolGraph/GraphCollector.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,25 @@ extension GraphCollector {
8888
}
8989
}
9090

91-
public func finishLoading() -> (unifiedGraphs: [String: UnifiedSymbolGraph], graphSources: [String: [GraphKind]]) {
91+
/// Finalizes the collected symbol graphs, loading in extension symbol graphs and processing orphan relationships.
92+
///
93+
/// - Parameter createOverloadGroups: Whether to create overload group symbols in the resulting unified symbol graphs.
94+
/// If overload groups were created in the individual symbol graphs, they will be automatically combined regardless of this setting.
95+
/// - Returns: A tuple containing a map of module names to unified symbol graphs, and a map of module names to symbol graph locations.
96+
public func finishLoading(
97+
createOverloadGroups: Bool = false
98+
) -> (unifiedGraphs: [String: UnifiedSymbolGraph], graphSources: [String: [GraphKind]]) {
9299
for (url, graph) in self.extensionGraphs {
93100
self.mergeSymbolGraph(graph, at: url, forceLoading: true)
94101
}
95102

96103
for (_, graph) in self.unifiedGraphs {
97104
graph.collectOrphans()
98-
graph.combineOverloadGroups()
105+
if createOverloadGroups {
106+
graph.createOverloadGroupSymbols()
107+
} else {
108+
graph.combineOverloadGroups()
109+
}
99110
}
100111

101112
return (self.unifiedGraphs, self.graphSources)

Sources/SymbolKit/UnifiedSymbolGraph/UnifiedSymbol.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,22 @@ extension UnifiedSymbolGraph {
6969
/// Like an individual symbol's mixins, the `String` key for each mixin is its ``Mixin/mixinKey``.
7070
public var unifiedMixins: [String: Mixin]
7171

72+
/// Initialize an empty symbol with the given identifier.
73+
init(uniqueIdentifier: String) {
74+
self.uniqueIdentifier = uniqueIdentifier
75+
self.mainGraphSelectors = []
76+
self.modules = [:]
77+
self.kind = [:]
78+
self.pathComponents = [:]
79+
self.type = nil
80+
self.names = [:]
81+
self.docComment = [:]
82+
self.accessLevel = [:]
83+
self.isVirtual = [:]
84+
self.mixins = [:]
85+
self.unifiedMixins = [:]
86+
}
87+
7288
/// Initialize a combined symbol view from a single symbol.
7389
public init(fromSingleSymbol sym: SymbolGraph.Symbol, module: SymbolGraph.Module, isMainGraph: Bool) {
7490
let lang = sym.identifier.interfaceLanguage
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
13+
/// A graph holding information about overloaded symbols in a ``UnifiedSymbolGraph``.
14+
class Overloads {
15+
var overloadGroups: [OverloadKey: [UnifiedSymbolGraph.Symbol]] = [:]
16+
17+
func calculateOverloadGroups(forSymbol symbol: UnifiedSymbolGraph.Symbol) {
18+
let interfaceLanguages = Set(symbol.allSelectors.map(\.interfaceLanguage))
19+
let keys = interfaceLanguages.compactMap({ OverloadKey(fromUnifiedSymbol: symbol, interfaceLanguage: $0) })
20+
21+
for key in keys where key.kind.isOverloadableKind {
22+
overloadGroups[key, default: []].append(symbol)
23+
}
24+
}
25+
}
26+
27+
extension Overloads {
28+
/// A key structure used to group overloaded symbols together.
29+
struct OverloadKey: Hashable {
30+
let interfaceLanguage: String
31+
let path: [String]
32+
let kind: SymbolGraph.Symbol.KindIdentifier
33+
34+
public init?(fromUnifiedSymbol symbol: UnifiedSymbolGraph.Symbol, interfaceLanguage: String) {
35+
let kinds = Set(symbol.kind.filter({ $0.key.interfaceLanguage == interfaceLanguage }).values.map(\.identifier))
36+
let paths = Set(symbol.pathComponents.filter({ $0.key.interfaceLanguage == interfaceLanguage }).values)
37+
38+
guard kinds.count == 1, paths.count == 1 else {
39+
// If a symbol has different paths or different symbol kinds in its constituent
40+
// symbol graphs, then it shouldn't participate in overloads.
41+
return nil
42+
}
43+
44+
self.interfaceLanguage = interfaceLanguage
45+
self.kind = kinds.first!
46+
self.path = paths.first!
47+
}
48+
}
49+
}
50+
51+
extension UnifiedSymbolGraph {
52+
func createOverloadGroupSymbols() {
53+
// If the individual symbol graphs had overload groups created, clear them from the symbols
54+
// and relationships listings before continuing.
55+
if !self.overloadGroupsFromOriginalGraphs.isEmpty {
56+
self.symbols = self.symbols.filter({ !self.overloadGroupsFromOriginalGraphs.contains($0.key) })
57+
self.relationshipsByLanguage = self.relationshipsByLanguage.mapValues({ relationships in
58+
relationships.filter({ relationship in
59+
!self.overloadGroupsFromOriginalGraphs.contains(relationship.source) &&
60+
!self.overloadGroupsFromOriginalGraphs.contains(relationship.target) &&
61+
relationship.kind != .overloadOf
62+
})
63+
})
64+
}
65+
66+
let defaultImplementationSymbols = Set(relationshipsByLanguage.values.flatMap({
67+
$0.filter({ $0.kind == .defaultImplementationOf }).map(\.source)
68+
}))
69+
70+
// Calculate all the overload keys for the remaining symbols.
71+
let overloadData = Overloads()
72+
for (identifier, symbol) in symbols where !defaultImplementationSymbols.contains(identifier) {
73+
overloadData.calculateOverloadGroups(forSymbol: symbol)
74+
}
75+
76+
for (overloadKey, var overloadedSymbols) in overloadData.overloadGroups where overloadedSymbols.count > 1 {
77+
overloadedSymbols.sort(by: Symbol.sortForOverloads(
78+
orderByDeclaration: overloadedSymbols.allSatisfy({
79+
!$0.declarationFragments.isEmpty
80+
})))
81+
82+
let firstOverload = overloadedSymbols.first!
83+
let overloadGroupIdentifier = firstOverload.uniqueIdentifier + SymbolGraph.Symbol.overloadGroupIdentifierSuffix
84+
let overloadGroupSymbol = Symbol(
85+
cloning: firstOverload,
86+
uniqueIdentifier: overloadGroupIdentifier,
87+
withSelectorsFromSourceLanguage: overloadKey.interfaceLanguage)
88+
89+
overloadGroupSymbol.isVirtual = overloadGroupSymbol.isVirtual.mapValues({ _ in true })
90+
91+
for (selector, simplifiedDeclaration) in overloadGroupSymbol.overloadSubheadingFragments() {
92+
overloadGroupSymbol.names[selector]?.navigator = simplifiedDeclaration
93+
overloadGroupSymbol.names[selector]?.subHeading = simplifiedDeclaration
94+
}
95+
96+
self.symbols[overloadGroupIdentifier] = overloadGroupSymbol
97+
self.overloadGroupSymbols.insert(overloadGroupIdentifier)
98+
99+
var overloadSelectors: Set<Selector> = []
100+
for overloadedSymbol in overloadedSymbols {
101+
overloadSelectors.formUnion(overloadedSymbol.selectors(forLanguage: overloadKey.interfaceLanguage))
102+
}
103+
104+
// Clone the relationships from the first overload and add them to the overload group
105+
for selector in overloadSelectors {
106+
var newRelationships = (relationshipsByLanguage[selector] ?? []).filter({
107+
$0.target == firstOverload.uniqueIdentifier || $0.source == firstOverload.uniqueIdentifier
108+
}).map({ relationship in
109+
var relationship = relationship
110+
if relationship.source == firstOverload.uniqueIdentifier {
111+
relationship.source = overloadGroupIdentifier
112+
} else if relationship.target == firstOverload.uniqueIdentifier {
113+
relationship.target = overloadGroupIdentifier
114+
}
115+
return relationship
116+
})
117+
118+
// Add in new relationships from the overloaded symbols to the overload group
119+
for symbol in overloadedSymbols where symbol.allSelectors.contains(selector) {
120+
newRelationships.append(.init(
121+
source: symbol.uniqueIdentifier,
122+
target: overloadGroupIdentifier,
123+
kind: .overloadOf,
124+
targetFallback: nil))
125+
}
126+
127+
if !newRelationships.isEmpty {
128+
relationshipsByLanguage[selector, default: []].append(contentsOf: newRelationships)
129+
}
130+
}
131+
132+
for overloadIndex in overloadedSymbols.indices {
133+
let overloadedSymbol = overloadedSymbols[overloadIndex]
134+
135+
overloadedSymbol.unifiedMixins[SymbolGraph.Symbol.OverloadData.mixinKey] =
136+
SymbolGraph.Symbol.OverloadData(
137+
overloadGroupIdentifier: overloadGroupIdentifier,
138+
overloadGroupIndex: overloadIndex)
139+
}
140+
}
141+
}
142+
}
143+
144+
extension UnifiedSymbolGraph.Symbol {
145+
convenience init(
146+
cloning original: UnifiedSymbolGraph.Symbol,
147+
uniqueIdentifier: String,
148+
withSelectorsFromSourceLanguage sourceLanguage: String? = nil
149+
) {
150+
self.init(uniqueIdentifier: uniqueIdentifier)
151+
152+
let selectors: [UnifiedSymbolGraph.Selector]
153+
if let sourceLanguage = sourceLanguage {
154+
selectors = original.selectors(forLanguage: sourceLanguage)
155+
} else {
156+
selectors = original.allSelectors
157+
}
158+
159+
self.mainGraphSelectors = original.mainGraphSelectors.filter({ selectors.contains($0) })
160+
161+
self.modules = original.modules.filter({ selectors.contains($0.key) })
162+
self.kind = original.kind.filter({ selectors.contains($0.key) })
163+
self.pathComponents = original.pathComponents.filter({ selectors.contains($0.key) })
164+
self.type = original.type
165+
self.names = original.names.filter({ selectors.contains($0.key) })
166+
self.docComment = original.docComment.filter({ selectors.contains($0.key) })
167+
self.accessLevel = original.accessLevel.filter({ selectors.contains($0.key) })
168+
self.isVirtual = original.isVirtual.filter({ selectors.contains($0.key) })
169+
self.mixins = original.mixins.filter({ selectors.contains($0.key) })
170+
}
171+
172+
func selectors(forLanguage language: String) -> [UnifiedSymbolGraph.Selector] {
173+
self.allSelectors.filter({ $0.interfaceLanguage == language })
174+
}
175+
}

0 commit comments

Comments
 (0)