From 79f7219c96fb1aa354378c12fe24fc1a8a0aaca2 Mon Sep 17 00:00:00 2001 From: Max Obermeier Date: Wed, 18 Jan 2023 16:22:51 +0100 Subject: [PATCH] address review feedback - create TopicReferenceResolutionError with minimal public interface - replace TopicReferenceResolutionResult.failure's errorMessage with TopicReferenceResolutionError - add support for non-symbol reference syntax - add Solution for PathHierarchy.Error.nonSymbolMatchForSymbolLink - add meaningful information to Solution.summary - scope Replacements as small as possible (on disambiguation, path segment, or reference delimiters) - add note listing all available candidates even if near-miss suggestions are available - add Solutions if no near-miss is available, but there are only three candidates or less - make offsetWithRange mutating - remove XCTAssertElements test helper --- .../Diagnostics/Diagnostic.swift | 14 +- .../Infrastructure/Diagnostics/Problem.swift | 19 +- .../Diagnostics/Replacement.swift | 25 +- .../Infrastructure/DocumentationContext.swift | 9 +- .../OutOfProcessReferenceResolver.swift | 4 +- .../DocumentationCacheBasedLinkResolver.swift | 6 +- .../Link Resolution/PathHierarchy.swift | 134 +++-- .../PathHierarchyBasedLinkResolver.swift | 4 +- .../SwiftDocC/Model/DocumentationNode.swift | 2 +- Sources/SwiftDocC/Model/Identifier.swift | 90 +++- .../Semantics/MarkupReferenceResolver.swift | 6 +- .../Semantics/ReferenceResolver.swift | 28 +- .../SourceRangeExtensions.swift | 38 ++ .../Sequence+XCTAssert.swift | 42 -- .../Diagnostics/DiagnosticTests.swift | 5 +- .../ExternalReferenceResolverTests.swift | 6 +- .../Infrastructure/PathHierarchyTests.swift | 47 +- .../Semantics/SymbolTests.swift | 503 ++++++++++++------ .../OutOfProcessReferenceResolverTests.swift | 4 +- 19 files changed, 608 insertions(+), 378 deletions(-) create mode 100644 Sources/SwiftDocC/Utility/MarkupExtensions/SourceRangeExtensions.swift delete mode 100644 Sources/SwiftDocCTestUtilities/Sequence+XCTAssert.swift diff --git a/Sources/SwiftDocC/Infrastructure/Diagnostics/Diagnostic.swift b/Sources/SwiftDocC/Infrastructure/Diagnostics/Diagnostic.swift index 1df694aaaf..2e6934b770 100644 --- a/Sources/SwiftDocC/Infrastructure/Diagnostics/Diagnostic.swift +++ b/Sources/SwiftDocC/Infrastructure/Diagnostics/Diagnostic.swift @@ -87,17 +87,13 @@ public struct Diagnostic: DescribedError { public extension Diagnostic { - /// Returns a copy of the diagnostic but offset using a certain `SourceRange`. + /// Offsets the diagnostic using a certain SymbolKit `SourceRange`. + /// /// Useful when validating a doc comment that needs to be projected in its containing file "space". - func offsetedWithRange(_ docRange: SymbolGraph.LineList.SourceRange) -> Diagnostic { - guard let diagnosticRange = range else { - // No location information in the source diagnostic, might be removed for safety reasons. - return self - } + mutating func offsetWithRange(_ docRange: SymbolGraph.LineList.SourceRange) { + // If there is no location information in the source diagnostic, the diagnostic might be removed for safety reasons. + range?.offsetWithRange(docRange) - var result = self - result.range = diagnosticRange.offsetedWithRange(docRange) - return result } var localizedDescription: String { diff --git a/Sources/SwiftDocC/Infrastructure/Diagnostics/Problem.swift b/Sources/SwiftDocC/Infrastructure/Diagnostics/Problem.swift index 30e8fdedec..c2bbc2ae5f 100644 --- a/Sources/SwiftDocC/Infrastructure/Diagnostics/Problem.swift +++ b/Sources/SwiftDocC/Infrastructure/Diagnostics/Problem.swift @@ -52,22 +52,17 @@ extension Problem { } extension Problem { - - /// Returns a copy of the problem but offset using a certain `SourceRange`. + /// Offsets the problem using a certain SymbolKit `SourceRange`. + /// /// Useful when validating a doc comment that needs to be projected in its containing file "space". - func offsetedWithRange(_ docRange: SymbolGraph.LineList.SourceRange) -> Problem { - var result = self - result.diagnostic = diagnostic.offsetedWithRange(docRange) + mutating func offsetWithRange(_ docRange: SymbolGraph.LineList.SourceRange) { + diagnostic.offsetWithRange(docRange) - result.possibleSolutions = possibleSolutions.map { - var solution = $0 - solution.replacements = solution.replacements.map { replacement in - replacement.offsetedWithRange(docRange) + for i in possibleSolutions.indices { + for j in possibleSolutions[i].replacements.indices { + possibleSolutions[i].replacements[j].offsetWithRange(docRange) } - return solution } - - return result } } diff --git a/Sources/SwiftDocC/Infrastructure/Diagnostics/Replacement.swift b/Sources/SwiftDocC/Infrastructure/Diagnostics/Replacement.swift index b4424f5fbb..9d8e8540a0 100644 --- a/Sources/SwiftDocC/Infrastructure/Diagnostics/Replacement.swift +++ b/Sources/SwiftDocC/Infrastructure/Diagnostics/Replacement.swift @@ -28,22 +28,17 @@ public struct Replacement { } extension Replacement { - - /// Returns a copy of the replacement but offset using a certain `SourceRange`. + /// Offsets the replacement using a certain `SourceRange`. + /// /// Useful when validating a doc comment that needs to be projected in its containing file "space". - func offsetedWithRange(_ docRange: SymbolGraph.LineList.SourceRange) -> Replacement { - var result = self - result.range = range.offsetedWithRange(docRange) - return result + mutating func offsetWithRange(_ range: SourceRange) { + self.range.offsetWithRange(range) } -} - -extension SourceRange { - /// Returns a copy of the `SourceRange` offset using a certain SymbolKit `SourceRange`. - func offsetedWithRange(_ docRange: SymbolGraph.LineList.SourceRange) -> SourceRange { - let start = SourceLocation(line: lowerBound.line + docRange.start.line, column: lowerBound.column + docRange.start.character, source: nil) - let end = SourceLocation(line: upperBound.line + docRange.start.line, column: upperBound.column + docRange.start.character, source: nil) - - return start.. Problem? in - guard let docRange = docs.lines.first?.range else { return nil } - return problem.offsetedWithRange(docRange) + if let docRange = docs.lines.first?.range { + for i in problems.indices { + problems[i].offsetWithRange(docRange) + } + } else { + problems.removeAll() } } diff --git a/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift b/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift index f23d66e745..bfee5ab17b 100644 --- a/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift @@ -123,7 +123,7 @@ public class OutOfProcessReferenceResolver: ExternalReferenceResolver, FallbackR do { guard let unresolvedTopicURL = unresolvedReference.topicURL.components.url else { // Return the unresolved reference if the underlying URL is not valid - return .failure(unresolvedReference, errorMessage: "URL \(unresolvedReference.topicURL.absoluteString.singleQuoted) is not valid.") + return .failure(unresolvedReference, TopicReferenceResolutionError("URL \(unresolvedReference.topicURL.absoluteString.singleQuoted) is not valid.")) } let metadata = try resolveInformationForTopicURL(unresolvedTopicURL) // Don't do anything with this URL. The external URL will be resolved during conversion to render nodes @@ -136,7 +136,7 @@ public class OutOfProcessReferenceResolver: ExternalReferenceResolver, FallbackR ) ) } catch let error { - return .failure(unresolvedReference, errorMessage: error.localizedDescription) + return .failure(unresolvedReference, TopicReferenceResolutionError(error)) } } } diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift index 652c06429b..559bfe09e4 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift @@ -129,7 +129,7 @@ final class DocumentationCacheBasedLinkResolver { // Ensure we are resolving either relative links or "doc:" scheme links guard unresolvedReference.topicURL.url.scheme == nil || ResolvedTopicReference.urlHasResolvedTopicScheme(unresolvedReference.topicURL.url) else { // Not resolvable in the topic graph - return .failure(unresolvedReference, errorMessage: "Reference URL \(unresolvedReference.description.singleQuoted) doesn't have \"doc:\" scheme.") + return .failure(unresolvedReference, TopicReferenceResolutionError("Reference URL \(unresolvedReference.description.singleQuoted) doesn't have \"doc:\" scheme.")) } // Fall back on the parent's bundle identifier for relative paths @@ -267,7 +267,7 @@ final class DocumentationCacheBasedLinkResolver { // Return the successful or failed externally resolved reference. return resolvedExternalReference } else if !context.registeredBundles.contains(where: { $0.identifier == bundleID }) { - return .failure(unresolvedReference, errorMessage: "No external resolver registered for \(bundleID.singleQuoted).") + return .failure(unresolvedReference, TopicReferenceResolutionError("No external resolver registered for \(bundleID.singleQuoted).")) } } @@ -295,7 +295,7 @@ final class DocumentationCacheBasedLinkResolver { // Give up: there is no local or external document for this reference. // External references which failed to resolve will already have returned a more specific error message. - return .failure(unresolvedReference, errorMessage: "No local documentation matches this reference.") + return .failure(unresolvedReference, TopicReferenceResolutionError("No local documentation matches this reference.")) } diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift index bd6145fb45..b31ba2a22c 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift @@ -10,6 +10,7 @@ import Foundation import SymbolKit +import Markdown /// An opaque identifier that uniquely identifies a resolved entry in the path hierarchy, /// @@ -829,63 +830,100 @@ private func availableChildNameIsBefore(_ lhs: String, _ rhs: String) -> Bool { } extension PathHierarchy.Error { - /// Formats the error into an error message suitable for presentation - func errorMessage(context: DocumentationContext) -> String { + /// Generate a ``TopicReferenceResolutionError`` from this error using the given `context` and `originalReference`. + /// + /// The resulting ``TopicReferenceResolutionError`` is human-readable and provides helpful solutions. + /// + /// - Parameters: + /// - context: The ``DocumentationContext`` the `originalReference` was resolved in. + /// - originalReference: The raw input string that represents the body of the reference that failed to resolve. + /// + /// - Note: `Replacement`s produced by this function use `SourceLocation`s relative to the `originalReference`, i.e. the beginning + /// of the _body_ of the original reference. + func asTopicReferenceResolutionError(context: DocumentationContext, originalReference: String) -> TopicReferenceResolutionError { switch self { - case .partialResult(let partialResult, let remaining, let available): - let nearMisses = NearMiss.bestMatches(for: available, against: remaining) - let suggestion: String - switch nearMisses.count { - case 0: - suggestion = "No similar pages. Available children: \(available.joined(separator: ", "))." - case 1: - suggestion = "Did you mean: \(nearMisses[0])?" - default: - suggestion = "Did you mean one of: \(nearMisses.joined(separator: ", "))?" - } - return "Reference at \(partialResult.pathWithoutDisambiguation().singleQuoted) can't resolve \(remaining.singleQuoted). \(suggestion)" - case .notFound, .unfindableMatch: - return "No local documentation matches this reference." - + case .notFound(availableChildren: let availableChildren): + return TopicReferenceResolutionError("No local documentation matches this reference.", note: availabilityNote(category: "top-level elements", candidates: availableChildren)) + case .unfindableMatch: + return TopicReferenceResolutionError("No local documentation matches this reference.") case .nonSymbolMatchForSymbolLink: - return "Symbol links can only resolve symbols." + return TopicReferenceResolutionError("Symbol links can only resolve symbols.", solutions: [ + Solution(summary: "Use a '' style reference.", replacements: [ + // the SourceRange points to the opening double-backtick + Replacement(range: SourceLocation(line: 0, column: -2, source: nil).."), + ]) + ]) + case .partialResult(partialResult: let partialResult, remainingSubpath: let remainingSubpath, availableChildren: let availableChildren): + let nearMisses = NearMiss.bestMatches(for: availableChildren, against: remainingSubpath) - case .lookupCollision(let partialResult, _, let collisions): - let collisionDescription = collisions.map { "Append '-\($0.disambiguation)' to refer to \($0.node.fullNameOfValue(context: context).singleQuoted)" }.sorted() - return "Reference is ambiguous after \(partialResult.pathWithoutDisambiguation().singleQuoted): \(collisionDescription.joined(separator: ". "))." - } - } - - /// Generates replacements for a faulty `originalPath` that fix the first unresolvable path segment - /// by adding disambiguation suffixes or provding near-miss alternatives. - /// - /// The replacement options only fix the first faulty segment in a reference. They do not alter the valid prefix and keep - /// the unchecked suffix in place. - func replacements(for originalPath: String) -> [String] { - switch self { - case .partialResult(_, let remaining, let available): - var validPrefix = originalPath - validPrefix.removeLast(remaining.count) + let validPrefix = originalReference.dropLast(remainingSubpath.count) + + let unprocessedSuffix = remainingSubpath.suffix(from: remainingSubpath.firstIndex(of: "/") ?? remainingSubpath.endIndex) + + let replacementRange = SourceLocation(line: 0, column: validPrefix.count, source: nil).. lowerBoundOfLastSegment ? lowerBoundOfLastDisambiguation : nil + let validPrefix = prefixPotentiallyIncludingInvalidDisambiguation[..<(lowerBoundOfInvalidDisambiguation ?? prefixPotentiallyIncludingInvalidDisambiguation.endIndex)] + + let replacementRange = SourceLocation(line: 0, column: validPrefix.count, source: nil).. String { + switch candidates.count { + case 0: + return "No \(category) available." + default: + return "Available \(category): \(candidates.joined(separator: ", "))." } } } diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift index f75844d407..488518e0aa 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift @@ -207,7 +207,7 @@ final class PathHierarchyBasedLinkResolver { // Return the successful or failed externally resolved reference. return resolvedExternalReference } else if !context.registeredBundles.contains(where: { $0.identifier == bundleID }) { - return .failure(unresolvedReference, errorMessage: "No external resolver registered for \(bundleID.singleQuoted).") + return .failure(unresolvedReference, TopicReferenceResolutionError("No external resolver registered for \(bundleID.singleQuoted).")) } } @@ -227,7 +227,7 @@ final class PathHierarchyBasedLinkResolver { originalReferenceString += "#" + fragment } - return .failure(unresolvedReference.with(canidates: error.replacements(for: originalReferenceString)), errorMessage: error.errorMessage(context: context)) + return .failure(unresolvedReference, error.asTopicReferenceResolutionError(context: context, originalReference: originalReferenceString)) } } catch { fatalError("Only SymbolPathTree.Error errors are raised from the symbol link resolution code above.") diff --git a/Sources/SwiftDocC/Model/DocumentationNode.swift b/Sources/SwiftDocC/Model/DocumentationNode.swift index 9c0e5d2c58..60d3a86f5c 100644 --- a/Sources/SwiftDocC/Model/DocumentationNode.swift +++ b/Sources/SwiftDocC/Model/DocumentationNode.swift @@ -429,7 +429,7 @@ public struct DocumentationNode { var problem = Problem(diagnostic: diagnostic, possibleSolutions: []) if let offset = docComment.lines.first?.range { - problem = problem.offsetedWithRange(offset) + problem.offsetWithRange(offset) } engine.emit(problem) diff --git a/Sources/SwiftDocC/Model/Identifier.swift b/Sources/SwiftDocC/Model/Identifier.swift index c0249f0ca9..8b6d12243c 100644 --- a/Sources/SwiftDocC/Model/Identifier.swift +++ b/Sources/SwiftDocC/Model/Identifier.swift @@ -10,6 +10,7 @@ import Foundation import SymbolKit +import Markdown /// A resolved or unresolved reference to a piece of documentation. /// @@ -54,7 +55,7 @@ public enum TopicReferenceResolutionResult: Hashable, CustomStringConvertible { /// A topic reference that has successfully been resolved to known documentation. case success(ResolvedTopicReference) /// A topic reference that has failed to resolve to known documentation and an error message with information about why the reference failed to resolve. - case failure(UnresolvedTopicReference, errorMessage: String) + case failure(UnresolvedTopicReference, TopicReferenceResolutionError) public var description: String { switch self { @@ -66,6 +67,78 @@ public enum TopicReferenceResolutionResult: Hashable, CustomStringConvertible { } } +/// The error causing the failure in the resolution of a ``TopicReference``. +public struct TopicReferenceResolutionError: Error { + // we store the base as an `Error` so that + // we can potentailly pass through a `DescribedError` + // conformance in the future + private let base: Error + private let solutions: [Solution] + + init(_ error: Error, solutions: [Solution] = []) { + self.base = error + self.solutions = solutions + } + + var localizedDescription: String { + base.localizedDescription + } + + public var note: String? { + (base as? GenericBaseError)?.note + } +} + +extension TopicReferenceResolutionError { + init(_ message: String, note: String? = nil, solutions: [Solution] = []) { + self.base = GenericBaseError(message: message, note: note) + self.solutions = solutions + } + + private struct GenericBaseError: DescribedError { + let message: String + let note: String? + + var errorDescription: String { + message + } + + var recoverySuggestion: String? { + note + } + } +} + +extension TopicReferenceResolutionError: Hashable { + public static func == (lhs: TopicReferenceResolutionError, rhs: TopicReferenceResolutionError) -> Bool { + (lhs.base as CustomStringConvertible).description == (rhs.base as CustomStringConvertible).description + } + + public func hash(into hasher: inout Hasher) { + (base as CustomStringConvertible).description.hash(into: &hasher) + } +} + +extension TopicReferenceResolutionError { + /// Extracts any `Solution`s from this error, if available. + /// + /// The error can provide `Solution`s if appropriate. Since the absolute location of + /// the faulty reference is not known at the error's origin, the `Replacement`s + /// will use `SourceLocation`s relative to the reference text. Provide range of the + /// reference **body** to obtain correctly placed `Replacement`s. + func solutions(referenceSourceRange: SourceRange) -> [Solution] { + var solutions = self.solutions + + for i in solutions.indices { + for j in solutions[i].replacements.indices { + solutions[i].replacements[j].offsetWithRange(referenceSourceRange) + } + } + + return solutions + } +} + /// A reference to a piece of documentation which has been verified to exist. /// /// A `ResolvedTopicReference` refers to some piece of documentation, such as an article or symbol. @@ -489,12 +562,6 @@ public struct UnresolvedTopicReference: Hashable, CustomStringConvertible { /// An optional title. public var title: String? = nil - /// Optional candidates in case the reference couldn't be resolved. - /// - /// A resolution algorithm may attach possible candidates for an unresolvable reference to this field - /// for usage in diagnostics and fixits. - var canidates: [String] = [] - /// Creates a new unresolved reference from another unresolved reference with a resolved parent reference. /// - Parameters: /// - parent: The resolved parent reference of the unresolved reference. @@ -536,15 +603,6 @@ public struct UnresolvedTopicReference: Hashable, CustomStringConvertible { } } -extension UnresolvedTopicReference { - /// Create a copy of this reference that stores the given `candidates`. - func with(canidates: C) -> Self where C.Element == String { - var copy = self - copy.canidates.append(contentsOf: canidates) - return copy - } -} - /** A reference to an auxiliary resource such as an image. */ diff --git a/Sources/SwiftDocC/Semantics/MarkupReferenceResolver.swift b/Sources/SwiftDocC/Semantics/MarkupReferenceResolver.swift index c85e9aa5bc..45dc334f35 100644 --- a/Sources/SwiftDocC/Semantics/MarkupReferenceResolver.swift +++ b/Sources/SwiftDocC/Semantics/MarkupReferenceResolver.swift @@ -63,15 +63,15 @@ struct MarkupReferenceResolver: MarkupRewriter { } return resolved - case .failure(let unresolved, let errorMessage): + case .failure(let unresolved, let error): if let callback = problemForUnresolvedReference, - let problem = callback(unresolved, source, range, fromSymbolLink, errorMessage) { + let problem = callback(unresolved, source, range, fromSymbolLink, error.localizedDescription) { problems.append(problem) return nil } let uncuratedArticleMatch = context.uncuratedArticles[bundle.articlesDocumentationRootReference.appendingPathOfReference(unresolved)]?.source - problems.append(unresolvedReferenceProblem(reference: reference, source: source, range: range, severity: severity, uncuratedArticleMatch: uncuratedArticleMatch, underlyingErrorMessage: errorMessage, candidates: unresolved.canidates)) + problems.append(unresolvedReferenceProblem(reference: reference, source: source, range: range, severity: severity, uncuratedArticleMatch: uncuratedArticleMatch, underlyingError: error, fromSymbolLink: fromSymbolLink)) return nil } } diff --git a/Sources/SwiftDocC/Semantics/ReferenceResolver.swift b/Sources/SwiftDocC/Semantics/ReferenceResolver.swift index 5adf3f3efc..64cc0a11cd 100644 --- a/Sources/SwiftDocC/Semantics/ReferenceResolver.swift +++ b/Sources/SwiftDocC/Semantics/ReferenceResolver.swift @@ -11,19 +11,29 @@ import Foundation import Markdown -func unresolvedReferenceProblem(reference: TopicReference, source: URL?, range: SourceRange?, severity: DiagnosticSeverity, uncuratedArticleMatch: URL?, underlyingErrorMessage: String, candidates: [String]) -> Problem { - let notes = uncuratedArticleMatch.map { +func unresolvedReferenceProblem(reference: TopicReference, source: URL?, range: SourceRange?, severity: DiagnosticSeverity, uncuratedArticleMatch: URL?, underlyingError: TopicReferenceResolutionError, fromSymbolLink: Bool) -> Problem { + var notes = uncuratedArticleMatch.map { [DiagnosticNote(source: $0, range: SourceLocation(line: 1, column: 1, source: nil).. + : SourceLocation(line: range.lowerBound.line, column: range.lowerBound.column+5, source: range.lowerBound.source)..(in sequence: S, - keyedBy key: KeyPath, - using assertions: [K: (S.Element) throws -> Void], - allowUnusedAssertions: Bool = false, - allowOtherContent: Bool = true) throws where K: Hashable, S: Sequence { - var assertionWasUsed: [K: Bool] = [:] - for element in sequence { - if let assertion = assertions[element[keyPath: key]] { - try assertion(element) - assertionWasUsed[element[keyPath: key]] = true - } else if !allowOtherContent { - XCTFail("The given sequence contained an element keyed '\(element[keyPath: key])', but no assertion was given for that key.") - } - } - - if !allowUnusedAssertions { - for key in assertions.keys { - XCTAssertEqual(assertionWasUsed[key], true, "No element in the given 'sequence' used the key '\(key)'") - } - } -} diff --git a/Tests/SwiftDocCTests/Diagnostics/DiagnosticTests.swift b/Tests/SwiftDocCTests/Diagnostics/DiagnosticTests.swift index 8b571eb4e7..bd07682118 100644 --- a/Tests/SwiftDocCTests/Diagnostics/DiagnosticTests.swift +++ b/Tests/SwiftDocCTests/Diagnostics/DiagnosticTests.swift @@ -93,7 +93,10 @@ class DiagnosticTests: XCTestCase { let offset = SymbolGraph.LineList.SourceRange(start: .init(line: 10, character: 10), end: .init(line: 10, character: 20)) - XCTAssertEqual((resolver.problems.first)?.offsetedWithRange(offset).diagnostic.range, SourceLocation(line: 11, column: 18, source: nil).. Int { get }'. \ - Append '-type.method' to refer to 'static func `subscript`()'. - """) + try assertPathRaisesErrorMessage("/MixedFramework/CollisionsWithEscapedKeywords/subscript()", in: tree, context: context, expectedErrorMessage: "Reference is ambiguous after '/MixedFramework/CollisionsWithEscapedKeywords'.") // public enum CollisionsWithDifferentFunctionArguments { // public func something(argument: Int) -> Int { 0 } @@ -341,11 +329,7 @@ class PathHierarchyTests: XCTestCase { (symbolID: "s:14MixedFramework40CollisionsWithDifferentFunctionArgumentsO9something8argumentS2i_tF", disambiguation: "1cyvp"), (symbolID: "s:14MixedFramework40CollisionsWithDifferentFunctionArgumentsO9something8argumentSiSS_tF", disambiguation: "2vke2"), ]) - try assertPathRaisesErrorMessage("/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)", in: tree, context: context, expectedErrorMessage: """ - Reference is ambiguous after '/MixedFramework/CollisionsWithDifferentFunctionArguments': \ - Append '-1cyvp' to refer to 'func something(argument: Int) -> Int'. \ - Append '-2vke2' to refer to 'func something(argument: String) -> Int'. - """) + try assertPathRaisesErrorMessage("/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)", in: tree, context: context, expectedErrorMessage: "Reference is ambiguous after '/MixedFramework/CollisionsWithDifferentFunctionArguments'.") // public enum CollisionsWithDifferentSubscriptArguments { // public subscript(something: Int) -> Int { 0 } @@ -355,11 +339,7 @@ class PathHierarchyTests: XCTestCase { (symbolID: "s:14MixedFramework41CollisionsWithDifferentSubscriptArgumentsOyS2icip", disambiguation: "4fd0l"), (symbolID: "s:14MixedFramework41CollisionsWithDifferentSubscriptArgumentsOySiSScip", disambiguation: "757cj"), ]) - try assertPathRaisesErrorMessage("/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)", in: tree, context: context, expectedErrorMessage: """ - Reference is ambiguous after '/MixedFramework/CollisionsWithDifferentSubscriptArguments': \ - Append '-4fd0l' to refer to 'subscript(something: Int) -> Int { get }'. \ - Append '-757cj' to refer to 'subscript(somethingElse: String) -> Int { get }'. - """) + try assertPathRaisesErrorMessage("/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)", in: tree, context: context, expectedErrorMessage: "Reference is ambiguous after '/MixedFramework/CollisionsWithDifferentSubscriptArguments'.") // typedef NS_OPTIONS(NSInteger, MyObjectiveCOption) { // MyObjectiveCOptionNone = 0, @@ -865,11 +845,7 @@ class PathHierarchyTests: XCTestCase { ("s:5MyKit0A5MyProtocol0Afunc()DefaultImp", "2dxqn"), ("s:5MyKit0A5MyProtocol0Afunc()", "6ijsi"), ]) - try assertPathRaisesErrorMessage("/SideKit/SideProtocol/func()", in: tree, context: context, expectedErrorMessage: """ - Reference is ambiguous after '/SideKit/SideProtocol': \ - Append '-2dxqn' to refer to 'func1()'. \ - Append '-6ijsi' to refer to 'func1()'. - """) // This test data have the same declaration for both symbols. + try assertPathRaisesErrorMessage("/SideKit/SideProtocol/func()", in: tree, context: context, expectedErrorMessage: "Reference is ambiguous after '/SideKit/SideProtocol'.") // This test data have the same declaration for both symbols. try assertFindsPath("/FillIntroduced/iOSOnlyDeprecated()", in: tree, asSymbolID: "s:14FillIntroduced17iOSOnlyDeprecatedyyF") try assertFindsPath("/FillIntroduced/macCatalystOnlyIntroduced()", in: tree, asSymbolID: "s:14FillIntroduced015macCatalystOnlyB0yyF") @@ -910,12 +886,7 @@ class PathHierarchyTests: XCTestCase { ("c:@E@Foo", "struct"), ("c:MixedLanguageFramework.h@T@Foo", "typealias"), ]) - try assertPathRaisesErrorMessage("MixedLanguageFramework/Foo", in: tree, context: context, expectedErrorMessage: """ - Reference is ambiguous after '/MixedLanguageFramework': \ - Append '-enum' to refer to 'typedef enum Foo : NSString { ... } Foo;'. \ - Append '-struct' to refer to 'struct Foo'. \ - Append '-typealias' to refer to 'typedef enum Foo : NSString { ... } Foo;'. - """) // The 'enum' and 'typealias' symbols have multi-line declarations that are presented on a single line + try assertPathRaisesErrorMessage("MixedLanguageFramework/Foo", in: tree, context: context, expectedErrorMessage: "Reference is ambiguous after '/MixedLanguageFramework'.") // The 'enum' and 'typealias' symbols have multi-line declarations that are presented on a single line try assertFindsPath("MixedLanguageFramework/Foo/first", in: tree, asSymbolID: "c:@E@Foo@first") @@ -1282,8 +1253,8 @@ class PathHierarchyTests: XCTestCase { private func assertPathRaisesErrorMessage(_ path: String, in tree: PathHierarchy, context: DocumentationContext, expectedErrorMessage: String, file: StaticString = #file, line: UInt = #line) throws { XCTAssertThrowsError(try tree.findSymbol(path: path), "Finding path \(path) didn't raise an error.",file: file,line: line) { untypedError in let error = untypedError as! PathHierarchy.Error - let errorMessage = error.errorMessage(context: context) - XCTAssertEqual(errorMessage, expectedErrorMessage, file: file, line: line) + let referenceError = error.asTopicReferenceResolutionError(context: context, originalReference: path) + XCTAssertEqual(referenceError.localizedDescription, expectedErrorMessage, file: file, line: line) } } diff --git a/Tests/SwiftDocCTests/Semantics/SymbolTests.swift b/Tests/SwiftDocCTests/Semantics/SymbolTests.swift index 91ae6c07a6..a516bcbbd6 100644 --- a/Tests/SwiftDocCTests/Semantics/SymbolTests.swift +++ b/Tests/SwiftDocCTests/Semantics/SymbolTests.swift @@ -544,11 +544,13 @@ class SymbolTests: XCTestCase { - ``otherFunction()`` - ``/MyKit/MyClas`` - ``MyKit/MyClas/myFunction()`` + - ### Ambiguous curation - ``init()`` - ``MyClass/init()-swift.init`` + - """ let documentationExtensionURL = url.appendingPathComponent("documentation/myclass.md") @@ -560,223 +562,386 @@ class SymbolTests: XCTestCase { XCTAssertTrue(unresolvedTopicProblems.contains(where: { $0.diagnostic.localizedSummary == "Topic reference 'doc://com.test.external/ExternalPage' couldn't be resolved. No external resolver registered for 'com.test.external'." })) if LinkResolutionMigrationConfiguration.shouldUseHierarchyBasedLinkResolver { - try XCTAssertElements(in: unresolvedTopicProblems, keyedBy: \.diagnostic.localizedSummary, using: [ - "Topic reference 'UnresolvableSymbolLinkInMyClassOverview<>(_:))' couldn't be resolved. Reference at '/MyKit/MyClass' can't resolve 'UnresolvableSymbolLinkInMyClassOverview<>(_:))'. No similar pages. Available children: init(), myFunction().": { problem in - XCTAssert(problem.possibleSolutions.isEmpty) - }, - "Topic reference 'UnresolvableClassInMyClassTopicCuration' couldn't be resolved. Reference at '/MyKit/MyClass' can't resolve 'UnresolvableClassInMyClassTopicCuration'. No similar pages. Available children: init(), myFunction().": { problem in - XCTAssert(problem.possibleSolutions.isEmpty) - }, - "Topic reference 'MyClass/unresolvablePropertyInMyClassTopicCuration' couldn't be resolved. Reference at '/MyKit/MyClass' can't resolve 'unresolvablePropertyInMyClassTopicCuration'. No similar pages. Available children: init(), myFunction().": { problem in - XCTAssert(problem.possibleSolutions.isEmpty) - }, - "Topic reference 'init()' couldn't be resolved. Reference is ambiguous after '/MyKit/MyClass': Append '-33vaw' to refer to 'init()'. Append '-3743d' to refer to 'init()'.": { problem in - XCTAssertEqual(problem.possibleSolutions.count, 2) - XCTAssert(problem.possibleSolutions.map(\.replacements.count).allSatisfy { $0 == 1 }) - XCTAssertEqual(problem.possibleSolutions.map(\.replacements.first!.replacement).sorted(), [ - "``init()-33vaw``", - "``init()-3743d``" - ]) - - XCTAssertEqual(try problem.possibleSolutions.sorted(by: \.replacements.first!.replacement).first!.apply(to: url.appendingPathComponent("documentation/myclass.md")), """ - # ``MyKit/MyClass`` - - @Metadata { - @DocumentationExtension(mergeBehavior: override) - } + var problem: Problem + + print(unresolvedTopicProblems.map(\.diagnostic.localizedSummary)) + + problem = try XCTUnwrap(unresolvedTopicProblems.first(where: { $0.diagnostic.localizedSummary == "Topic reference 'UnresolvableSymbolLinkInMyClassOverview<>(_:))' couldn't be resolved. Reference at '/MyKit/MyClass' can't resolve 'UnresolvableSymbolLinkInMyClassOverview<>(_:))'. No similar pages." })) + XCTAssertEqual(problem.diagnostic.notes.map(\.message), ["Available children: init(), myFunction()."]) + XCTAssertEqual(problem.possibleSolutions.count, 2) + + + problem = try XCTUnwrap(unresolvedTopicProblems.first(where: { $0.diagnostic.localizedSummary == "Topic reference 'UnresolvableClassInMyClassTopicCuration' couldn't be resolved. Reference at '/MyKit/MyClass' can't resolve 'UnresolvableClassInMyClassTopicCuration'. No similar pages." })) + XCTAssertEqual(problem.diagnostic.notes.map(\.message), ["Available children: init(), myFunction()."]) + XCTAssertEqual(problem.possibleSolutions.count, 2) - A cool API to call. + + problem = try XCTUnwrap(unresolvedTopicProblems.first(where: { $0.diagnostic.localizedSummary == "Topic reference 'MyClass/unresolvablePropertyInMyClassTopicCuration' couldn't be resolved. Reference at '/MyKit/MyClass' can't resolve 'unresolvablePropertyInMyClassTopicCuration'. No similar pages." })) + XCTAssertEqual(problem.diagnostic.notes.map(\.message), ["Available children: init(), myFunction()."]) + XCTAssertEqual(problem.possibleSolutions.count, 2) - This overview has an ``UnresolvableSymbolLinkInMyClassOverview<>(_:))``. + + problem = try XCTUnwrap(unresolvedTopicProblems.first(where: { $0.diagnostic.localizedSummary == "Topic reference 'init()' couldn't be resolved. Reference is ambiguous after '/MyKit/MyClass'." })) + XCTAssert(problem.diagnostic.notes.isEmpty) + XCTAssertEqual(problem.possibleSolutions.count, 2) + XCTAssert(problem.possibleSolutions.map(\.replacements.count).allSatisfy { $0 == 1 }) + XCTAssertEqual(problem.possibleSolutions.map { [$0.summary, $0.replacements.first!.replacement] }, [ + ["Insert disambiguation suffix for 'init()'", "-33vaw"], + ["Insert disambiguation suffix for 'init()'", "-3743d"], + ]) + XCTAssertEqual(try problem.possibleSolutions.first!.apply(to: url.appendingPathComponent("documentation/myclass.md")), """ + # ``MyKit/MyClass`` - - Parameters: - - name: A parameter - - Returns: Return value + @Metadata { + @DocumentationExtension(mergeBehavior: override) + } - ## Topics + A cool API to call. - ### Unresolvable curation + This overview has an ``UnresolvableSymbolLinkInMyClassOverview<>(_:))``. - - ``UnresolvableClassInMyClassTopicCuration`` - - ``MyClass/unresolvablePropertyInMyClassTopicCuration`` - - + - Parameters: + - name: A parameter + - Returns: Return value - ### Near Miss + ## Topics - - ``otherFunction()`` - - ``/MyKit/MyClas`` - - ``MyKit/MyClas/myFunction()`` + ### Unresolvable curation - ### Ambiguous curation + - ``UnresolvableClassInMyClassTopicCuration`` + - ``MyClass/unresolvablePropertyInMyClassTopicCuration`` + - - - ``init()-33vaw`` - - ``MyClass/init()-swift.init`` - """) - }, - "Topic reference 'MyClass/init()-swift.init' couldn't be resolved. Reference is ambiguous after '/MyKit/MyClass': Append '-33vaw' to refer to 'init()'. Append '-3743d' to refer to 'init()'.": { problem in - XCTAssertEqual(problem.possibleSolutions.count, 2) - XCTAssert(problem.possibleSolutions.map(\.replacements.count).allSatisfy { $0 == 1 }) - XCTAssertEqual(problem.possibleSolutions.map(\.replacements.first!.replacement).sorted(), [ - "``MyClass/init()-33vaw``", - "``MyClass/init()-3743d``" - ]) - - XCTAssertEqual(try problem.possibleSolutions.sorted(by: \.replacements.first!.replacement).first!.apply(to: url.appendingPathComponent("documentation/myclass.md")), """ - # ``MyKit/MyClass`` + ### Near Miss - @Metadata { - @DocumentationExtension(mergeBehavior: override) - } + - ``otherFunction()`` + - ``/MyKit/MyClas`` + - ``MyKit/MyClas/myFunction()`` + - - A cool API to call. + ### Ambiguous curation - This overview has an ``UnresolvableSymbolLinkInMyClassOverview<>(_:))``. + - ``init()-33vaw`` + - ``MyClass/init()-swift.init`` + - + """) + + + problem = try XCTUnwrap(unresolvedTopicProblems.first(where: { $0.diagnostic.localizedSummary == "Topic reference 'MyClass/init()-swift.init' couldn't be resolved. Reference is ambiguous after '/MyKit/MyClass'." })) + XCTAssert(problem.diagnostic.notes.isEmpty) + XCTAssertEqual(problem.possibleSolutions.count, 2) + XCTAssert(problem.possibleSolutions.map(\.replacements.count).allSatisfy { $0 == 1 }) + XCTAssertEqual(problem.possibleSolutions.map { [$0.summary, $0.replacements.first!.replacement] }, [ + ["Insert disambiguation suffix for 'init()'", "-33vaw"], + ["Insert disambiguation suffix for 'init()'", "-3743d"], + ]) + XCTAssertEqual(try problem.possibleSolutions.first!.apply(to: url.appendingPathComponent("documentation/myclass.md")), """ + # ``MyKit/MyClass`` - - Parameters: - - name: A parameter - - Returns: Return value + @Metadata { + @DocumentationExtension(mergeBehavior: override) + } - ## Topics + A cool API to call. - ### Unresolvable curation + This overview has an ``UnresolvableSymbolLinkInMyClassOverview<>(_:))``. - - ``UnresolvableClassInMyClassTopicCuration`` - - ``MyClass/unresolvablePropertyInMyClassTopicCuration`` - - - - ### Near Miss + - Parameters: + - name: A parameter + - Returns: Return value - - ``otherFunction()`` - - ``/MyKit/MyClas`` - - ``MyKit/MyClas/myFunction()`` + ## Topics - ### Ambiguous curation + ### Unresolvable curation - - ``init()`` - - ``MyClass/init()-33vaw`` - """) - }, - "Topic reference 'otherFunction()' couldn't be resolved. Reference at '/MyKit/MyClass' can't resolve 'otherFunction()'. Did you mean: myFunction()?": { problem in - XCTAssertEqual(problem.possibleSolutions.count, 1) - XCTAssertEqual(problem.possibleSolutions.first!.replacements.count, 1) - XCTAssertEqual(problem.possibleSolutions.first!.replacements.first!.replacement, "``myFunction()``") - - XCTAssertEqual(try problem.possibleSolutions.first!.apply(to: url.appendingPathComponent("documentation/myclass.md")), """ - # ``MyKit/MyClass`` + - ``UnresolvableClassInMyClassTopicCuration`` + - ``MyClass/unresolvablePropertyInMyClassTopicCuration`` + - - @Metadata { - @DocumentationExtension(mergeBehavior: override) - } + ### Near Miss - A cool API to call. + - ``otherFunction()`` + - ``/MyKit/MyClas`` + - ``MyKit/MyClas/myFunction()`` + - - This overview has an ``UnresolvableSymbolLinkInMyClassOverview<>(_:))``. + ### Ambiguous curation - - Parameters: - - name: A parameter - - Returns: Return value + - ``init()`` + - ``MyClass/init()-33vaw`` + - + """) + + + problem = try XCTUnwrap(unresolvedTopicProblems.first(where: { $0.diagnostic.localizedSummary == "Topic reference 'doc:MyClass/init()-swift.init' couldn't be resolved. Reference is ambiguous after '/MyKit/MyClass'." })) + XCTAssert(problem.diagnostic.notes.isEmpty) + XCTAssertEqual(problem.possibleSolutions.count, 2) + XCTAssert(problem.possibleSolutions.map(\.replacements.count).allSatisfy { $0 == 1 }) + XCTAssertEqual(problem.possibleSolutions.map { [$0.summary, $0.replacements.first!.replacement] }, [ + ["Insert disambiguation suffix for 'init()'", "-33vaw"], + ["Insert disambiguation suffix for 'init()'", "-3743d"], + ]) + XCTAssertEqual(try problem.possibleSolutions.first!.apply(to: url.appendingPathComponent("documentation/myclass.md")), """ + # ``MyKit/MyClass`` - ## Topics + @Metadata { + @DocumentationExtension(mergeBehavior: override) + } - ### Unresolvable curation + A cool API to call. - - ``UnresolvableClassInMyClassTopicCuration`` - - ``MyClass/unresolvablePropertyInMyClassTopicCuration`` - - - - ### Near Miss + This overview has an ``UnresolvableSymbolLinkInMyClassOverview<>(_:))``. - - ``myFunction()`` - - ``/MyKit/MyClas`` - - ``MyKit/MyClas/myFunction()`` + - Parameters: + - name: A parameter + - Returns: Return value - ### Ambiguous curation + ## Topics - - ``init()`` - - ``MyClass/init()-swift.init`` - """) - }, - "Topic reference '/MyKit/MyClas' couldn't be resolved. Reference at '/MyKit' can't resolve 'MyClas'. Did you mean: MyClass?": { problem in - XCTAssertEqual(problem.possibleSolutions.count, 1) - XCTAssertEqual(problem.possibleSolutions.first!.replacements.count, 1) - XCTAssertEqual(problem.possibleSolutions.first!.replacements.first!.replacement, "``/MyKit/MyClass``") - - XCTAssertEqual(try problem.possibleSolutions.first!.apply(to: url.appendingPathComponent("documentation/myclass.md")), """ - # ``MyKit/MyClass`` + ### Unresolvable curation - @Metadata { - @DocumentationExtension(mergeBehavior: override) - } + - ``UnresolvableClassInMyClassTopicCuration`` + - ``MyClass/unresolvablePropertyInMyClassTopicCuration`` + - - A cool API to call. + ### Near Miss - This overview has an ``UnresolvableSymbolLinkInMyClassOverview<>(_:))``. + - ``otherFunction()`` + - ``/MyKit/MyClas`` + - ``MyKit/MyClas/myFunction()`` + - - - Parameters: - - name: A parameter - - Returns: Return value + ### Ambiguous curation - ## Topics + - ``init()`` + - ``MyClass/init()-swift.init`` + - + """) - ### Unresolvable curation + + problem = try XCTUnwrap(unresolvedTopicProblems.first(where: { $0.diagnostic.localizedSummary == "Topic reference 'otherFunction()' couldn't be resolved. Reference at '/MyKit/MyClass' can't resolve 'otherFunction()'. Did you mean myFunction()?" })) + XCTAssertEqual(problem.diagnostic.notes.map(\.message), ["Available children: init(), myFunction()."]) + XCTAssertEqual(problem.possibleSolutions.count, 1) + XCTAssert(problem.possibleSolutions.map(\.replacements.count).allSatisfy { $0 == 1 }) + XCTAssertEqual(problem.possibleSolutions.map { [$0.summary, $0.replacements.first!.replacement] }, [ + ["Correct reference to myFunction().", "myFunction()"], + ]) + XCTAssertEqual(try problem.possibleSolutions.first!.apply(to: url.appendingPathComponent("documentation/myclass.md")), """ + # ``MyKit/MyClass`` - - ``UnresolvableClassInMyClassTopicCuration`` - - ``MyClass/unresolvablePropertyInMyClassTopicCuration`` - - - - ### Near Miss + @Metadata { + @DocumentationExtension(mergeBehavior: override) + } - - ``otherFunction()`` - - ``/MyKit/MyClass`` - - ``MyKit/MyClas/myFunction()`` + A cool API to call. - ### Ambiguous curation + This overview has an ``UnresolvableSymbolLinkInMyClassOverview<>(_:))``. - - ``init()`` - - ``MyClass/init()-swift.init`` - """) - }, - "Topic reference 'MyKit/MyClas/myFunction()' couldn't be resolved. Reference at '/MyKit' can't resolve 'MyClas/myFunction()'. Did you mean: MyClass?": { problem in - XCTAssertEqual(problem.possibleSolutions.count, 1) - XCTAssertEqual(problem.possibleSolutions.first!.replacements.count, 1) - XCTAssertEqual(problem.possibleSolutions.first!.replacements.first!.replacement, "``MyKit/MyClass/myFunction()``") - - XCTAssertEqual(try problem.possibleSolutions.first!.apply(to: url.appendingPathComponent("documentation/myclass.md")), """ - # ``MyKit/MyClass`` + - Parameters: + - name: A parameter + - Returns: Return value - @Metadata { - @DocumentationExtension(mergeBehavior: override) - } + ## Topics - A cool API to call. + ### Unresolvable curation - This overview has an ``UnresolvableSymbolLinkInMyClassOverview<>(_:))``. + - ``UnresolvableClassInMyClassTopicCuration`` + - ``MyClass/unresolvablePropertyInMyClassTopicCuration`` + - - - Parameters: - - name: A parameter - - Returns: Return value + ### Near Miss - ## Topics + - ``myFunction()`` + - ``/MyKit/MyClas`` + - ``MyKit/MyClas/myFunction()`` + - - ### Unresolvable curation + ### Ambiguous curation - - ``UnresolvableClassInMyClassTopicCuration`` - - ``MyClass/unresolvablePropertyInMyClassTopicCuration`` - - - - ### Near Miss + - ``init()`` + - ``MyClass/init()-swift.init`` + - + """) + + + problem = try XCTUnwrap(unresolvedTopicProblems.first(where: { $0.diagnostic.localizedSummary == "Topic reference '/MyKit/MyClas' couldn't be resolved. Reference at '/MyKit' can't resolve 'MyClas'. Did you mean MyClass?" })) + XCTAssertEqual(problem.diagnostic.notes.map(\.message), ["Available children: Discussion, globalFunction(_:considering:), MyClass, MyProtocol."]) + XCTAssertEqual(problem.possibleSolutions.count, 1) + XCTAssert(problem.possibleSolutions.map(\.replacements.count).allSatisfy { $0 == 1 }) + XCTAssertEqual(problem.possibleSolutions.map { [$0.summary, $0.replacements.first!.replacement] }, [ + ["Correct reference to /MyKit/MyClass.", "MyClass"], + ]) + XCTAssertEqual(try problem.possibleSolutions.first!.apply(to: url.appendingPathComponent("documentation/myclass.md")), """ + # ``MyKit/MyClass`` - - ``otherFunction()`` - - ``/MyKit/MyClas`` - - ``MyKit/MyClass/myFunction()`` + @Metadata { + @DocumentationExtension(mergeBehavior: override) + } - ### Ambiguous curation + A cool API to call. - - ``init()`` - - ``MyClass/init()-swift.init`` - """) - } + This overview has an ``UnresolvableSymbolLinkInMyClassOverview<>(_:))``. + + - Parameters: + - name: A parameter + - Returns: Return value + + ## Topics + + ### Unresolvable curation + + - ``UnresolvableClassInMyClassTopicCuration`` + - ``MyClass/unresolvablePropertyInMyClassTopicCuration`` + - + + ### Near Miss + + - ``otherFunction()`` + - ``/MyKit/MyClass`` + - ``MyKit/MyClas/myFunction()`` + - + + ### Ambiguous curation + + - ``init()`` + - ``MyClass/init()-swift.init`` + - + """) + + + problem = try XCTUnwrap(unresolvedTopicProblems.first(where: { $0.diagnostic.localizedSummary == "Topic reference 'MyKit/MyClas/myFunction()' couldn't be resolved. Reference at '/MyKit' can't resolve 'MyClas/myFunction()'. Did you mean MyClass?" })) + XCTAssertEqual(problem.diagnostic.notes.map(\.message), ["Available children: Discussion, globalFunction(_:considering:), MyClass, MyProtocol."]) + XCTAssertEqual(problem.possibleSolutions.count, 1) + XCTAssert(problem.possibleSolutions.map(\.replacements.count).allSatisfy { $0 == 1 }) + XCTAssertEqual(problem.possibleSolutions.map { [$0.summary, $0.replacements.first!.replacement] }, [ + ["Correct reference to MyKit/MyClass/myFunction().", "MyClass"], + ]) + XCTAssertEqual(try problem.possibleSolutions.first!.apply(to: url.appendingPathComponent("documentation/myclass.md")), """ + # ``MyKit/MyClass`` + + @Metadata { + @DocumentationExtension(mergeBehavior: override) + } + + A cool API to call. + + This overview has an ``UnresolvableSymbolLinkInMyClassOverview<>(_:))``. + + - Parameters: + - name: A parameter + - Returns: Return value + + ## Topics + + ### Unresolvable curation + + - ``UnresolvableClassInMyClassTopicCuration`` + - ``MyClass/unresolvablePropertyInMyClassTopicCuration`` + - + + ### Near Miss + + - ``otherFunction()`` + - ``/MyKit/MyClas`` + - ``MyKit/MyClass/myFunction()`` + - + + ### Ambiguous curation + + - ``init()`` + - ``MyClass/init()-swift.init`` + - + """) + + + problem = try XCTUnwrap(unresolvedTopicProblems.first(where: { $0.diagnostic.localizedSummary == "Topic reference 'MyKit/MyClas/myFunction()' couldn't be resolved. Reference at '/MyKit' can't resolve 'MyClas/myFunction()'. Did you mean MyClass?" })) + XCTAssertEqual(problem.diagnostic.notes.map(\.message), ["Available children: Discussion, globalFunction(_:considering:), MyClass, MyProtocol."]) + XCTAssertEqual(problem.possibleSolutions.count, 1) + XCTAssert(problem.possibleSolutions.map(\.replacements.count).allSatisfy { $0 == 1 }) + XCTAssertEqual(problem.possibleSolutions.map { [$0.summary, $0.replacements.first!.replacement] }, [ + ["Correct reference to MyKit/MyClass/myFunction().", "MyClass"], ]) + XCTAssertEqual(try problem.possibleSolutions.first!.apply(to: url.appendingPathComponent("documentation/myclass.md")), """ + # ``MyKit/MyClass`` + + @Metadata { + @DocumentationExtension(mergeBehavior: override) + } + + A cool API to call. + + This overview has an ``UnresolvableSymbolLinkInMyClassOverview<>(_:))``. + + - Parameters: + - name: A parameter + - Returns: Return value + + ## Topics + + ### Unresolvable curation + + - ``UnresolvableClassInMyClassTopicCuration`` + - ``MyClass/unresolvablePropertyInMyClassTopicCuration`` + - + + ### Near Miss + + - ``otherFunction()`` + - ``/MyKit/MyClas`` + - ``MyKit/MyClass/myFunction()`` + - + + ### Ambiguous curation + + - ``init()`` + - ``MyClass/init()-swift.init`` + - + """) + + + problem = try XCTUnwrap(unresolvedTopicProblems.first(where: { $0.diagnostic.localizedSummary == "Topic reference 'doc:MyKit/MyClas/myFunction()' couldn't be resolved. Reference at '/MyKit' can't resolve 'MyClas/myFunction()'. Did you mean MyClass?" })) + XCTAssertEqual(problem.diagnostic.notes.map(\.message), ["Available children: Discussion, globalFunction(_:considering:), MyClass, MyProtocol."]) + XCTAssertEqual(problem.possibleSolutions.count, 1) + XCTAssert(problem.possibleSolutions.map(\.replacements.count).allSatisfy { $0 == 1 }) + XCTAssertEqual(problem.possibleSolutions.map { [$0.summary, $0.replacements.first!.replacement] }, [ + ["Correct reference to MyKit/MyClass/myFunction().", "MyClass"], + ]) + XCTAssertEqual(try problem.possibleSolutions.first!.apply(to: url.appendingPathComponent("documentation/myclass.md")), """ + # ``MyKit/MyClass`` + + @Metadata { + @DocumentationExtension(mergeBehavior: override) + } + + A cool API to call. + + This overview has an ``UnresolvableSymbolLinkInMyClassOverview<>(_:))``. + + - Parameters: + - name: A parameter + - Returns: Return value + + ## Topics + + ### Unresolvable curation + + - ``UnresolvableClassInMyClassTopicCuration`` + - ``MyClass/unresolvablePropertyInMyClassTopicCuration`` + - + + ### Near Miss + + - ``otherFunction()`` + - ``/MyKit/MyClas`` + - ``MyKit/MyClas/myFunction()`` + - + + ### Ambiguous curation + + - ``init()`` + - ``MyClass/init()-swift.init`` + - + """) } else { XCTAssertTrue(unresolvedTopicProblems.contains(where: { $0.diagnostic.localizedSummary == "Topic reference 'UnresolvableSymbolLinkInMyClassOverview<>(_:))' couldn't be resolved. No local documentation matches this reference." })) XCTAssertTrue(unresolvedTopicProblems.contains(where: { $0.diagnostic.localizedSummary == "Topic reference 'UnresolvableClassInMyClassTopicCuration' couldn't be resolved. No local documentation matches this reference." })) @@ -820,9 +985,9 @@ class SymbolTests: XCTestCase { let unresolvedTopicProblems = context.problems.filter { $0.diagnostic.identifier == "org.swift.docc.unresolvedTopicReference" } if LinkResolutionMigrationConfiguration.shouldUseHierarchyBasedLinkResolver { - XCTAssertTrue(unresolvedTopicProblems.contains(where: { $0.diagnostic.localizedSummary == "Topic reference 'UnresolvableSymbolLinkInMyClassOverview' couldn't be resolved. Reference at '/MyKit/MyClass/myFunction()' can't resolve 'UnresolvableSymbolLinkInMyClassOverview'. Did you mean: Unresolvable-curation?" })) - XCTAssertTrue(unresolvedTopicProblems.contains(where: { $0.diagnostic.localizedSummary == "Topic reference 'UnresolvableClassInMyClassTopicCuration' couldn't be resolved. Reference at '/MyKit/MyClass/myFunction()' can't resolve 'UnresolvableClassInMyClassTopicCuration'. Did you mean: Unresolvable-curation?" })) - XCTAssertTrue(unresolvedTopicProblems.contains(where: { $0.diagnostic.localizedSummary == "Topic reference 'MyClass/unresolvablePropertyInMyClassTopicCuration' couldn't be resolved. Reference at '/MyKit/MyClass' can't resolve 'unresolvablePropertyInMyClassTopicCuration'. No similar pages. Available children: init(), myFunction()." })) + XCTAssertTrue(unresolvedTopicProblems.contains(where: { $0.diagnostic.localizedSummary == "Topic reference 'UnresolvableSymbolLinkInMyClassOverview' couldn't be resolved. Reference at '/MyKit/MyClass/myFunction()' can't resolve 'UnresolvableSymbolLinkInMyClassOverview'. Did you mean Unresolvable-curation?" })) + XCTAssertTrue(unresolvedTopicProblems.contains(where: { $0.diagnostic.localizedSummary == "Topic reference 'UnresolvableClassInMyClassTopicCuration' couldn't be resolved. Reference at '/MyKit/MyClass/myFunction()' can't resolve 'UnresolvableClassInMyClassTopicCuration'. Did you mean Unresolvable-curation?" })) + XCTAssertTrue(unresolvedTopicProblems.contains(where: { $0.diagnostic.localizedSummary == "Topic reference 'MyClass/unresolvablePropertyInMyClassTopicCuration' couldn't be resolved. Reference at '/MyKit/MyClass' can't resolve 'unresolvablePropertyInMyClassTopicCuration'. No similar pages." })) } else { XCTAssertTrue(unresolvedTopicProblems.contains(where: { $0.diagnostic.localizedSummary == "Topic reference 'UnresolvableSymbolLinkInMyClassOverview' couldn't be resolved. No local documentation matches this reference." })) XCTAssertTrue(unresolvedTopicProblems.contains(where: { $0.diagnostic.localizedSummary == "Topic reference 'UnresolvableClassInMyClassTopicCuration' couldn't be resolved. No local documentation matches this reference." })) diff --git a/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift b/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift index f561f1c7a9..a5f4ec75a3 100644 --- a/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift @@ -399,11 +399,11 @@ class OutOfProcessReferenceResolverTests: XCTestCase { func assertForwardsResolverErrors(resolver: OutOfProcessReferenceResolver) throws { XCTAssertEqual(resolver.bundleIdentifier, "com.test.bundle") let resolverResult = resolver.resolve(.unresolved(UnresolvedTopicReference(topicURL: ValidatedURL(parsingExact: "doc://com.test.bundle/something")!)), sourceLanguage: .swift) - guard case .failure(_, let errorMessage) = resolverResult else { + guard case .failure(_, let error) = resolverResult else { XCTFail("Encountered an unexpected type of error.") return } - XCTAssertEqual(errorMessage, "Some error message.") + XCTAssertEqual(error.localizedDescription, "Some error message.") } func testForwardsResolverErrorsProcess() throws {