Skip to content

Commit cf6dde1

Browse files
authored
Adopt typed throws to provide callers with more information about possible raised errors (#1295)
* Use typed throws in path hierarchy to avoid handing impossible cases * Use typed throws in some methods that only raise a single error * Preserve typed error information in helpers
1 parent 3dc6dd0 commit cf6dde1

File tree

10 files changed

+53
-56
lines changed

10 files changed

+53
-56
lines changed

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2707,7 +2707,7 @@ public class DocumentationContext {
27072707
- Returns: A ``DocumentationNode`` with the given identifier.
27082708
- Throws: ``ContextError/notFound(_:)`` if a documentation node with the given identifier was not found.
27092709
*/
2710-
public func entity(with reference: ResolvedTopicReference) throws -> DocumentationNode {
2710+
public func entity(with reference: ResolvedTopicReference) throws(ContextError) -> DocumentationNode {
27112711
if let cached = documentationCache[reference] {
27122712
return cached
27132713
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,10 @@ final class ExternalPathHierarchyResolver {
4444
}
4545

4646
return .success(foundReference)
47-
} catch let error as PathHierarchy.Error {
47+
} catch {
4848
return .failure(unresolvedReference, error.makeTopicReferenceResolutionErrorInfo() { collidingNode in
4949
self.fullName(of: collidingNode) // If the link was ambiguous, determine the full name of each colliding node to be presented in the link diagnostic.
5050
})
51-
} catch {
52-
fatalError("Only PathHierarchy.Error errors are raised from the symbol link resolution code above.")
5351
}
5452
}
5553

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public class LinkResolver {
6767

6868
do {
6969
return try localResolver.resolve(unresolvedReference, in: parent, fromSymbolLink: isCurrentlyResolvingSymbolLink)
70-
} catch let error as PathHierarchy.Error {
70+
} catch {
7171
// Check if there's a known external resolver for this module.
7272
if case .moduleNotFound(_, let remainingPathComponents, _) = error, let resolver = externalResolvers[remainingPathComponents.first!.full] {
7373
let result = resolver.resolve(unresolvedReference, fromSymbolLink: isCurrentlyResolvingSymbolLink)
@@ -86,8 +86,6 @@ public class LinkResolver {
8686
} else {
8787
return .failure(unresolvedReference, error.makeTopicReferenceResolutionErrorInfo() { localResolver.fullName(of: $0, in: context) })
8888
}
89-
} catch {
90-
fatalError("Only SymbolPathTree.Error errors are raised from the symbol link resolution code above.")
9189
}
9290
}
9391

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

Lines changed: 38 additions & 37 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-2024 Apple Inc. and the Swift project authors
4+
Copyright (c) 2023-2025 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
@@ -20,11 +20,11 @@ extension PathHierarchy {
2020
/// - onlyFindSymbols: Whether or not only symbol matches should be found.
2121
/// - Returns: Returns the unique identifier for the found match or raises an error if no match can be found.
2222
/// - Throws: Raises a ``PathHierarchy/Error`` if no match can be found.
23-
func find(path rawPath: String, parent: ResolvedIdentifier? = nil, onlyFindSymbols: Bool) throws -> ResolvedIdentifier {
23+
func find(path rawPath: String, parent: ResolvedIdentifier? = nil, onlyFindSymbols: Bool) throws(Error) -> ResolvedIdentifier {
2424
return try findNode(path: rawPath, parentID: parent, onlyFindSymbols: onlyFindSymbols).identifier
2525
}
2626

27-
private func findNode(path rawPath: String, parentID: ResolvedIdentifier?, onlyFindSymbols: Bool) throws -> Node {
27+
private func findNode(path rawPath: String, parentID: ResolvedIdentifier?, onlyFindSymbols: Bool) throws(Error) -> Node {
2828
// The search for a documentation element can be though of as 3 steps:
2929
// - First, parse the path into structured path components.
3030
// - Second, find nodes that match the beginning of the path as starting points for the search
@@ -79,15 +79,15 @@ extension PathHierarchy {
7979
}
8080

8181
// A function to avoid repeating the
82-
func searchForNodeInModules() throws -> Node {
82+
func searchForNodeInModules() throws(Error) -> Node {
8383
// Note: This captures `parentID`, `remaining`, and `rawPathForError`.
8484
if let moduleMatch = modules.first(where: { $0.matches(firstComponent) }) {
8585
return try searchForNode(descendingFrom: moduleMatch, pathComponents: remaining.dropFirst(), onlyFindSymbols: onlyFindSymbols, rawPathForError: rawPath)
8686
}
8787
if modules.count == 1 {
8888
do {
8989
return try searchForNode(descendingFrom: modules.first!, pathComponents: remaining, onlyFindSymbols: onlyFindSymbols, rawPathForError: rawPath)
90-
} catch let error as PathHierarchy.Error {
90+
} catch {
9191
switch error {
9292
case .notFound:
9393
// Ignore this error and raise an error about not finding the module instead.
@@ -129,7 +129,7 @@ extension PathHierarchy {
129129
}
130130

131131
// A recursive function to traverse up the path hierarchy searching for the matching node
132-
func searchForNodeUpTheHierarchy(from startingPoint: Node?, path: ArraySlice<PathComponent>) throws -> Node {
132+
func searchForNodeUpTheHierarchy(from startingPoint: Node?, path: ArraySlice<PathComponent>) throws(Error) -> Node {
133133
guard let possibleStartingPoint = startingPoint else {
134134
// If the search has reached the top of the hierarchy, check the modules as a base case to break the recursion.
135135
do {
@@ -147,7 +147,7 @@ extension PathHierarchy {
147147
let firstComponent = path.first!
148148

149149
// Keep track of the inner most error and raise that if no node is found.
150-
var innerMostError: (any Swift.Error)?
150+
var innerMostError: Error?
151151

152152
// If the starting point's children match this component, descend the path hierarchy from there.
153153
if possibleStartingPoint.anyChildMatches(firstComponent) {
@@ -211,7 +211,7 @@ extension PathHierarchy {
211211
pathComponents: ArraySlice<PathComponent>,
212212
onlyFindSymbols: Bool,
213213
rawPathForError: String
214-
) throws -> Node {
214+
) throws(Error) -> Node {
215215
// All code paths through this function wants to perform extra verification on the return value before returning it to the caller.
216216
// To accomplish that, the core implementation happens in `_innerImplementation`, which is called once, right below its definition.
217217

@@ -220,7 +220,7 @@ extension PathHierarchy {
220220
pathComponents: ArraySlice<PathComponent>,
221221
onlyFindSymbols: Bool,
222222
rawPathForError: String
223-
) throws -> Node {
223+
) throws(Error) -> Node {
224224
var node = startingPoint
225225
var remaining = pathComponents[...]
226226

@@ -234,21 +234,13 @@ extension PathHierarchy {
234234
while true {
235235
let (children, pathComponent) = try findChildContainer(node: &node, remaining: remaining, rawPathForError: rawPathForError)
236236

237+
let child: PathHierarchy.Node?
237238
do {
238-
guard let child = try children.find(pathComponent.disambiguation) else {
239-
// The search has ended with a node that doesn't have a child matching the next path component.
240-
throw makePartialResultError(node: node, remaining: remaining, rawPathForError: rawPathForError)
241-
}
242-
node = child
243-
remaining = remaining.dropFirst()
244-
if remaining.isEmpty {
245-
// If all path components are consumed, then the match is found.
246-
return child
247-
}
248-
} catch DisambiguationContainer.Error.lookupCollision(let collisions) {
249-
func handleWrappedCollision() throws -> Node {
250-
let match = try handleCollision(node: node, remaining: remaining, collisions: collisions, onlyFindSymbols: onlyFindSymbols, rawPathForError: rawPathForError)
251-
return match
239+
child = try children.find(pathComponent.disambiguation)
240+
} catch {
241+
let collisions = error.collisions
242+
func handleWrappedCollision() throws(Error) -> Node {
243+
try handleCollision(node: node, remaining: remaining, collisions: collisions, onlyFindSymbols: onlyFindSymbols, rawPathForError: rawPathForError)
252244
}
253245

254246
// When there's a collision, use the remaining path components to try and narrow down the possible collisions.
@@ -314,6 +306,17 @@ extension PathHierarchy {
314306
// Couldn't resolve the collision by look ahead.
315307
return try handleWrappedCollision()
316308
}
309+
310+
guard let child else {
311+
// The search has ended with a node that doesn't have a child matching the next path component.
312+
throw makePartialResultError(node: node, remaining: remaining, rawPathForError: rawPathForError)
313+
}
314+
node = child
315+
remaining = remaining.dropFirst()
316+
if remaining.isEmpty {
317+
// If all path components are consumed, then the match is found.
318+
return child
319+
}
317320
}
318321
}
319322

@@ -336,7 +339,7 @@ extension PathHierarchy {
336339
collisions: [(node: PathHierarchy.Node, disambiguation: String)],
337340
onlyFindSymbols: Bool,
338341
rawPathForError: String
339-
) throws -> Node {
342+
) throws(Error) -> Node {
340343
if let favoredMatch = collisions.singleMatch({ !$0.node.isDisfavoredInLinkCollisions }) {
341344
return favoredMatch.node
342345
}
@@ -421,7 +424,7 @@ extension PathHierarchy {
421424
node: inout Node,
422425
remaining: ArraySlice<PathComponent>,
423426
rawPathForError: String
424-
) throws -> (DisambiguationContainer, PathComponent) {
427+
) throws(Error) -> (DisambiguationContainer, PathComponent) {
425428
var pathComponent = remaining.first!
426429
if let match = node.children[pathComponent.full] {
427430
// The path component parsing may treat dash separated words as disambiguation information.
@@ -439,12 +442,10 @@ extension PathHierarchy {
439442
// MARK: Disambiguation Container
440443

441444
extension PathHierarchy.DisambiguationContainer {
442-
/// Errors finding values in the disambiguation tree
443-
enum Error: Swift.Error {
444-
/// Multiple matches found.
445-
///
446-
/// Includes a list of values paired with their missing disambiguation suffixes.
447-
case lookupCollision([(node: PathHierarchy.Node, disambiguation: String)])
445+
/// Multiple matches found.
446+
struct LookupCollisionError: Swift.Error {
447+
/// A list of values paired with their missing disambiguation suffixes.
448+
let collisions: [(node: PathHierarchy.Node, disambiguation: String)]
448449
}
449450

450451
/// Attempts to find the only element in the disambiguation container without using any disambiguation information.
@@ -464,7 +465,7 @@ extension PathHierarchy.DisambiguationContainer {
464465
/// - No match is found; indicated by a `nil` return value.
465466
/// - Exactly one match is found; indicated by a non-nil return value.
466467
/// - More than one match is found; indicated by a raised error listing the matches and their missing disambiguation.
467-
func find(_ disambiguation: PathHierarchy.PathComponent.Disambiguation?) throws -> PathHierarchy.Node? {
468+
func find(_ disambiguation: PathHierarchy.PathComponent.Disambiguation?) throws(LookupCollisionError) -> PathHierarchy.Node? {
468469
if disambiguation == nil, let match = singleMatch() {
469470
return match
470471
}
@@ -478,13 +479,13 @@ extension PathHierarchy.DisambiguationContainer {
478479
let matches = storage.filter({ $0.kind == kind })
479480
guard matches.count <= 1 else {
480481
// Suggest not only hash disambiguation, but also type signature disambiguation.
481-
throw Error.lookupCollision(Self.disambiguatedValues(for: matches).map { ($0.value, $0.disambiguation.makeSuffix()) })
482+
throw LookupCollisionError(collisions: Self.disambiguatedValues(for: matches).map { ($0.value, $0.disambiguation.makeSuffix()) })
482483
}
483484
return matches.first?.node
484485
case (nil, let hash?):
485486
let matches = storage.filter({ $0.hash == hash })
486487
guard matches.count <= 1 else {
487-
throw Error.lookupCollision(matches.map { ($0.node, "-" + $0.kind!) }) // An element wouldn't match if it didn't have kind disambiguation.
488+
throw LookupCollisionError(collisions: matches.map { ($0.node, "-" + $0.kind!) }) // An element wouldn't match if it didn't have kind disambiguation.
488489
}
489490
return matches.first?.node
490491
case (nil, nil):
@@ -498,13 +499,13 @@ extension PathHierarchy.DisambiguationContainer {
498499
case (let parameterTypes?, nil):
499500
let matches = storage.filter({ typesMatch(provided: parameterTypes, actual: $0.parameterTypes) })
500501
guard matches.count <= 1 else {
501-
throw Error.lookupCollision(matches.map { ($0.node, "->" + formattedTypes($0.parameterTypes)!) }) // An element wouldn't match if it didn't have parameter type disambiguation.
502+
throw LookupCollisionError(collisions: matches.map { ($0.node, "->" + formattedTypes($0.parameterTypes)!) }) // An element wouldn't match if it didn't have parameter type disambiguation.
502503
}
503504
return matches.first?.node
504505
case (nil, let returnTypes?):
505506
let matches = storage.filter({ typesMatch(provided: returnTypes, actual: $0.returnTypes) })
506507
guard matches.count <= 1 else {
507-
throw Error.lookupCollision(matches.map { ($0.node, "-" + formattedTypes($0.returnTypes)!) }) // An element wouldn't match if it didn't have return type disambiguation.
508+
throw LookupCollisionError(collisions: matches.map { ($0.node, "-" + formattedTypes($0.returnTypes)!) }) // An element wouldn't match if it didn't have return type disambiguation.
508509
}
509510
return matches.first?.node
510511
case (nil, nil):
@@ -515,7 +516,7 @@ extension PathHierarchy.DisambiguationContainer {
515516
}
516517

517518
// Disambiguate by a mix of kinds and USRs
518-
throw Error.lookupCollision(self.disambiguatedValues().map { ($0.value, $0.disambiguation.makeSuffix()) })
519+
throw LookupCollisionError(collisions: self.disambiguatedValues().map { ($0.value, $0.disambiguation.makeSuffix()) })
519520
}
520521
}
521522

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ final class PathHierarchyBasedLinkResolver {
228228
/// - isCurrentlyResolvingSymbolLink: Whether or not the documentation link is a symbol link.
229229
/// - context: The documentation context to resolve the link in.
230230
/// - Returns: The result of resolving the reference.
231-
func resolve(_ unresolvedReference: UnresolvedTopicReference, in parent: ResolvedTopicReference, fromSymbolLink isCurrentlyResolvingSymbolLink: Bool) throws -> TopicReferenceResolutionResult {
231+
func resolve(_ unresolvedReference: UnresolvedTopicReference, in parent: ResolvedTopicReference, fromSymbolLink isCurrentlyResolvingSymbolLink: Bool) throws(PathHierarchy.Error) -> TopicReferenceResolutionResult {
232232
let parentID = resolvedReferenceMap[parent]
233233
let found = try pathHierarchy.find(path: Self.path(for: unresolvedReference), parent: parentID, onlyFindSymbols: isCurrentlyResolvingSymbolLink)
234234
guard let foundReference = resolvedReferenceMap[found] else {

Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypeFormatTransformation.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) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2025 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
@@ -569,7 +569,7 @@ extension ExtendedTypeFormatTransformation {
569569
// MARK: Apply Mappings to SymbolGraph
570570

571571
private extension SymbolGraph {
572-
mutating func apply(compactMap include: (SymbolGraph.Symbol) throws -> SymbolGraph.Symbol?) rethrows {
572+
mutating func apply<Error>(compactMap include: (SymbolGraph.Symbol) throws(Error) -> SymbolGraph.Symbol?) throws(Error) {
573573
for (key, symbol) in self.symbols {
574574
self.symbols.removeValue(forKey: key)
575575
if let newSymbol = try include(symbol) {

Sources/SwiftDocC/Semantics/Symbol/Symbol.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,14 +461,14 @@ extension Symbol {
461461
/// When building multi-platform documentation symbols might have more than one declaration
462462
/// depending on variances in their implementation across platforms (e.g. use `NSPoint` vs `CGPoint` parameter in a method).
463463
/// This method finds matching symbols between graphs and merges their declarations in case there are differences.
464-
func mergeDeclaration(mergingDeclaration: SymbolGraph.Symbol.DeclarationFragments, identifier: String, symbolAvailability: SymbolGraph.Symbol.Availability?, alternateSymbols: SymbolGraph.Symbol.AlternateSymbols?, selector: UnifiedSymbolGraph.Selector) throws {
464+
func mergeDeclaration(mergingDeclaration: SymbolGraph.Symbol.DeclarationFragments, identifier: String, symbolAvailability: SymbolGraph.Symbol.Availability?, alternateSymbols: SymbolGraph.Symbol.AlternateSymbols?, selector: UnifiedSymbolGraph.Selector) throws(DocumentationContext.ContextError) {
465465
let trait = DocumentationDataVariantsTrait(for: selector)
466466
let platformName = selector.platform
467467

468468
func merge<Value: Equatable>(
469469
_ mergingValue: Value,
470470
into variants: inout DocumentationDataVariants<[[PlatformName?] : Value]>
471-
) throws {
471+
) throws(DocumentationContext.ContextError) {
472472
guard let platformName else {
473473
variants[trait]?[[nil]] = mergingValue
474474
return

Sources/SwiftDocC/Utility/FoundationExtensions/RangeReplaceableCollection+Group.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) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2025 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
@@ -15,7 +15,7 @@ extension RangeReplaceableCollection {
1515
///
1616
/// - Parameter belongsInGroupWithPrevious: A check whether the given element belongs in the same group as the previous element
1717
/// - Returns: An array of subsequences of elements that belong together.
18-
func group(asLongAs belongsInGroupWithPrevious: (_ previous: Element, _ current: Element) throws -> Bool) rethrows -> [SubSequence] {
18+
func group<Error>(asLongAs belongsInGroupWithPrevious: (_ previous: Element, _ current: Element) throws(Error) -> Bool) throws(Error) -> [SubSequence] {
1919
var result = [SubSequence]()
2020

2121
let indexPairs = zip(indices, indices.dropFirst())

Sources/SwiftDocC/Utility/FoundationExtensions/Sequence+FirstMap.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) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2025 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
@@ -30,7 +30,7 @@ extension Sequence {
3030
/// - Parameter predicate: A mapping closure that accepts an element of this sequence as its parameter and returns a transformed value or `nil`.
3131
/// - Throws: Any error that's raised by the mapping closure.
3232
/// - Returns: The first mapped, non-nil value, or `nil` if the mapping closure returned `nil` for every value in the sequence.
33-
func mapFirst<T>(where predicate: (Element) throws -> T?) rethrows -> T? {
33+
func mapFirst<Result, Error>(where predicate: (Element) throws(Error) -> Result?) throws(Error) -> Result? {
3434
for element in self {
3535
if let result = try predicate(element) {
3636
return result

0 commit comments

Comments
 (0)