File: PathHierarchy%2BError.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (259 lines) | stat: -rw-r--r-- 14,479 bytes parent folder | download
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)
    }
}