diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift index aac21f4c81..1b671e4c9e 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift @@ -1383,12 +1383,16 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { referencesIndex.removeAll() referencesIndex.reserveCapacity(knownIdentifiers.count) for reference in knownIdentifiers { - referencesIndex[reference.absoluteString] = reference + registerReference(reference) } return (moduleReferences: Set(moduleReferences.values), urlHierarchy: symbolsURLHierarchy) } } + + private func registerReference(_ resolvedReference: ResolvedTopicReference) { + referencesIndex[resolvedReference.absoluteString] = resolvedReference + } private func shouldContinueRegistration() throws { guard isRegistrationEnabled.sync({ $0 }) else { @@ -2607,6 +2611,11 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { if case .success(let resolvedReference) = reference { cacheReference(resolvedReference, withKey: ResolvedTopicReference.cacheIdentifier(unresolvedReference, fromSymbolLink: isCurrentlyResolvingSymbolLink, in: parent)) + + // Register the resolved reference in the context so that it can be looked up via its absolute + // path. We only do this for in-bundle content, and since we've just resolved an in-bundle link, + // we register the reference. + registerReference(resolvedReference) return .success(resolvedReference) } } diff --git a/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift b/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift index 87808e3568..a4dcca1cab 100644 --- a/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift +++ b/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift @@ -1187,6 +1187,112 @@ class ConvertServiceTests: XCTestCase { } } + func testConvertTopLevelSymbolWithLinkResolving() throws { + let symbolGraphFile = Bundle.module.url( + forResource: "one-symbol-top-level", + withExtension: "symbols.json", + subdirectory: "Test Resources" + )! + + let symbolGraph = try Data(contentsOf: symbolGraphFile) + + let request = ConvertRequest( + bundleInfo: DocumentationBundle.Info( + displayName: "TestBundle", + identifier: "org.swift.example", + version: "1.0.0" + ), + externalIDsToConvert: ["s:32MyKit3FooV"], + documentPathsToConvert: [], + symbolGraphs: [symbolGraph], + markupFiles: [], + miscResourceURLs: [] + ) + + let server = DocumentationServer() + + let mockLinkResolvingService = LinkResolvingService { message in + do { + let payload = try XCTUnwrap(message.payload) + let request = try JSONDecoder() + .decode( + ConvertRequestContextWrapper.self, + from: payload + ) + + let errorResponse = DocumentationServer.Message( + type: "resolve-reference-response", + payload: try JSONEncoder().encode( + OutOfProcessReferenceResolver.Response + .errorMessage("Unable to resolve reference.") + ) + ) + + switch request.payload { + case .topic(let url): + let resolvableBarURL = URL( + string: "doc://org.swift.example/documentation/MyKit/Foo/bar()" + )! + + if url == resolvableBarURL { + let testSymbolInformationResponse = OutOfProcessReferenceResolver + .ResolvedInformation( + kind: .init( + name: "bar()", + id: "org.swift.docc.kind.method", + isSymbol: true + ), + url: resolvableBarURL, + title: "bar()", + abstract: "", + language: .init(name: "Swift", id: "swift"), + availableLanguages: [], + platforms: [], + declarationFragments: nil + ) + + let payloadData = OutOfProcessReferenceResolver.Response + .resolvedInformation(testSymbolInformationResponse) + + return DocumentationServer.Message( + type: "resolve-reference-response", + payload: try JSONEncoder().encode(payloadData) + ) + } else { + return errorResponse + } + default: + return errorResponse + } + } catch { + XCTFail(error.localizedDescription) + return nil + } + } + + server.register(service: mockLinkResolvingService) + + try processAndAssert(request: request, linkResolvingServer: server) { message in + let renderNodes = try JSONDecoder().decode( + ConvertResponse.self, + from: XCTUnwrap(message.payload) + ).renderNodes + + XCTAssertEqual(renderNodes.count, 1) + let data = try XCTUnwrap(renderNodes.first) + let renderNode = try JSONDecoder().decode(RenderNode.self, from: data) + + XCTAssertEqual( + Set(renderNode.references.keys), + [ + "doc://org.swift.example/documentation/MyKit", + "doc://org.swift.example/documentation/MyKit/Foo", + "doc://org.swift.example/documentation/MyKit/Foo/bar()", + ] + ) + } + } + func testOrderOfLinkResolutionRequestsForDocLink() throws { let symbolGraphFile = try XCTUnwrap( Bundle.module.url( diff --git a/Tests/SwiftDocCTests/Test Resources/one-symbol-top-level.symbols.json b/Tests/SwiftDocCTests/Test Resources/one-symbol-top-level.symbols.json new file mode 100644 index 0000000000..36bbe5df57 --- /dev/null +++ b/Tests/SwiftDocCTests/Test Resources/one-symbol-top-level.symbols.json @@ -0,0 +1,213 @@ +{ + "metadata": { + "formatVersion": { + "major": 0, + "minor": 5, + "patch": 3 + }, + "generator": "Apple Swift version 5.7 (swiftlang-5.7.0.101.10 clang-1400.0.10.4.2)" + }, + "module": { + "name": "MyKit", + "platform": { + "architecture": "x86_64", + "vendor": "apple", + "operatingSystem": { + "name": "macosx", + "minimumVersion": { + "major": 10, + "minor": 10, + "patch": 0 + } + } + } + }, + "symbols": [ + { + "kind": { + "identifier": "swift.struct", + "displayName": "Structure" + }, + "identifier": { + "precise": "s:32MyKit3FooV", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "Foo" + ], + "names": { + "title": "Foo" + }, + "docComment": { + "lines": [ + { + "range": { + "start": { + "line": 0, + "character": 4 + }, + "end": { + "line": 0, + "character": 15 + } + }, + "text": "Foo" + }, + { + "range": { + "start": { + "line": 1, + "character": 3 + }, + "end": { + "line": 1, + "character": 3 + } + }, + "text": "" + }, + { + "range": { + "start": { + "line": 2, + "character": 4 + }, + "end": { + "line": 2, + "character": 39 + } + }, + "text": "All of these links should resolve:" + }, + { + "range": { + "start": { + "line": 3, + "character": 3 + }, + "end": { + "line": 3, + "character": 3 + } + }, + "text": "" + }, + { + "range": { + "start": { + "line": 4, + "character": 4 + }, + "end": { + "line": 4, + "character": 13 + } + }, + "text": "``bar()``" + }, + { + "range": { + "start": { + "line": 5, + "character": 3 + }, + "end": { + "line": 5, + "character": 3 + } + }, + "text": "" + }, + { + "range": { + "start": { + "line": 6, + "character": 4 + }, + "end": { + "line": 6, + "character": 17 + } + }, + "text": "``Foo/bar()``" + }, + { + "range": { + "start": { + "line": 7, + "character": 3 + }, + "end": { + "line": 7, + "character": 3 + } + }, + "text": "" + }, + { + "range": { + "start": { + "line": 8, + "character": 4 + }, + "end": { + "line": 8, + "character": 22 + } + }, + "text": "``Foo/otherBar()``" + }, + { + "range": { + "start": { + "line": 9, + "character": 3 + }, + "end": { + "line": 9, + "character": 3 + } + }, + "text": "" + }, + { + "range": { + "start": { + "line": 10, + "character": 4 + }, + "end": { + "line": 10, + "character": 50 + } + }, + "text": "``MyKit/Foo/bar()``" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "struct" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "Foo" + } + ], + "accessLevel": "public", + "location": { + "uri": "file:///tmp/Downloads/MyKit/MyKit.swift", + "position": { + "line": 11, + "character": 14 + } + } + } + ], + "relationships": [] +}