diff --git a/Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift b/Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift index 5b07d7bea4..90515a95e5 100644 --- a/Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift +++ b/Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift @@ -149,6 +149,7 @@ public struct ConvertService: DocumentationService { // Enable support for generating documentation for standalone articles and tutorials. context.allowsRegisteringArticlesWithoutTechnologyRoot = true context.allowsRegisteringUncuratedTutorials = true + context.considerDocumentationExtensionsThatDoNotMatchSymbolsAsResolved = true context.configureSymbolGraph = { symbolGraph in for (symbolIdentifier, overridingDocumentationComment) in request.overridingDocumentationComments ?? [:] { @@ -252,9 +253,7 @@ public struct ConvertService: DocumentationService { baseReferenceStore: RenderReferenceStore? ) -> RenderReferenceStore { let uncuratedArticles = context.uncuratedArticles.map { ($0, isDocumentationExtensionContent: false) } - let uncuratedDocumentationExtensions = context.uncuratedDocumentationExtensions.flatMap { reference, articles in - articles.map { article in ((reference, article), isDocumentationExtensionContent: true) } - } + let uncuratedDocumentationExtensions = context.uncuratedDocumentationExtensions.map { ($0, isDocumentationExtensionContent: true) } let topicContent = (uncuratedArticles + uncuratedDocumentationExtensions) .compactMap { (value, isDocumentationExtensionContent) -> (ResolvedTopicReference, RenderReferenceStore.TopicContent)? in let (topicReference, article) = value diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift index 8f07f352df..a854b741c2 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift @@ -149,9 +149,18 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { /// Controls whether tutorials that aren't curated in a tutorials overview page are registered and translated. /// /// Set this property to `true` to enable registering documentation for standalone tutorials, - /// for example when ``ConvertService``. + /// for example when using ``ConvertService``. var allowsRegisteringUncuratedTutorials: Bool = false + /// Controls whether documentation extension files are considered resolved even when they don't match a symbol. + /// + /// Set this property to `true` to always consider documentation extensions as "resolved", for example when using ``ConvertService``. + /// + /// > Note: + /// > Setting this property tor `true` means taking over the responsibility to match documentation extension files to symbols + /// > diagnosing unmatched documentation extension files, and diagnostic symbols that match multiple documentation extension files. + var considerDocumentationExtensionsThatDoNotMatchSymbolsAsResolved: Bool = false + /// A closure that modifies each symbol graph that the context registers. /// /// Set this property if you need to modify symbol graphs before the context registers its information. @@ -291,9 +300,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { /// has been built, this list of uncurated documentation extensions will be empty. /// /// The key to lookup a documentation extension file is the symbol reference from its title (level 1 heading). - /// - /// - Warning: It's possible—but not supported—for multiple documentation extension files to specify the same symbol link. - var uncuratedDocumentationExtensions = [ResolvedTopicReference: [SemanticResult
]]() + var uncuratedDocumentationExtensions = [ResolvedTopicReference: SemanticResult
]() /// External metadata injected into the context, for example via command line arguments. public var externalMetadata = ExternalMetadata() @@ -823,7 +830,8 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { technologies: [SemanticResult], tutorials: [SemanticResult], tutorialArticles: [SemanticResult], - articles: [SemanticResult
] + articles: [SemanticResult
], + documentationExtensions: [SemanticResult
] ) { // First, try to understand the basic structure of the document by // analyzing it and putting references in as "unresolved". @@ -831,6 +839,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { var tutorials = [SemanticResult]() var tutorialArticles = [SemanticResult]() var articles = [SemanticResult
]() + var documentationExtensions = [SemanticResult
]() var references: [ResolvedTopicReference: URL] = [:] @@ -937,25 +946,21 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { // Separate articles that look like documentation extension files from other articles, so that the documentation extension files can be matched up with a symbol. // At this point we consider all articles with an H1 containing link "documentation extension" - some links might not resolve in the final documentation hierarchy // and we will emit warnings for those later on when we finalize the bundle discovery phase. - if let link = result.value.title?.child(at: 0) as? AnyLink, - let url = link.destination.flatMap(ValidatedURL.init(parsingExact:)) { - let reference = result.topicGraphNode.reference - - let symbolPath = NodeURLGenerator.Path.documentation(path: url.components.path).stringValue - let symbolReference = ResolvedTopicReference( - bundleIdentifier: reference.bundleIdentifier, - path: symbolPath, - fragment: nil, - sourceLanguages: reference.sourceLanguages - ) - - uncuratedDocumentationExtensions[symbolReference, default: []].append(result) + if result.value.title?.child(at: 0) is AnyLink { + documentationExtensions.append(result) // Warn for an incorrect root page metadata directive. - if result.value.metadata?.technologyRoot != nil { - let diagnostic = Diagnostic(source: url.url, severity: .warning, range: article.metadata?.technologyRoot?.originalMarkup.range, identifier: "org.swift.docc.UnexpectedTechnologyRoot", summary: "Don't use TechnologyRoot in documentation extension files because it's only valid as a directive in articles") - let problem = Problem(diagnostic: diagnostic, possibleSolutions: []) - diagnosticEngine.emit(problem) + if let technologyRoot = result.value.metadata?.technologyRoot { + let diagnostic = Diagnostic(source: url, severity: .warning, range: article.metadata?.technologyRoot?.originalMarkup.range, identifier: "org.swift.docc.UnexpectedTechnologyRoot", summary: "Documentation extension files can't become technology roots.") + let solutions: [Solution] + if let range = technologyRoot.originalMarkup.range { + solutions = [ + Solution(summary: "Remove the TechnologyRoot directive", replacements: [Replacement(range: range, replacement: "")]) + ] + } else { + solutions = [] + } + diagnosticEngine.emit(Problem(diagnostic: diagnostic, possibleSolutions: solutions)) } } else { precondition(uncuratedArticles[result.topicGraphNode.reference] == nil, "Article references are unique.") @@ -976,7 +981,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { } } - return (technologies, tutorials, tutorialArticles, articles) + return (technologies, tutorials, tutorialArticles, articles, documentationExtensions) } private func insertLandmarks(_ landmarks: Landmarks, from topicGraphNode: TopicGraph.Node, source url: URL) where Landmarks.Element == Landmark { @@ -1012,24 +1017,13 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { /// A lookup of resolved references based on the reference's absolute string. private(set) var referenceIndex = [String: ResolvedTopicReference]() - private func nodeWithInitializedContent(reference: ResolvedTopicReference, matches: [DocumentationContext.SemanticResult
]?) -> DocumentationNode { + private func nodeWithInitializedContent(reference: ResolvedTopicReference, match foundDocumentationExtension: DocumentationContext.SemanticResult
?) -> DocumentationNode { precondition(documentationCache.keys.contains(reference)) - // A symbol can have only one documentation extension markdown file, so emit warnings if there are more. - if let matches = matches, matches.count > 1 { - let zeroRange = SourceLocation(line: 1, column: 1, source: nil).. (moduleReferences: Set, urlHierarchy: BidirectionalTree) { + private func registerSymbols( + from bundle: DocumentationBundle, + symbolGraphLoader: SymbolGraphLoader, + documentationExtensions: [SemanticResult
] + ) throws -> (moduleReferences: Set, urlHierarchy: BidirectionalTree) + { // Making sure that we correctly let decoding memory get released, do not remove the autorelease pool. return try autoreleasepool { [documentationCacheBasedLinkResolver] () -> (Set, BidirectionalTree) in /// A tree of the symbol hierarchy as defined by the combined symbol graph. @@ -1313,6 +1312,119 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { if LinkResolutionMigrationConfiguration.shouldUseHierarchyBasedLinkResolver { hierarchyBasedLinkResolver!.addMappingForSymbols(symbolIndex: symbolIndex) } + // Track the symbols that have multiple matching documentation extension files for diagnostics. + var symbolsWithMultipleDocumentationExtensionMatches = [ResolvedTopicReference: [SemanticResult
]]() + for documentationExtension in documentationExtensions { + guard let link = documentationExtension.value.title?.child(at: 0) as? AnyLink else { + fatalError("An article shouldn't have ended up in the documentation extension list unless its title was a link. File: \(documentationExtension.source.absoluteString.singleQuoted)") + } + + guard let destination = link.destination else { + let diagnostic = Diagnostic(source: documentationExtension.source, severity: .warning, range: link.range, identifier: "org.swift.docc.emptyLinkDestination", summary: """ + Documentation extension with an empty link doesn't correspond to any symbol. + """, explanation: nil, notes: []) + diagnosticEngine.emit(Problem(diagnostic: diagnostic)) + continue + } + guard let url = ValidatedURL(parsingExact: destination) else { + let diagnostic = Diagnostic(source: documentationExtension.source, severity: .warning, range: link.range, identifier: "org.swift.docc.invalidLinkDestination", summary: """ + \(destination.singleQuoted) is + """, explanation: nil, notes: []) + diagnosticEngine.emit(Problem(diagnostic: diagnostic)) + continue + } + + if LinkResolutionMigrationConfiguration.shouldUseHierarchyBasedLinkResolver { + // If there's a single module then resolve relative to the module symbol. Otherwise resolve relative to the bundle root. + // This means that links can omit the module name if there's only one module but need to start with the module name if there are multiple modules. + let rootReference: ResolvedTopicReference + let moduleReferences = hierarchyBasedLinkResolver!.modules() + if moduleReferences.count == 1 { + rootReference = moduleReferences.first! + } else { + rootReference = bundle.rootReference + } + + let reference = TopicReference.unresolved(.init(topicURL: url)) + switch resolve(reference, in: rootReference, fromSymbolLink: true) { + case .success(let resolved): + if let existing = uncuratedDocumentationExtensions[resolved] { + if symbolsWithMultipleDocumentationExtensionMatches[resolved] == nil { + symbolsWithMultipleDocumentationExtensionMatches[resolved] = [existing] + } + symbolsWithMultipleDocumentationExtensionMatches[resolved]!.append(documentationExtension) + } else { + uncuratedDocumentationExtensions[resolved] = documentationExtension + } + case .failure(_, let errorInfo): + guard !considerDocumentationExtensionsThatDoNotMatchSymbolsAsResolved else { + // The ConvertService relies on old implementation detail where documentation extension files were always considered "resolved" even when they didn't match a symbol. + // + // Don't rely on this behavior for new functionality. The behavior will be removed once we have a new solution to meets the needs of the ConvertService. (rdar://108563483) + // https://github.com/apple/swift-docc/issues/567 + // + // The process that interacts with the convert service is responsible for: + // - Distinguishing between documentation extension files that match symbols and documentation extension files that don't match symbols. + // - Resolving symbol link in a way that match the behavior of regular documentation builds. + // the process that interacts with the convert service is responsible for maintaining it's own link resolutions implementation to match the behavior of a regular build. + // - Diagnosing documentation extension files that don't match any symbols. + let reference = documentationExtension.topicGraphNode.reference + + let symbolPath = NodeURLGenerator.Path.documentation(path: url.components.path).stringValue + let symbolReference = ResolvedTopicReference( + bundleIdentifier: reference.bundleIdentifier, + path: symbolPath, + fragment: nil, + sourceLanguages: reference.sourceLanguages + ) + + if let existing = uncuratedDocumentationExtensions[symbolReference] { + if symbolsWithMultipleDocumentationExtensionMatches[symbolReference] == nil { + symbolsWithMultipleDocumentationExtensionMatches[symbolReference] = [existing] + } + symbolsWithMultipleDocumentationExtensionMatches[symbolReference]!.append(documentationExtension) + } else { + uncuratedDocumentationExtensions[symbolReference] = documentationExtension + } + continue + } + + // Present a diagnostic specific to documentation extension files but get the solutions and notes from the general unresolved link problem. + let unresolvedLinkProblem = + unresolvedReferenceProblem(reference: reference, source: documentationExtension.source, range: link.range, severity: .warning, uncuratedArticleMatch: nil, errorInfo: errorInfo, fromSymbolLink: link is SymbolLink) + + diagnosticEngine.emit( + Problem( + diagnostic: Diagnostic(source: documentationExtension.source, severity: .warning, range: link.range, identifier: "org.swift.docc.SymbolUnmatched", summary: "No symbol matched \(destination.singleQuoted). \(errorInfo.message).", notes: unresolvedLinkProblem.diagnostic.notes), + possibleSolutions: unresolvedLinkProblem.possibleSolutions + ) + ) + } + } else { + // The documentation cache based link resolver doesn't "resolve" the links in the documentation extension titles. + // Instead it matches them by exact match or not at all. + let reference = documentationExtension.topicGraphNode.reference + + let symbolPath = NodeURLGenerator.Path.documentation(path: url.components.path).stringValue + let symbolReference = ResolvedTopicReference( + bundleIdentifier: reference.bundleIdentifier, + path: symbolPath, + fragment: nil, + sourceLanguages: reference.sourceLanguages + ) + + if let existing = uncuratedDocumentationExtensions[symbolReference] { + if symbolsWithMultipleDocumentationExtensionMatches[symbolReference] == nil { + symbolsWithMultipleDocumentationExtensionMatches[symbolReference] = [existing] + } + symbolsWithMultipleDocumentationExtensionMatches[symbolReference]!.append(documentationExtension) + } else { + uncuratedDocumentationExtensions[symbolReference] = documentationExtension + } + } + } + emitWarningsForSymbolsMatchedInMultipleDocumentationExtensions(with: symbolsWithMultipleDocumentationExtensionMatches) + symbolsWithMultipleDocumentationExtensionMatches.removeAll() if hierarchyBasedLinkResolver == nil || LinkResolutionMigrationConfiguration.shouldReportLinkResolutionMismatches { // The `symbolsURLHierarchy` is only used in the cache-based link resolver to traverse the documentation and @@ -1436,12 +1548,12 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { let updatedNodes: [(node: DocumentationNode, matchedArticleURL: URL?)] = Array(symbolIndex.values) .concurrentPerform { finalReference, results in // Match the symbol's documentation extension and initialize the node content. - let matches = uncuratedDocumentationExtensions[finalReference] - let updatedNode = nodeWithInitializedContent(reference: finalReference, matches: matches) + let match = uncuratedDocumentationExtensions[finalReference] + let updatedNode = nodeWithInitializedContent(reference: finalReference, match: match) results.append(( node: updatedNode, - matchedArticleURL: matches?.first?.source + matchedArticleURL: match?.source )) } @@ -1496,6 +1608,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { } } + /// Builds in-memory relationships between symbols based on the relationship information in a given symbol graph file. /// /// - Parameters: @@ -2157,7 +2270,8 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { technologies: [SemanticResult], tutorials: [SemanticResult], tutorialArticles: [SemanticResult], - articles: [SemanticResult
] + articles: [SemanticResult
], + documentationExtensions: [SemanticResult
] )! discoveryGroup.async(queue: discoveryQueue) { [unowned self] in @@ -2181,10 +2295,10 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { } // All discovery went well, process the inputs. - let (technologies, tutorials, tutorialArticles, allArticles) = result + let (technologies, tutorials, tutorialArticles, allArticles, documentationExtensions) = result var (otherArticles, rootPageArticles) = splitArticles(allArticles) - let globalOptions = (allArticles + uncuratedDocumentationExtensions.values.flatMap { $0 }).compactMap { article in + let globalOptions = (allArticles + documentationExtensions).compactMap { article in return article.value.options[.global] } @@ -2236,7 +2350,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { } let rootPages = registerRootPages(from: rootPageArticles, in: bundle) - let (moduleReferences, symbolsURLHierarchy) = try registerSymbols(from: bundle, symbolGraphLoader: symbolGraphLoader) + let (moduleReferences, symbolsURLHierarchy) = try registerSymbols(from: bundle, symbolGraphLoader: symbolGraphLoader, documentationExtensions: documentationExtensions) // We don't need to keep the loader in memory after we've registered all symbols. symbolGraphLoader = nil @@ -2528,25 +2642,40 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { return crawler.curatedNodes } - /// Emits warnings for unmatched documentation extensions and uncurated articles. - private func emitWarningsForUncuratedTopics() { - // Check that all documentation extension files matched a symbol and that all articles are curated - for results in uncuratedDocumentationExtensions.values { - let articleResult = results.first! - let remaining = results.dropFirst() + /// Emits warnings for symbols that are matched by multiple documentation extensions. + private func emitWarningsForSymbolsMatchedInMultipleDocumentationExtensions(with symbolsWithMultipleDocumentationExtensionMatches: [ResolvedTopicReference : [DocumentationContext.SemanticResult
]]) { + for (reference, documentationExtensions) in symbolsWithMultipleDocumentationExtensionMatches { + let symbolPath = reference.url.pathComponents.dropFirst(2).joined(separator: "/") + let firstExtension = documentationExtensions.first! - guard let link = articleResult.value.title?.child(at: 0) as? AnyLink else { - fatalError("An article shouldn't have ended up in the documentation extension cache unless its title was a link. File: \(articleResult.source.absoluteString.singleQuoted)") + guard let link = firstExtension.value.title?.child(at: 0) as? AnyLink else { + fatalError("An article shouldn't have ended up in the documentation extension list unless its title was a link. File: \(firstExtension.source.absoluteString.singleQuoted)") + } + let zeroRange = SourceLocation(line: 1, column: 1, source: nil).. [ResolvedTopicReference] { + return pathHierarchy.modules.values.map { resolvedReferenceMap[$0.identifier]! } + } + // MARK: - Adding non-symbols /// Map the resolved identifiers to resolved topic references for a given bundle's article, tutorial, and technology root pages. diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/GeneratedDocumentationTopics.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/GeneratedDocumentationTopics.swift index 7860fd98c6..bc72d33a51 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/GeneratedDocumentationTopics.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/GeneratedDocumentationTopics.swift @@ -160,7 +160,7 @@ enum GeneratedDocumentationTopics { var collectionArticle: Article // Find matching doc extension or create an empty article. - if let docExtensionMatch = context.uncuratedDocumentationExtensions[collectionReference]?.first?.value { + if let docExtensionMatch = context.uncuratedDocumentationExtensions[collectionReference]?.value { collectionArticle = docExtensionMatch collectionArticle.title = Heading(level: 1, Text(title)) context.uncuratedDocumentationExtensions.removeValue(forKey: collectionReference) diff --git a/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift b/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift index ba9cc94713..5d198552fb 100644 --- a/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift +++ b/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift @@ -1221,6 +1221,9 @@ class ConvertServiceTests: XCTestCase { assert: { renderNodes, referenceStore in let referenceStore = try XCTUnwrap(referenceStore) + // The ConvertService relies on old implementation detail where documentation extension files were always considered "resolved" even when they didn't match a symbol. (rdar://108563483) + // https://github.com/apple/swift-docc/issues/567 + XCTAssertEqual( Set(referenceStore.topics.keys.map(\.path)), [ @@ -1245,7 +1248,6 @@ class ConvertServiceTests: XCTestCase { "/documentation/Test-Bundle/Default-Code-Listing-Syntax", ] ) - try self.assertReferenceStoreContains( referenceStore: referenceStore, topicPath: "/documentation/MyKit/MyClass", diff --git a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+RootPageTests.swift b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+RootPageTests.swift index 250270b7fd..4ed47f15c4 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+RootPageTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+RootPageTests.swift @@ -88,10 +88,12 @@ class DocumentationContext_RootPageTests: XCTestCase { try workspace.registerProvider(dataProvider) // Verify that we emit a warning when trying to make a symbol a root page - XCTAssertTrue(context.problems.contains(where: { problem -> Bool in - return problem.diagnostic.identifier == "org.swift.docc.UnexpectedTechnologyRoot" - && problem.diagnostic.source?.path == "ReleaseNotes/MyClass" - })) + let technologyRootProblem = try XCTUnwrap(context.problems.first(where: { $0.diagnostic.identifier == "org.swift.docc.UnexpectedTechnologyRoot" })) + XCTAssertEqual(technologyRootProblem.diagnostic.source, tempFolderURL.appendingPathComponent("no-sgf-test.docc").appendingPathComponent("MyClass.md")) + XCTAssertEqual(technologyRootProblem.diagnostic.range?.lowerBound.line, 3) + let solution = try XCTUnwrap(technologyRootProblem.possibleSolutions.first) + XCTAssertEqual(solution.replacements.first?.range.lowerBound.line, 3) + XCTAssertEqual(solution.replacements.first?.range.upperBound.line, 3) } } diff --git a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift index f8069be6e8..d1ce125a24 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift @@ -1900,32 +1900,29 @@ let expected = """ // Add a sidecar file for a symbol that doesn't exist let (_, _, context) = try testBundleAndContext(copying: "TestBundle") { root in unknownSymbolSidecarURL = root.appendingPathComponent("documentation/unknownSymbol.md") - otherUnknownSymbolSidecarURL = root.appendingPathComponent("documentation/xanotherSidecarFileForThisUnknownSymbol.md") + otherUnknownSymbolSidecarURL = root.appendingPathComponent("documentation/anotherSidecarFileForThisUnknownSymbol.md") try content.write(to: unknownSymbolSidecarURL, atomically: true, encoding: .utf8) try content.write(to: otherUnknownSymbolSidecarURL, atomically: true, encoding: .utf8) } - let unmatchedSidecarProblem = context.problems.first(where: { $0.diagnostic.identifier == "org.swift.docc.SymbolUnmatched" }) + let unmatchedSidecarProblem = try XCTUnwrap(context.problems.first(where: { $0.diagnostic.identifier == "org.swift.docc.SymbolUnmatched" })) // Verify the diagnostics have the sidecar source URL - XCTAssertNotNil(unmatchedSidecarProblem?.diagnostic.source) - var sidecarFilesForUnknownSymbol: Set = [unknownSymbolSidecarURL.standardizedFileURL, otherUnknownSymbolSidecarURL.standardizedFileURL] + XCTAssertNotNil(unmatchedSidecarProblem.diagnostic.source) + let sidecarFilesForUnknownSymbol: Set = [unknownSymbolSidecarURL.standardizedFileURL, otherUnknownSymbolSidecarURL.standardizedFileURL] XCTAssertNotNil(unmatchedSidecarProblem) - if let unmatchedSidecarDiagnostic = unmatchedSidecarProblem?.diagnostic { - XCTAssertTrue(sidecarFilesForUnknownSymbol.contains(unmatchedSidecarDiagnostic.source?.standardizedFileURL), "One of the files should be the diagnostic source") - XCTAssertEqual(unmatchedSidecarDiagnostic.range, SourceLocation(line: 1, column: 3, source: unmatchedSidecarProblem?.diagnostic.source)..