Skip to content

Commit 2f7b3aa

Browse files
authored
Make it easier to store other types of link disambiguating information in the path hierarchy (#828)
* Internally store disambiguated path hierarchy elements as small lists * Continue to disfavor sparse nodes without disambiguation * Expand on internal comments to describe more implementation details
1 parent 0f9187a commit 2f7b3aa

File tree

5 files changed

+187
-150
lines changed

5 files changed

+187
-150
lines changed

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

Lines changed: 57 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,11 @@ extension PathHierarchy {
112112
// When descending through placeholder nodes, we trust that the known disambiguation
113113
// that they were created with is necessary.
114114
var knownDisambiguation = ""
115-
let (kind, subtree) = tree.storage.first!
116-
if kind != "_" {
115+
let element = tree.storage.first!
116+
if let kind = element.kind {
117117
knownDisambiguation += "-\(kind)"
118118
}
119-
let hash = subtree.keys.first!
120-
if hash != "_" {
119+
if let hash = element.hash {
121120
knownDisambiguation += "-\(hash)"
122121
}
123122
path = accumulatedPath + "/" + nameTransform(node.name) + knownDisambiguation
@@ -170,33 +169,45 @@ extension PathHierarchy {
170169
}
171170

172171
extension PathHierarchy.DisambiguationContainer {
172+
173+
static func disambiguatedValues<E: Sequence>(
174+
for elements: E,
175+
includeLanguage: Bool = false
176+
) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] where E.Element == Element {
177+
var collisions: [(value: PathHierarchy.Node, disambiguation: Disambiguation)] = []
178+
179+
var remainingIDs = Set(elements.map(\.node.identifier))
180+
181+
// Kind disambiguation is the most readable, so we start by checking if any element has a unique kind.
182+
let groupedByKind = [String?: [Element]](grouping: elements, by: \.kind)
183+
for (kind, elements) in groupedByKind where elements.count == 1 && kind != nil {
184+
let element = elements.first!
185+
if includeLanguage, let symbol = element.node.symbol {
186+
collisions.append((value: element.node, disambiguation: .kind("\(SourceLanguage(id: symbol.identifier.interfaceLanguage).linkDisambiguationID).\(kind!)")))
187+
} else {
188+
collisions.append((value: element.node, disambiguation: .kind(kind!)))
189+
}
190+
remainingIDs.remove(element.node.identifier)
191+
}
192+
if remainingIDs.isEmpty {
193+
return collisions
194+
}
195+
196+
for element in elements where remainingIDs.contains(element.node.identifier) {
197+
collisions.append((value: element.node, disambiguation: element.hash.map { .hash($0) } ?? .none))
198+
}
199+
return collisions
200+
}
201+
173202
/// Returns all values paired with their disambiguation suffixes.
174203
///
175204
/// - Parameter includeLanguage: Whether or not the kind disambiguation information should include the language, for example: "swift".
176205
func disambiguatedValues(includeLanguage: Bool = false) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] {
177206
if storage.count == 1 {
178-
let tree = storage.values.first!
179-
if tree.count == 1 {
180-
return [(tree.values.first!, .none)]
181-
}
207+
return [(storage.first!.node, .none)]
182208
}
183209

184-
var collisions: [(value: PathHierarchy.Node, disambiguation: Disambiguation)] = []
185-
for (kind, kindTree) in storage {
186-
if kindTree.count == 1 {
187-
// No other match has this kind
188-
if includeLanguage, let symbol = kindTree.first!.value.symbol {
189-
collisions.append((value: kindTree.first!.value, disambiguation: .kind("\(SourceLanguage(id: symbol.identifier.interfaceLanguage).linkDisambiguationID).\(kind)")))
190-
} else {
191-
collisions.append((value: kindTree.first!.value, disambiguation: .kind(kind)))
192-
}
193-
continue
194-
}
195-
for (usr, value) in kindTree {
196-
collisions.append((value: value, disambiguation: .hash(usr)))
197-
}
198-
}
199-
return collisions
210+
return Self.disambiguatedValues(for: storage, includeLanguage: includeLanguage)
200211
}
201212

202213
/// Returns all values paired with their disambiguation suffixes without needing to disambiguate between two different versions of the same symbol.
@@ -205,31 +216,29 @@ extension PathHierarchy.DisambiguationContainer {
205216
func disambiguatedValuesWithCollapsedUniqueSymbols(includeLanguage: Bool) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] {
206217
typealias DisambiguationPair = (String, String)
207218

208-
var uniqueSymbolIDs = [String: [DisambiguationPair]]()
209-
var nonSymbols = [DisambiguationPair]()
210-
for (kind, kindTree) in storage {
211-
for (hash, value) in kindTree {
212-
guard let symbol = value.symbol else {
213-
nonSymbols.append((kind, hash))
214-
continue
215-
}
216-
if symbol.identifier.interfaceLanguage == "swift" {
217-
uniqueSymbolIDs[symbol.identifier.precise, default: []].insert((kind, hash), at: 0)
218-
} else {
219-
uniqueSymbolIDs[symbol.identifier.precise, default: []].append((kind, hash))
220-
}
219+
var uniqueSymbolIDs = [String: [Element]]()
220+
var nonSymbols = [Element]()
221+
for element in storage {
222+
guard let symbol = element.node.symbol else {
223+
nonSymbols.append(element)
224+
continue
225+
}
226+
if symbol.identifier.interfaceLanguage == "swift" {
227+
uniqueSymbolIDs[symbol.identifier.precise, default: []].insert(element, at: 0)
228+
} else {
229+
uniqueSymbolIDs[symbol.identifier.precise, default: []].append(element)
221230
}
222231
}
223232

224-
var duplicateSymbols = [String: ArraySlice<DisambiguationPair>]()
233+
var duplicateSymbols = [String: ArraySlice<Element>]()
225234

226-
var new = Self()
227-
for (kind, hash) in nonSymbols {
228-
new.add(kind, hash, storage[kind]![hash]!)
235+
var new = PathHierarchy.DisambiguationContainer()
236+
for element in nonSymbols {
237+
new.add(element.node, kind: element.kind, hash: element.hash)
229238
}
230239
for (id, symbolDisambiguations) in uniqueSymbolIDs {
231-
let (kind, hash) = symbolDisambiguations[0]
232-
new.add(kind, hash, storage[kind]![hash]!)
240+
let element = symbolDisambiguations.first!
241+
new.add(element.node, kind: element.kind, hash: element.hash)
233242

234243
if symbolDisambiguations.count > 1 {
235244
duplicateSymbols[id] = symbolDisambiguations.dropFirst()
@@ -243,8 +252,8 @@ extension PathHierarchy.DisambiguationContainer {
243252

244253
for (id, disambiguations) in duplicateSymbols {
245254
let primaryDisambiguation = disambiguated.first(where: { $0.value.symbol?.identifier.precise == id })!.disambiguation
246-
for (kind, hash) in disambiguations {
247-
disambiguated.append((storage[kind]![hash]!, primaryDisambiguation.updated(kind: kind, hash: hash)))
255+
for element in disambiguations {
256+
disambiguated.append((element.node, primaryDisambiguation.updated(kind: element.kind, hash: element.hash)))
248257
}
249258
}
250259

@@ -280,14 +289,14 @@ extension PathHierarchy.DisambiguationContainer {
280289
}
281290

282291
/// Creates a new disambiguation with a new kind or hash value.
283-
func updated(kind: String, hash: String) -> Self {
292+
func updated(kind: String?, hash: String?) -> Self {
284293
switch self {
285294
case .none:
286295
return .none
287296
case .kind:
288-
return .kind(kind)
297+
return kind.map { .kind($0) } ?? self
289298
case .hash:
290-
return .hash(hash)
299+
return hash.map { .hash($0) } ?? self
291300
}
292301
}
293302
}

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

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

77
See https://swift.org/LICENSE.txt for license information
@@ -23,17 +23,17 @@ private extension PathHierarchy.Node {
2323
// Each node is printed as 3-layer hierarchy with the child names, their kind disambiguation, and their hash disambiguation.
2424
return DumpableNode(
2525
name: symbol.map { "{ \($0.identifier.precise) : \($0.identifier.interfaceLanguage).\($0.kind.identifier.identifier) }" } ?? "[ \(name) ]",
26-
children:
27-
children.sorted(by: \.key).map { (key, disambiguationTree) -> DumpableNode in
28-
DumpableNode(
26+
children: children.sorted(by: \.key).map { (key, disambiguationTree) -> DumpableNode in
27+
let grouped = [String: [PathHierarchy.DisambiguationContainer.Element]](grouping: disambiguationTree.storage, by: { $0.kind ?? "_" })
28+
return DumpableNode(
2929
name: key,
30-
children: disambiguationTree.storage.sorted(by: \.key).map { (kind, kindTree) -> DumpableNode in
30+
children: grouped.sorted(by: \.key).map { (kind, kindTree) -> DumpableNode in
3131
DumpableNode(
3232
name: kind,
33-
children: kindTree.sorted(by: \.key).map { (usr, node) -> DumpableNode in
33+
children: kindTree.sorted(by: { lhs, rhs in (lhs.hash ?? "_") < (rhs.hash ?? "_") }).map { (element) -> DumpableNode in
3434
DumpableNode(
35-
name: usr,
36-
children: [node.dumpableNode()]
35+
name: element.hash ?? "_",
36+
children: [element.node.dumpableNode()]
3737
)
3838
}
3939
)

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

Lines changed: 30 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -410,49 +410,33 @@ extension PathHierarchy.DisambiguationContainer {
410410
/// - Exactly one match is found; indicated by a non-nil return value.
411411
/// - More than one match is found; indicated by a raised error listing the matches and their missing disambiguation.
412412
func find(_ disambiguation: PathHierarchy.PathComponent.Disambiguation?) throws -> PathHierarchy.Node? {
413-
var kind: String?
414-
var hash: String?
415-
switch disambiguation {
416-
case .kindAndHash(kind: let maybeKind, hash: let maybeHash):
417-
kind = maybeKind.map(String.init)
418-
hash = maybeHash.map(String.init)
419-
case nil:
420-
kind = nil
421-
hash = nil
413+
if storage.count <= 1, disambiguation == nil {
414+
return storage.first?.node
422415
}
423416

424-
if let kind = kind {
425-
// Need to match the provided kind
426-
guard let subtree = storage[kind] else { return nil }
427-
if let hash = hash {
428-
return subtree[hash]
429-
} else if subtree.count == 1 {
430-
return subtree.values.first
431-
} else {
432-
// Subtree contains more than one match.
433-
throw Error.lookupCollision(subtree.map { ($0.value, $0.key) })
434-
}
435-
} else if storage.count == 1, let subtree = storage.values.first {
436-
// Tree only contains one kind subtree
437-
if let hash = hash {
438-
return subtree[hash]
439-
} else if subtree.count == 1 {
440-
return subtree.values.first
441-
} else {
442-
// Subtree contains more than one match.
443-
throw Error.lookupCollision(subtree.map { ($0.value, $0.key) })
444-
}
445-
} else if let hash = hash {
446-
// Need to match the provided hash
447-
let kinds = storage.filter { $0.value.keys.contains(hash) }
448-
if kinds.isEmpty {
449-
return nil
450-
} else if kinds.count == 1 {
451-
return kinds.first!.value[hash]
452-
} else {
453-
// Subtree contains more than one match
454-
throw Error.lookupCollision(kinds.map { ($0.value[hash]!, $0.key) })
417+
switch disambiguation {
418+
case .kindAndHash(let kind, let hash):
419+
switch (kind, hash) {
420+
case (let kind?, let hash?):
421+
return storage.first(where: { $0.kind == kind && $0.hash == hash })?.node
422+
case (let kind?, nil):
423+
let matches = storage.filter({ $0.kind == kind })
424+
guard matches.count <= 1 else {
425+
// Suggest not only hash disambiguation, but also type signature disambiguation.
426+
throw Error.lookupCollision(Self.disambiguatedValues(for: matches).map { ($0.value, $0.disambiguation.value()) })
427+
}
428+
return matches.first?.node
429+
case (nil, let hash?):
430+
let matches = storage.filter({ $0.hash == hash })
431+
guard matches.count <= 1 else {
432+
throw Error.lookupCollision(matches.map { ($0.node, $0.kind!) }) // An element wouldn't match if it didn't have kind disambiguation.
433+
}
434+
return matches.first?.node
435+
case (nil, nil):
436+
break
455437
}
438+
case nil:
439+
break
456440
}
457441
// Disambiguate by a mix of kinds and USRs
458442
throw Error.lookupCollision(self.disambiguatedValues().map { ($0.value, $0.disambiguation.value()) })
@@ -461,6 +445,12 @@ extension PathHierarchy.DisambiguationContainer {
461445

462446
// MARK: Private helper extensions
463447

448+
// Allow optional substrings to be compared to non-optional strings
449+
private func == <S1: StringProtocol, S2: StringProtocol>(lhs: S1?, rhs: S2) -> Bool {
450+
guard let lhs = lhs else { return false }
451+
return lhs == rhs
452+
}
453+
464454
private extension Sequence {
465455
/// Returns the only element of the sequence that satisfies the given predicate.
466456
/// - Parameters:

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,9 @@ extension PathHierarchy.FileRepresentation {
4545
isDisfavoredInCollision: node.isDisfavoredInCollision,
4646
children: node.children.values.flatMap({ tree in
4747
var disambiguations = [Node.Disambiguation]()
48-
for (kind, kindTree) in tree.storage {
49-
for (hash, childNode) in kindTree where childNode.identifier != nil { // nodes without identifiers can't be found in the tree
50-
disambiguations.append(.init(kind: kind, hash: hash, nodeID: identifierMap[childNode.identifier]!))
48+
for element in tree.storage where element.node.identifier != nil { // nodes without identifiers can't be found in the tree
49+
disambiguations.append(.init(kind: element.kind, hash: element.hash, nodeID: identifierMap[element.node.identifier]!))
5150
}
52-
}
5351
return disambiguations
5452
}),
5553
symbolID: node.symbol?.identifier
@@ -101,7 +99,7 @@ extension PathHierarchy {
10199
/// The container of tutorial overview pages.
102100
var tutorialOverviewContainer: Int
103101

104-
/// A node in the
102+
/// A node in the hierarchy.
105103
struct Node: Codable {
106104
var name: String
107105
var isDisfavoredInCollision: Bool = false

0 commit comments

Comments
 (0)