Skip to content

Commit f0a3d7a

Browse files
authored
Make it easier to support other link disambiguation in the future (#824)
* Make it easier to support other types of disambiguation in the future * Don't globally register the extended symbol kinds
1 parent 32980ce commit f0a3d7a

File tree

5 files changed

+102
-83
lines changed

5 files changed

+102
-83
lines changed

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

Lines changed: 2 additions & 2 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
@@ -169,7 +169,7 @@ extension PathHierarchy.Error {
169169
// Use the authored disambiguation to try and reduce the possible near misses. For example, if the link was disambiguated with `-struct` we should
170170
// only make suggestions for similarly spelled structs.
171171
let filteredNearMisses = nearMisses.filter { name in
172-
(try? partialResult.node.children[name]?.find(nextPathComponent.kind.map(String.init), nextPathComponent.hash.map(String.init))) != nil
172+
(try? partialResult.node.children[name]?.find(nextPathComponent.disambiguation)) != nil
173173
}
174174

175175
let pathPrefix = partialResult.pathPrefix

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

Lines changed: 22 additions & 19 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
@@ -206,7 +206,7 @@ extension PathHierarchy {
206206
let (children, pathComponent) = try findChildContainer(node: &node, remaining: remaining, rawPathForError: rawPathForError)
207207

208208
do {
209-
guard let child = try children.find(pathComponent.kind, pathComponent.hash) else {
209+
guard let child = try children.find(pathComponent.disambiguation) else {
210210
// The search has ended with a node that doesn't have a child matching the next path component.
211211
throw makePartialResultError(node: node, remaining: remaining, rawPathForError: rawPathForError)
212212
}
@@ -252,7 +252,7 @@ extension PathHierarchy {
252252
// Look ahead one path component to narrow down the list of collisions.
253253
// For each collision where the next path component can be found unambiguously, return that matching node one level down.
254254
let possibleMatchesOneLevelDown = collisions.compactMap {
255-
return try? $0.node.children[String(nextPathComponent.name)]?.find(nextPathComponent.kind, nextPathComponent.hash)
255+
return try? $0.node.children[String(nextPathComponent.name)]?.find(nextPathComponent.disambiguation)
256256
}
257257
let onlyPossibleMatch: Node?
258258

@@ -382,8 +382,7 @@ extension PathHierarchy {
382382
if let match = node.children[pathComponent.full] {
383383
// The path component parsing may treat dash separated words as disambiguation information.
384384
// If the parsed name didn't match, also try the original.
385-
pathComponent.kind = nil
386-
pathComponent.hash = nil
385+
pathComponent.disambiguation = nil
387386
return (match, pathComponent)
388387
} else if let match = node.children[String(pathComponent.name)] {
389388
return (match, pathComponent)
@@ -404,16 +403,24 @@ extension PathHierarchy.DisambiguationContainer {
404403
case lookupCollision([(node: PathHierarchy.Node, disambiguation: String)])
405404
}
406405

407-
fileprivate func find(_ kind: Substring?, _ hash: Substring?) throws -> PathHierarchy.Node? {
408-
try find(kind.map(String.init), hash.map(String.init))
409-
}
410406
/// Attempts to find a value in the disambiguation tree based on partial disambiguation information.
411407
///
412408
/// There are 3 possible results:
413409
/// - No match is found; indicated by a `nil` return value.
414410
/// - Exactly one match is found; indicated by a non-nil return value.
415411
/// - More than one match is found; indicated by a raised error listing the matches and their missing disambiguation.
416-
func find(_ kind: String?, _ hash: String?) throws -> PathHierarchy.Node? {
412+
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
422+
}
423+
417424
if let kind = kind {
418425
// Need to match the provided kind
419426
guard let subtree = storage[kind] else { return nil }
@@ -480,17 +487,13 @@ private extension PathHierarchy.Node {
480487
return true
481488
}
482489
// Otherwise, check if the node's symbol matches the provided disambiguation
483-
else if let symbol = symbol {
484-
guard name == component.name else {
485-
return false
490+
else if let symbol = symbol, let disambiguation = component.disambiguation {
491+
switch disambiguation {
492+
case .kindAndHash(let kind, let hash):
493+
return name == component.name
494+
&& (kind == nil || kind! == symbol.kind.identifier.identifier)
495+
&& (hash == nil || hash! == symbol.identifier.precise.stableHashString)
486496
}
487-
if let kind = component.kind, kind != symbol.kind.identifier.identifier {
488-
return false
489-
}
490-
if let hash = component.hash, hash != symbol.identifier.precise.stableHashString {
491-
return false
492-
}
493-
return true
494497
}
495498

496499
return false

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

Lines changed: 20 additions & 16 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
@@ -14,16 +14,16 @@ import SymbolKit
1414
///
1515
/// This is used to identify parsed path components as kind information.
1616
private let knownSymbolKinds: Set<String> = {
17-
// There's nowhere else that registers these extended symbol kinds and we need to know them in this list.
18-
SymbolGraph.Symbol.KindIdentifier.register(
17+
// We don't want to register these extended symbol kinds because that makes them available for decoding from symbol graphs which is unexpected.
18+
let knownKinds = SymbolGraph.Symbol.KindIdentifier.allCases + [
1919
.extendedProtocol,
2020
.extendedStructure,
2121
.extendedClass,
2222
.extendedEnumeration,
2323
.unknownExtendedType,
2424
.extendedModule
25-
)
26-
return Set(SymbolGraph.Symbol.KindIdentifier.allCases.map(\.identifier))
25+
]
26+
return Set(knownKinds.map(\.identifier))
2727
}()
2828

2929
/// All known source language identifiers.
@@ -38,10 +38,13 @@ extension PathHierarchy {
3838
let full: String
3939
/// The parsed entity name
4040
let name: Substring
41-
/// The parsed entity kind, if any.
42-
var kind: Substring?
43-
/// The parsed entity hash, if any.
44-
var hash: Substring?
41+
/// The parsed disambiguation information, if any.
42+
var disambiguation: Disambiguation?
43+
44+
enum Disambiguation {
45+
/// This path component uses a combination of kind and hash disambiguation
46+
case kindAndHash(kind: Substring?, hash: Substring?)
47+
}
4548
}
4649

4750
enum PathParser {
@@ -65,13 +68,14 @@ extension PathHierarchy.PathParser {
6568
static func parse(pathComponent original: Substring) -> PathComponent {
6669
let full = String(original)
6770
guard let dashIndex = original.lastIndex(of: "-") else {
68-
return PathComponent(full: full, name: full[...], kind: nil, hash: nil)
71+
return PathComponent(full: full, name: full[...], disambiguation: nil)
6972
}
7073

7174
let hash = original[dashIndex...].dropFirst()
7275
let name = original[..<dashIndex]
7376

7477
func isValidHash(_ hash: Substring) -> Bool {
78+
// Checks if a string looks like a truncated, lowercase FNV-1 hash string.
7579
var index: UInt8 = 0
7680
for char in hash.utf8 {
7781
guard index <= 5, (48...57).contains(char) || (97...122).contains(char) else { return false }
@@ -82,28 +86,28 @@ extension PathHierarchy.PathParser {
8286

8387
if knownSymbolKinds.contains(String(hash)) {
8488
// The parsed hash value is a symbol kind
85-
return PathComponent(full: full, name: name, kind: hash, hash: nil)
89+
return PathComponent(full: full, name: name, disambiguation: .kindAndHash(kind: hash, hash: nil))
8690
}
8791
if let languagePrefix = knownLanguagePrefixes.first(where: { hash.starts(with: $0) }) {
8892
// The hash is actually a symbol kind with a language prefix
89-
return PathComponent(full: full, name: name, kind: hash.dropFirst(languagePrefix.count), hash: nil)
93+
return PathComponent(full: full, name: name, disambiguation: .kindAndHash(kind: hash.dropFirst(languagePrefix.count), hash: nil))
9094
}
9195
if !isValidHash(hash) {
9296
// The parsed hash is neither a symbol not a valid hash. It's probably a hyphen-separated name.
93-
return PathComponent(full: full, name: full[...], kind: nil, hash: nil)
97+
return PathComponent(full: full, name: full[...], disambiguation: nil)
9498
}
9599

96100
if let dashIndex = name.lastIndex(of: "-") {
97101
let kind = name[dashIndex...].dropFirst()
98102
let name = name[..<dashIndex]
99103
if knownSymbolKinds.contains(String(kind)) {
100-
return PathComponent(full: full, name: name, kind: kind, hash: hash)
104+
return PathComponent(full: full, name: name, disambiguation: .kindAndHash(kind: kind, hash: hash))
101105
} else if let languagePrefix = knownLanguagePrefixes.first(where: { kind.starts(with: $0) }) {
102106
let kindWithoutLanguage = kind.dropFirst(languagePrefix.count)
103-
return PathComponent(full: full, name: name, kind: kindWithoutLanguage, hash: hash)
107+
return PathComponent(full: full, name: name, disambiguation: .kindAndHash(kind: kindWithoutLanguage, hash: hash))
104108
}
105109
}
106-
return PathComponent(full: full, name: name, kind: nil, hash: hash)
110+
return PathComponent(full: full, name: name, disambiguation: .kindAndHash(kind: nil, hash: hash))
107111
}
108112

109113
static func split(_ path: String) -> (componentSubstrings: [Substring], isAbsolute: Bool) {

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ struct PathHierarchy {
198198
return original
199199
}
200200
}(node.symbol!)[...].dropLast()
201-
while !components.isEmpty, let child = try? parent.children[components.first!]?.find(nil, nil) {
201+
while !components.isEmpty, let child = try? parent.children[components.first!]?.find(nil) {
202202
parent = child
203203
components = components.dropFirst()
204204
}
@@ -210,7 +210,12 @@ struct PathHierarchy {
210210
let component = PathParser.parse(pathComponent: component[...])
211211
let nodeWithoutSymbol = Node(name: String(component.name))
212212
nodeWithoutSymbol.isDisfavoredInCollision = true
213-
parent.add(child: nodeWithoutSymbol, kind: component.kind.map(String.init), hash: component.hash.map(String.init))
213+
switch component.disambiguation {
214+
case .kindAndHash(kind: let kind, hash: let hash):
215+
parent.add(child: nodeWithoutSymbol, kind: kind.map(String.init), hash: hash.map(String.init))
216+
case nil:
217+
parent.add(child: nodeWithoutSymbol, kind: nil, hash: nil)
218+
}
214219
parent = nodeWithoutSymbol
215220
}
216221
parent.add(symbolChild: node)
@@ -425,7 +430,7 @@ extension PathHierarchy {
425430
fileprivate func add(child: Node, kind: String?, hash: String?) {
426431
guard child.parent !== self else {
427432
assert(
428-
(try? children[child.name]?.find(kind, hash)) === child,
433+
(try? children[child.name]?.find(.kindAndHash(kind: kind?[...], hash: hash?[...]))) === child,
429434
"If the new child node already has this node as its parent it should already exist among this node's children."
430435
)
431436
return

0 commit comments

Comments
 (0)