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
|
/*
This source file is part of the Swift.org open source project
Copyright (c) 2023-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 struct Markdown.SourceRange
import struct Markdown.SourceLocation
import SymbolKit
extension PathHierarchy {
/// An error finding an entry in the path hierarchy.
enum Error: Swift.Error {
/// Information about the portion of a link that could be found.
///
/// Includes information about:
/// - The node that was found
/// - The portion of the path up and including to the found node and its trailing path separator.
typealias PartialResult = (node: Node, pathPrefix: Substring)
/// No element was found at the beginning of the path.
///
/// Includes information about:
/// - The portion of the path up to the first path component.
/// - The remaining portion of the path. This may be empty
/// - A list of the names for the top level elements.
case notFound(pathPrefix: Substring, remaining: [PathComponent], availableChildren: Set<String>)
/// No element was found at the beginning of an absolute path.
///
/// Includes information about:
/// - The portion of the path up to the first path component.
/// - A list of the names for the available modules.
case moduleNotFound(pathPrefix: Substring, remaining: [PathComponent], availableChildren: Set<String>)
/// Matched node does not correspond to a documentation page.
///
/// For partial symbol graph files, sometimes sparse nodes that don't correspond to known documentation need to be created to form a hierarchy. These nodes are not findable.
case unfindableMatch(Node)
/// A symbol link found a non-symbol match.
///
/// Includes information about:
/// - The path to the non-symbol match.
case nonSymbolMatchForSymbolLink(path: String)
/// Encountered an unknown disambiguation for a found node.
///
/// Includes information about:
/// - The partial result for as much of the path that could be found.
/// - The remaining portion of the path.
/// - A list of possible matches paired with the disambiguation suffixes needed to distinguish them.
case unknownDisambiguation(partialResult: PartialResult, remaining: [PathComponent], candidates: [(node: Node, disambiguation: String)])
/// Encountered an unknown name in the path.
///
/// Includes information about:
/// - The partial result for as much of the path that could be found.
/// - The remaining portion of the path.
/// - A list of the names for the children of the partial result.
case unknownName(partialResult: PartialResult, remaining: [PathComponent], availableChildren: Set<String>)
/// Multiple matches are found partway through the path.
///
/// Includes information about:
/// - The partial result for as much of the path that could be found unambiguously.
/// - The remaining portion of the path.
/// - A list of possible matches paired with the disambiguation suffixes needed to distinguish them.
case lookupCollision(partialResult: PartialResult, remaining: [PathComponent], collisions: [(node: Node, disambiguation: String)])
}
}
extension PathHierarchy.Error {
/// Creates a value with structured information that can be used to present diagnostics about the error.
/// - Parameters:
/// - fullNameOfNode: A closure that determines the full name of a node, to be displayed in collision diagnostics to precisely identify symbols and other pages.
/// - Note: `Replacement`s produced by this function use `SourceLocation`s relative to the link text excluding its surrounding syntax.
func makeTopicReferenceResolutionErrorInfo(fullNameOfNode: (PathHierarchy.Node) -> String) -> TopicReferenceResolutionErrorInfo {
// This is defined inline because it captures `fullNameOfNode`.
func collisionIsBefore(_ lhs: (node: PathHierarchy.Node, disambiguation: String), _ rhs: (node: PathHierarchy.Node, disambiguation: String)) -> Bool {
return fullNameOfNode(lhs.node) + lhs.disambiguation
< fullNameOfNode(rhs.node) + rhs.disambiguation
}
switch self {
case .moduleNotFound(pathPrefix: let pathPrefix, remaining: let remaining, availableChildren: let availableChildren):
let firstPathComponent = remaining.first! // This would be a .notFound error if the remaining components were empty.
let replacementRange = SourceRange.makeRelativeRange(startColumn: pathPrefix.count, length: firstPathComponent.full.count)
let nearMisses = NearMiss.bestMatches(for: availableChildren, against: String(firstPathComponent.name))
let solutions = nearMisses.map { candidate in
Solution(summary: "\(Self.replacementOperationDescription(from: firstPathComponent.full, to: candidate))", replacements: [
Replacement(range: replacementRange, replacement: candidate)
])
}
return TopicReferenceResolutionErrorInfo("""
No module named \(firstPathComponent.full.singleQuoted)
""",
solutions: solutions
)
case .notFound(pathPrefix: let pathPrefix, remaining: let remaining, availableChildren: let availableChildren):
guard let firstPathComponent = remaining.first else {
return TopicReferenceResolutionErrorInfo(
"No local documentation matches this reference"
)
}
let replacementRange = SourceRange.makeRelativeRange(startColumn: pathPrefix.count, length: firstPathComponent.full.count)
let nearMisses = NearMiss.bestMatches(for: availableChildren, against: String(firstPathComponent.name))
let solutions = nearMisses.map { candidate in
Solution(summary: "\(Self.replacementOperationDescription(from: firstPathComponent.full, to: candidate))", replacements: [
Replacement(range: replacementRange, replacement: candidate)
])
}
return TopicReferenceResolutionErrorInfo("""
Can't resolve \(firstPathComponent.full.singleQuoted)
""",
solutions: solutions
)
case .unfindableMatch(let node):
return TopicReferenceResolutionErrorInfo("""
\(node.name.singleQuoted) can't be linked to in a partial documentation build
""")
case .nonSymbolMatchForSymbolLink(path: let path):
return TopicReferenceResolutionErrorInfo("Symbol links can only resolve symbols", solutions: [
Solution(summary: "Use a '<doc:>' style reference.", replacements: [
// the SourceRange points to the opening double-backtick
Replacement(range: .makeRelativeRange(startColumn: -2, endColumn: 0), replacement: "<doc:"),
// the SourceRange points to the closing double-backtick
Replacement(range: .makeRelativeRange(startColumn: path.count, endColumn: path.count+2), replacement: ">"),
])
])
case .unknownDisambiguation(partialResult: let partialResult, remaining: let remaining, candidates: let candidates):
let nextPathComponent = remaining.first!
let validPrefix = partialResult.pathPrefix + nextPathComponent.name
let disambiguations = nextPathComponent.full.dropFirst(nextPathComponent.name.count)
let replacementRange = SourceRange.makeRelativeRange(startColumn: validPrefix.count, length: disambiguations.count)
let solutions: [Solution] = candidates
.sorted(by: collisionIsBefore)
.map { (node: PathHierarchy.Node, disambiguation: String) -> Solution in
return Solution(summary: "\(Self.replacementOperationDescription(from: disambiguations, to: disambiguation)) for\n\(fullNameOfNode(node).singleQuoted)", replacements: [
Replacement(range: replacementRange, replacement: disambiguation)
])
}
return TopicReferenceResolutionErrorInfo("""
\(disambiguations.dropFirst().singleQuoted) isn't a disambiguation for \(nextPathComponent.name.singleQuoted) at \(partialResult.node.pathWithoutDisambiguation().singleQuoted)
""",
solutions: solutions,
rangeAdjustment: .makeRelativeRange(startColumn: validPrefix.count, length: disambiguations.count)
)
case .unknownName(partialResult: let partialResult, remaining: let remaining, availableChildren: let availableChildren):
let nextPathComponent = remaining.first!
let nearMisses = NearMiss.bestMatches(for: availableChildren, against: String(nextPathComponent.name))
// Use the authored disambiguation to try and reduce the possible near misses. For example, if the link was disambiguated with `-struct` we should
// only make suggestions for similarly spelled structs.
let filteredNearMisses = nearMisses.filter { name in
(try? partialResult.node.children[name]?.find(nextPathComponent.disambiguation)) != nil
}
let pathPrefix = partialResult.pathPrefix
let solutions: [Solution]
if filteredNearMisses.isEmpty {
// If there are no near-misses where the authored disambiguation narrow down the results, replace the full path component
let replacementRange = SourceRange.makeRelativeRange(startColumn: pathPrefix.count, length: nextPathComponent.full.count)
solutions = nearMisses.map { candidate in
Solution(summary: "\(Self.replacementOperationDescription(from: nextPathComponent.full, to: candidate))", replacements: [
Replacement(range: replacementRange, replacement: candidate)
])
}
} else {
// If the authored disambiguation narrows down the possible near-misses, only replace the name part of the path component
let replacementRange = SourceRange.makeRelativeRange(startColumn: pathPrefix.count, length: nextPathComponent.name.count)
solutions = filteredNearMisses.map { candidate in
Solution(summary: "\(Self.replacementOperationDescription(from: nextPathComponent.name, to: candidate))", replacements: [
Replacement(range: replacementRange, replacement: candidate)
])
}
}
return TopicReferenceResolutionErrorInfo("""
\(nextPathComponent.full.singleQuoted) doesn't exist at \(partialResult.node.pathWithoutDisambiguation().singleQuoted)
""",
solutions: solutions,
rangeAdjustment: .makeRelativeRange(startColumn: pathPrefix.count, length: nextPathComponent.full.count)
)
case .lookupCollision(partialResult: let partialResult, remaining: let remaining, collisions: let collisions):
let nextPathComponent = remaining.first!
let pathPrefix = partialResult.pathPrefix + nextPathComponent.name
let disambiguations = nextPathComponent.full.dropFirst(nextPathComponent.name.count)
let replacementRange = SourceRange.makeRelativeRange(startColumn: pathPrefix.count, length: disambiguations.count)
let solutions: [Solution] = collisions.sorted(by: collisionIsBefore).map { (node: PathHierarchy.Node, disambiguation: String) -> Solution in
return Solution(summary: "\(Self.replacementOperationDescription(from: disambiguations.dropFirst(), to: disambiguation)) for\n\(fullNameOfNode(node).singleQuoted)", replacements: [
Replacement(range: replacementRange, replacement: "-" + disambiguation)
])
}
return TopicReferenceResolutionErrorInfo("""
\(nextPathComponent.full.singleQuoted) is ambiguous at \(partialResult.node.pathWithoutDisambiguation().singleQuoted)
""",
solutions: solutions,
rangeAdjustment: .makeRelativeRange(startColumn: pathPrefix.count - nextPathComponent.full.count, length: nextPathComponent.full.count)
)
}
}
private static func replacementOperationDescription(from: some StringProtocol, to: some StringProtocol) -> String {
if from.isEmpty {
return "Insert \(to.singleQuoted)"
}
if to.isEmpty {
return "Remove \(from.singleQuoted)"
}
return "Replace \(from.singleQuoted) with \(to.singleQuoted)"
}
}
private extension PathHierarchy.Node {
/// Creates a path string without any disambiguation.
///
/// > Note: This value is only intended for error messages and other presentation.
func pathWithoutDisambiguation() -> String {
var components = [name]
var node = self
while let parent = node.parent {
components.insert(parent.name, at: 0)
node = parent
}
return "/" + components.joined(separator: "/")
}
}
private extension SourceRange {
static func makeRelativeRange(startColumn: Int, endColumn: Int) -> SourceRange {
return SourceLocation(line: 0, column: startColumn, source: nil) ..< SourceLocation(line: 0, column: endColumn, source: nil)
}
static func makeRelativeRange(startColumn: Int, length: Int) -> SourceRange {
return .makeRelativeRange(startColumn: startColumn, endColumn: startColumn + length)
}
}
|