diff --git a/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift b/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift index fa3666edd..51203e1a4 100644 --- a/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift @@ -194,7 +194,12 @@ public class OutOfProcessReferenceResolver: ExternalDocumentationSource, GlobalE imageReferences: (resolvedInformation.references ?? []).compactMap { $0 as? ImageReference } ) - return LinkResolver.ExternalEntity(topicRenderReference: renderReference, renderReferenceDependencies: dependencies, sourceLanguages: resolvedInformation.availableLanguages) + return LinkResolver.ExternalEntity( + topicRenderReference: renderReference, + renderReferenceDependencies: dependencies, + sourceLanguages: resolvedInformation.availableLanguages, + documentationKind: resolvedInformation.kind + ) } // MARK: Implementation diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift index 0d145a597..0b0c4391d 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift @@ -109,7 +109,8 @@ final class ExternalPathHierarchyResolver { return .init( topicRenderReference: resolvedInformation.topicRenderReference(), renderReferenceDependencies: dependencies, - sourceLanguages: resolvedInformation.availableLanguages + sourceLanguages: resolvedInformation.availableLanguages, + documentationKind: resolvedInformation.kind ) } diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver+NavigatorIndex.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver+NavigatorIndex.swift index c23a850f7..5b6d140a3 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver+NavigatorIndex.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver+NavigatorIndex.swift @@ -39,9 +39,10 @@ package struct ExternalRenderNode { } /// The symbol kind of this documentation node. + /// + /// This value is `nil` if the referenced page is not a symbol. var symbolKind: SymbolGraph.Symbol.KindIdentifier? { - // Symbol kind information is not available for external entities - return nil + DocumentationNode.symbolKind(for: externalEntity.documentationKind) } /// The additional "role" assigned to the symbol, if any @@ -116,7 +117,7 @@ struct NavigatorExternalRenderNode: NavigatorIndexableRenderNodeRepresentation { navigatorTitle: renderNode.navigatorTitleVariants.value(for: traits), externalID: renderNode.externalIdentifier.identifier, role: renderNode.role, - symbolKind: renderNode.symbolKind?.identifier, + symbolKind: renderNode.symbolKind?.renderingIdentifier, images: renderNode.images ) } @@ -145,4 +146,4 @@ struct ExternalRenderNodeMetadataRepresentation: NavigatorIndexableRenderMetadat var fragments: [DeclarationRenderSection.Token]? = nil var roleHeading: String? = nil var platforms: [AvailabilityRenderItem]? = nil -} \ No newline at end of file +} diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver.swift index 0ec34d9b4..10b26c4d4 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver.swift @@ -48,11 +48,13 @@ public class LinkResolver { /// - topicRenderReference: The render reference for this external topic. /// - renderReferenceDependencies: Any dependencies for the render reference. /// - sourceLanguages: The different source languages for which this page is available. + /// - documentationKind: The kind of external content that's being referenced. @_spi(ExternalLinks) - public init(topicRenderReference: TopicRenderReference, renderReferenceDependencies: RenderReferenceDependencies, sourceLanguages: Set) { + public init(topicRenderReference: TopicRenderReference, renderReferenceDependencies: RenderReferenceDependencies, sourceLanguages: Set, documentationKind: DocumentationNode.Kind) { self.topicRenderReference = topicRenderReference self.renderReferenceDependencies = renderReferenceDependencies self.sourceLanguages = sourceLanguages + self.documentationKind = documentationKind } /// The render reference for this external topic. @@ -63,7 +65,11 @@ public class LinkResolver { var renderReferenceDependencies: RenderReferenceDependencies /// The different source languages for which this page is available. var sourceLanguages: Set - + /// The kind of external content that's being referenced. + /// + /// For example, the navigator requires specific knowledge about what type of external symbol is being linked to. + var documentationKind: DocumentationNode.Kind + /// Creates a pre-render new topic content value to be added to a render context's reference store. func topicContent() -> RenderReferenceStore.TopicContent { return .init( diff --git a/Sources/SwiftDocC/Model/DocumentationNode.swift b/Sources/SwiftDocC/Model/DocumentationNode.swift index 3aca3e3e2..803ddb776 100644 --- a/Sources/SwiftDocC/Model/DocumentationNode.swift +++ b/Sources/SwiftDocC/Model/DocumentationNode.swift @@ -674,6 +674,7 @@ public struct DocumentationNode { case .union: return .union case .`var`: return .globalVariable case .module: return .module + case .extension: return .extension case .extendedModule: return .extendedModule case .extendedStructure: return .extendedStructure case .extendedClass: return .extendedClass @@ -684,6 +685,55 @@ public struct DocumentationNode { } } + /// Returns a symbol kind for the given documentation node. + /// - Parameter symbol: A documentation node kind. + /// - Returns: A symbol graph symbol. + static func symbolKind(for kind: Kind) -> SymbolGraph.Symbol.KindIdentifier? { + switch kind { + case .associatedType: return .`associatedtype` + case .class: return .`class` + case .deinitializer: return .`deinit` + case .dictionary, .object: return .dictionary + case .dictionaryKey: return .dictionaryKey + case .enumeration: return .`enum` + case .enumerationCase: return .`case` + case .function: return .`func` + case .httpRequest: return .httpRequest + case .httpParameter: return .httpParameter + case .httpBody: return .httpBody + case .httpResponse: return .httpResponse + case .operator: return .`operator` + case .initializer: return .`init` + case .instanceVariable: return .ivar + case .macro: return .macro + case .instanceMethod: return .`method` + case .namespace: return .namespace + case .instanceProperty: return .`property` + case .protocol: return .`protocol` + case .snippet: return .snippet + case .structure: return .`struct` + case .instanceSubscript: return .`subscript` + case .typeMethod: return .`typeMethod` + case .typeProperty, .typeConstant: return .`typeProperty` + case .typeSubscript: return .`typeSubscript` + case .typeAlias, .typeDef: return .`typealias` + case .union: return .union + case .globalVariable, .localVariable: return .`var` + case .module: return .module + case .extension: return .extension + case .extendedModule: return .extendedModule + case .extendedStructure: return .extendedStructure + case .extendedClass: return .extendedClass + case .extendedEnumeration: return .extendedEnumeration + case .extendedProtocol: return .extendedProtocol + case .unknownExtendedType: return .unknownExtendedType + default: + // For non-symbol kinds (like .article, .tutorial, etc.), + // return nil since these don't have corresponding SymbolGraph.Symbol.KindIdentifier values + return nil + } + } + /// Initializes a documentation node to represent a symbol from a symbol graph. /// /// - Parameters: diff --git a/Tests/SwiftDocCTests/Benchmark/ExternalTopicsHashTests.swift b/Tests/SwiftDocCTests/Benchmark/ExternalTopicsHashTests.swift index 20c36ce3f..d548f725d 100644 --- a/Tests/SwiftDocCTests/Benchmark/ExternalTopicsHashTests.swift +++ b/Tests/SwiftDocCTests/Benchmark/ExternalTopicsHashTests.swift @@ -31,7 +31,8 @@ class ExternalTopicsGraphHashTests: XCTestCase { estimatedTime: nil ), renderReferenceDependencies: .init(), - sourceLanguages: [.swift] + sourceLanguages: [.swift], + documentationKind: .class ) return (reference, entity) } diff --git a/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift b/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift index 43243e342..a71d0924a 100644 --- a/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift +++ b/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift @@ -92,7 +92,7 @@ class ExternalRenderNodeTests: XCTestCase { XCTAssertEqual(externalRenderNodes[1].identifier.absoluteString, "doc://org.swift.MixedLanguageFramework/example/path/to/external/objCSymbol") XCTAssertEqual(externalRenderNodes[1].kind, .symbol) - XCTAssertEqual(externalRenderNodes[1].symbolKind, nil) + XCTAssertEqual(externalRenderNodes[1].symbolKind, .func) XCTAssertEqual(externalRenderNodes[1].role, "symbol") XCTAssertEqual(externalRenderNodes[1].externalIdentifier.identifier, "doc://com.test.external/path/to/external/objCSymbol") @@ -104,7 +104,7 @@ class ExternalRenderNodeTests: XCTestCase { XCTAssertEqual(externalRenderNodes[3].identifier.absoluteString, "doc://org.swift.MixedLanguageFramework/example/path/to/external/swiftSymbol") XCTAssertEqual(externalRenderNodes[3].kind, .symbol) - XCTAssertEqual(externalRenderNodes[3].symbolKind, nil) + XCTAssertEqual(externalRenderNodes[3].symbolKind, .class) XCTAssertEqual(externalRenderNodes[3].role, "symbol") XCTAssertEqual(externalRenderNodes[3].externalIdentifier.identifier, "doc://com.test.external/path/to/external/swiftSymbol") } @@ -135,7 +135,8 @@ class ExternalRenderNodeTests: XCTestCase { navigatorTitleVariants: .init(defaultValue: navigatorTitle, objectiveCValue: occNavigatorTitle) ), renderReferenceDependencies: .init(), - sourceLanguages: [SourceLanguage(name: "swift"), SourceLanguage(name: "objc")]) + sourceLanguages: [SourceLanguage(name: "swift"), SourceLanguage(name: "objc")], + documentationKind: .function) let externalRenderNode = ExternalRenderNode( externalEntity: externalEntity, bundleIdentifier: "com.test.external" @@ -208,6 +209,8 @@ class ExternalRenderNodeTests: XCTestCase { XCTAssertEqual(occExternalNodes.map(\.title), ["ObjCArticle", "ObjCSymbol"]) XCTAssert(swiftExternalNodes.allSatisfy(\.isExternal)) XCTAssert(occExternalNodes.allSatisfy(\.isExternal)) + XCTAssertEqual(swiftExternalNodes.map(\.type), ["article", "class"]) + XCTAssertEqual(occExternalNodes.map(\.type), ["article", "func"]) } func testNavigatorWithExternalNodesOnlyAddsCuratedNodesToNavigator() throws { @@ -268,5 +271,7 @@ class ExternalRenderNodeTests: XCTestCase { XCTAssertEqual(occExternalNodes.map(\.title), ["ObjCSymbol"]) XCTAssert(swiftExternalNodes.allSatisfy(\.isExternal)) XCTAssert(occExternalNodes.allSatisfy(\.isExternal)) + XCTAssertEqual(swiftExternalNodes.map(\.type), ["article"]) + XCTAssertEqual(occExternalNodes.map(\.type), ["func"]) } } diff --git a/Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift b/Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift index e5ed51ddb..8ac864541 100644 --- a/Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift @@ -52,7 +52,8 @@ class ExternalReferenceResolverTests: XCTestCase { } ), renderReferenceDependencies: RenderReferenceDependencies(), - sourceLanguages: [resolvedEntityLanguage] + sourceLanguages: [resolvedEntityLanguage], + documentationKind: resolvedEntityKind ) } } @@ -707,7 +708,8 @@ class ExternalReferenceResolverTests: XCTestCase { estimatedTime: nil ), renderReferenceDependencies: RenderReferenceDependencies(), - sourceLanguages: [.swift] + sourceLanguages: [.swift], + documentationKind: .instanceProperty ) } } diff --git a/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift b/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift index 129d86633..45d0f011f 100644 --- a/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift +++ b/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift @@ -108,7 +108,8 @@ class TestMultiResultExternalReferenceResolver: ExternalDocumentationSource { images: entityInfo.topicImages?.map(\.0) ?? [] ), renderReferenceDependencies: dependencies, - sourceLanguages: [entityInfo.language] + sourceLanguages: [entityInfo.language], + documentationKind: entityInfo.kind ) } } diff --git a/Tests/SwiftDocCTests/Model/DocumentationNodeTests.swift b/Tests/SwiftDocCTests/Model/DocumentationNodeTests.swift index 9b371d23e..5f17246fd 100644 --- a/Tests/SwiftDocCTests/Model/DocumentationNodeTests.swift +++ b/Tests/SwiftDocCTests/Model/DocumentationNodeTests.swift @@ -11,6 +11,7 @@ import Foundation import Markdown @testable import SwiftDocC +import SymbolKit import XCTest class DocumentationNodeTests: XCTestCase { @@ -41,4 +42,38 @@ class DocumentationNodeTests: XCTestCase { XCTAssertEqual(anchorSection.reference, node.reference.withFragment(expectedTitle)) } } + + func testDocumentationKindToSymbolKindMapping() throws { + // Testing all symbol kinds map to a documentation kind + for symbolKind in SymbolGraph.Symbol.KindIdentifier.allCases { + let documentationKind = DocumentationNode.kind(forKind: symbolKind) + guard documentationKind != .unknown else { + continue + } + + let roundtrippedSymbolKind = DocumentationNode.symbolKind(for: documentationKind) + XCTAssertEqual(symbolKind, roundtrippedSymbolKind) + } + + // Testing that documentation kinds correctly map to a symbol kind + // Sometimes there are multiple mappings from DocumentationKind -> SymbolKind, exclude those here and test them separately + let documentationKinds = DocumentationNode.Kind.allKnownValues + .filter({ ![.localVariable, .typeDef, .typeConstant, .`keyword`, .tag, .object].contains($0) }) + for documentationKind in documentationKinds { + let symbolKind = DocumentationNode.symbolKind(for: documentationKind) + if documentationKind.isSymbol { + let symbolKind = try XCTUnwrap(DocumentationNode.symbolKind(for: documentationKind), "Expected a symbol kind equivalent for \(documentationKind)") + let rountrippedDocumentationKind = DocumentationNode.kind(forKind: symbolKind) + XCTAssertEqual(documentationKind, rountrippedDocumentationKind) + } else { + XCTAssertNil(symbolKind) + } + } + + // Test the exception documentation kinds + XCTAssertEqual(DocumentationNode.symbolKind(for: .localVariable), .var) + XCTAssertEqual(DocumentationNode.symbolKind(for: .typeDef), .typealias) + XCTAssertEqual(DocumentationNode.symbolKind(for: .typeConstant), .typeProperty) + XCTAssertEqual(DocumentationNode.symbolKind(for: .object), .dictionary) + } } diff --git a/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift b/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift index cc1d497b5..4ccaf7809 100644 --- a/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift +++ b/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift @@ -1188,7 +1188,8 @@ class SemaToRenderNodeTests: XCTestCase { estimatedTime: nil ), renderReferenceDependencies: .init(), - sourceLanguages: [.objectiveC] + sourceLanguages: [.objectiveC], + documentationKind: .class ) return (reference, entity) } @@ -1218,7 +1219,8 @@ class SemaToRenderNodeTests: XCTestCase { estimatedTime: nil ), renderReferenceDependencies: .init(), - sourceLanguages: [.swift] + sourceLanguages: [.swift], + documentationKind: .collection ) } } diff --git a/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift b/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift index d96a0216f..65bf572de 100644 --- a/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift @@ -133,6 +133,8 @@ class OutOfProcessReferenceResolverTests: XCTestCase { } else { XCTFail("Unexpected fragments variant patch") } + + XCTAssertEqual(entity.documentationKind, .init(name: "Kind Name", id: "com.test.kind.id", isSymbol: true)) } func testResolvingTopicLinkProcess() throws {