Skip to content

Commit 3268e73

Browse files
authored
Populate symbol kind in external render nodes (#1251)
* Add mapping from documentation kind to symbol kind There is a mapping from `SymbolGraph.Symbol.KindIdentifier` to `DocumentationNode.Kind`, but not the other way around; this commit adds one. For documentation kinds which both map to the same symbol kind, this has been expressed as: - both documentation kinds are converted to the same symbol kind in `symbolKind(for:)`. - a specific documentation kind is chosen as the default mapping in `kind(forKind:)` For example, for `DocumentationNode.Kind. typeConstant` [1]: - both `symbolKind(for: .typeConstant)` and `symbolKind(for: .typeProperty)` return `.typeProperty` - `kind(forKind: .typeProperty)` returns `.typeProperty` This function will be used to map and external entity's documentation kind to a symbol kind, so that that information can be populated as part of the navigator. ## Verification: This has been verified by testing the round trip conversion of all symbol kinds, and all documentation kinds which are symbols. [1]: https://github.com/swiftlang/swift-docc/blob/65aaf926ec079ddbd40f29540d4180a70af99e5e/Sources/SwiftDocC/Model/Kind.swift#L151 * Capture symbol kind as part of external entity Adds a new property to `LinkResolver.ExternalEntity` which stores the symbol kind of the external entity. This is derived at the level of `OutOfProcessReferenceResolver.ResolvedInformation`, which has access to the documentation kind [1] and already uses it to derive the render node kind and role, but then discards it. This commit adds logic to `OutOfProcessReferenceResolver.ResolvedInformation` to derive the symbol kind from the documentation kind, then capture that in `LinkResolver.ExternalEntity. We need access to the documentation kind in order to resolve the symbol kind as part of computing the navigator title. [2] [1]: https://github.com/swiftlang/swift-docc/blob/65aaf926ec079ddbd40f29540d4180a70af99e5e/Sources/SwiftDocC/Infrastructure/External%20Data/OutOfProcessReferenceResolver.swift#L564-L565 [2]: https://github.com/swiftlang/swift-docc/blob/65aaf926ec079ddbd40f29540d4180a70af99e5e/Sources/SwiftDocC/Indexing/Navigator/RenderNode%2BNavigatorIndex.swift#L163 * Propagate symbol kind to navigator for external nodes Propagates the symbol kind value from `LinkResolver.ExternalEntity` to the `ExternalRenderNode` used for generating the navigation index for external render nodes. This completes adding symbol kind support for external entities in the navigator. Also updates how the symbol kind is propagated to the navigator metadata, by using `.renderingIdentifier` over `.identifier`, to match the behaviour of local nodes [1]. Fixes rdar://156487928. [1]: https://github.com/swiftlang/swift-docc/blob/65aaf926ec079ddbd40f29540d4180a70af99e5e/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift#L1300
1 parent 1c54e10 commit 3268e73

12 files changed

+138
-20
lines changed

Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,12 @@ public class OutOfProcessReferenceResolver: ExternalDocumentationSource, GlobalE
182182
imageReferences: (resolvedInformation.references ?? []).compactMap { $0 as? ImageReference }
183183
)
184184

185-
return LinkResolver.ExternalEntity(topicRenderReference: renderReference, renderReferenceDependencies: dependencies, sourceLanguages: resolvedInformation.availableLanguages)
185+
return LinkResolver.ExternalEntity(
186+
topicRenderReference: renderReference,
187+
renderReferenceDependencies: dependencies,
188+
sourceLanguages: resolvedInformation.availableLanguages,
189+
symbolKind: DocumentationNode.symbolKind(for: resolvedInformation.kind)
190+
)
186191
}
187192

188193
// MARK: Implementation

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2023-2024 Apple Inc. and the Swift project authors
4+
Copyright (c) 2023-2025 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -109,7 +109,8 @@ final class ExternalPathHierarchyResolver {
109109
return .init(
110110
topicRenderReference: resolvedInformation.topicRenderReference(),
111111
renderReferenceDependencies: dependencies,
112-
sourceLanguages: resolvedInformation.availableLanguages
112+
sourceLanguages: resolvedInformation.availableLanguages,
113+
symbolKind: DocumentationNode.symbolKind(for: resolvedInformation.kind)
113114
)
114115
}
115116

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ package struct ExternalRenderNode {
3939
}
4040

4141
/// The symbol kind of this documentation node.
42+
///
43+
/// This value is `nil` if the referenced page is not a symbol.
4244
var symbolKind: SymbolGraph.Symbol.KindIdentifier? {
43-
// Symbol kind information is not available for external entities
44-
return nil
45+
externalEntity.symbolKind
4546
}
4647

4748
/// The additional "role" assigned to the symbol, if any
@@ -116,7 +117,7 @@ struct NavigatorExternalRenderNode: NavigatorIndexableRenderNodeRepresentation {
116117
navigatorTitle: renderNode.navigatorTitleVariants.value(for: traits),
117118
externalID: renderNode.externalIdentifier.identifier,
118119
role: renderNode.role,
119-
symbolKind: renderNode.symbolKind?.identifier,
120+
symbolKind: renderNode.symbolKind?.renderingIdentifier,
120121
images: renderNode.images,
121122
isBeta: renderNode.isBeta
122123
)

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*/
1010

1111
import Foundation
12-
import SymbolKit
12+
public import SymbolKit
1313

1414
/// A class that resolves documentation links by orchestrating calls to other link resolver implementations.
1515
public class LinkResolver {
@@ -48,11 +48,18 @@ public class LinkResolver {
4848
/// - topicRenderReference: The render reference for this external topic.
4949
/// - renderReferenceDependencies: Any dependencies for the render reference.
5050
/// - sourceLanguages: The different source languages for which this page is available.
51+
/// - symbolKind: The kind of symbol that's being referenced.
5152
@_spi(ExternalLinks)
52-
public init(topicRenderReference: TopicRenderReference, renderReferenceDependencies: RenderReferenceDependencies, sourceLanguages: Set<SourceLanguage>) {
53+
public init(
54+
topicRenderReference: TopicRenderReference,
55+
renderReferenceDependencies: RenderReferenceDependencies,
56+
sourceLanguages: Set<SourceLanguage>,
57+
symbolKind: SymbolGraph.Symbol.KindIdentifier? = nil
58+
) {
5359
self.topicRenderReference = topicRenderReference
5460
self.renderReferenceDependencies = renderReferenceDependencies
5561
self.sourceLanguages = sourceLanguages
62+
self.symbolKind = symbolKind
5663
}
5764

5865
/// The render reference for this external topic.
@@ -63,7 +70,13 @@ public class LinkResolver {
6370
var renderReferenceDependencies: RenderReferenceDependencies
6471
/// The different source languages for which this page is available.
6572
var sourceLanguages: Set<SourceLanguage>
66-
73+
/// The kind of symbol that's being referenced.
74+
///
75+
/// This value is `nil` if the entity does not reference a symbol.
76+
///
77+
/// For example, the navigator requires specific knowledge about what type of external symbol is being linked to.
78+
var symbolKind: SymbolGraph.Symbol.KindIdentifier?
79+
6780
/// Creates a pre-render new topic content value to be added to a render context's reference store.
6881
func topicContent() -> RenderReferenceStore.TopicContent {
6982
return .init(

Sources/SwiftDocC/Model/DocumentationNode.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,7 @@ public struct DocumentationNode {
674674
case .union: return .union
675675
case .`var`: return .globalVariable
676676
case .module: return .module
677+
case .extension: return .extension
677678
case .extendedModule: return .extendedModule
678679
case .extendedStructure: return .extendedStructure
679680
case .extendedClass: return .extendedClass
@@ -684,6 +685,55 @@ public struct DocumentationNode {
684685
}
685686
}
686687

688+
/// Returns a symbol kind for the given documentation node.
689+
/// - Parameter symbol: A documentation node kind.
690+
/// - Returns: A symbol graph symbol.
691+
static func symbolKind(for kind: Kind) -> SymbolGraph.Symbol.KindIdentifier? {
692+
switch kind {
693+
case .associatedType: return .`associatedtype`
694+
case .class: return .`class`
695+
case .deinitializer: return .`deinit`
696+
case .dictionary, .object: return .dictionary
697+
case .dictionaryKey: return .dictionaryKey
698+
case .enumeration: return .`enum`
699+
case .enumerationCase: return .`case`
700+
case .function: return .`func`
701+
case .httpRequest: return .httpRequest
702+
case .httpParameter: return .httpParameter
703+
case .httpBody: return .httpBody
704+
case .httpResponse: return .httpResponse
705+
case .operator: return .`operator`
706+
case .initializer: return .`init`
707+
case .instanceVariable: return .ivar
708+
case .macro: return .macro
709+
case .instanceMethod: return .`method`
710+
case .namespace: return .namespace
711+
case .instanceProperty: return .`property`
712+
case .protocol: return .`protocol`
713+
case .snippet: return .snippet
714+
case .structure: return .`struct`
715+
case .instanceSubscript: return .`subscript`
716+
case .typeMethod: return .`typeMethod`
717+
case .typeProperty, .typeConstant: return .`typeProperty`
718+
case .typeSubscript: return .`typeSubscript`
719+
case .typeAlias, .typeDef: return .`typealias`
720+
case .union: return .union
721+
case .globalVariable, .localVariable: return .`var`
722+
case .module: return .module
723+
case .extension: return .extension
724+
case .extendedModule: return .extendedModule
725+
case .extendedStructure: return .extendedStructure
726+
case .extendedClass: return .extendedClass
727+
case .extendedEnumeration: return .extendedEnumeration
728+
case .extendedProtocol: return .extendedProtocol
729+
case .unknownExtendedType: return .unknownExtendedType
730+
default:
731+
// For non-symbol kinds (like .article, .tutorial, etc.),
732+
// return nil since these don't have corresponding SymbolGraph.Symbol.KindIdentifier values
733+
return nil
734+
}
735+
}
736+
687737
/// Initializes a documentation node to represent a symbol from a symbol graph.
688738
///
689739
/// - Parameters:

Tests/SwiftDocCTests/Benchmark/ExternalTopicsHashTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ class ExternalTopicsGraphHashTests: XCTestCase {
3131
estimatedTime: nil
3232
),
3333
renderReferenceDependencies: .init(),
34-
sourceLanguages: [.swift]
34+
sourceLanguages: [.swift],
35+
symbolKind: .class
3536
)
3637
return (reference, entity)
3738
}

Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class ExternalRenderNodeTests: XCTestCase {
9797

9898
XCTAssertEqual(externalRenderNodes[1].identifier.absoluteString, "doc://org.swift.MixedLanguageFramework/example/path/to/external/objCSymbol")
9999
XCTAssertEqual(externalRenderNodes[1].kind, .symbol)
100-
XCTAssertEqual(externalRenderNodes[1].symbolKind, nil)
100+
XCTAssertEqual(externalRenderNodes[1].symbolKind, .func)
101101
XCTAssertEqual(externalRenderNodes[1].role, "symbol")
102102
XCTAssertEqual(externalRenderNodes[1].externalIdentifier.identifier, "doc://com.test.external/path/to/external/objCSymbol")
103103
XCTAssertFalse(externalRenderNodes[1].isBeta)
@@ -111,7 +111,7 @@ class ExternalRenderNodeTests: XCTestCase {
111111

112112
XCTAssertEqual(externalRenderNodes[3].identifier.absoluteString, "doc://org.swift.MixedLanguageFramework/example/path/to/external/swiftSymbol")
113113
XCTAssertEqual(externalRenderNodes[3].kind, .symbol)
114-
XCTAssertEqual(externalRenderNodes[3].symbolKind, nil)
114+
XCTAssertEqual(externalRenderNodes[3].symbolKind, .class)
115115
XCTAssertEqual(externalRenderNodes[3].role, "symbol")
116116
XCTAssertEqual(externalRenderNodes[3].externalIdentifier.identifier, "doc://com.test.external/path/to/external/swiftSymbol")
117117
XCTAssertTrue(externalRenderNodes[3].isBeta)
@@ -143,7 +143,8 @@ class ExternalRenderNodeTests: XCTestCase {
143143
navigatorTitleVariants: .init(defaultValue: navigatorTitle, objectiveCValue: occNavigatorTitle)
144144
),
145145
renderReferenceDependencies: .init(),
146-
sourceLanguages: [SourceLanguage(name: "swift"), SourceLanguage(name: "objc")])
146+
sourceLanguages: [SourceLanguage(name: "swift"), SourceLanguage(name: "objc")],
147+
symbolKind: .func)
147148
let externalRenderNode = ExternalRenderNode(
148149
externalEntity: externalEntity,
149150
bundleIdentifier: "com.test.external"
@@ -222,6 +223,8 @@ class ExternalRenderNodeTests: XCTestCase {
222223
XCTAssert(swiftExternalNodes.first { $0.title == "SwiftSymbol" }?.isBeta == true)
223224
XCTAssert(occExternalNodes.first { $0.title == "ObjCArticle" }?.isBeta == true)
224225
XCTAssert(occExternalNodes.first { $0.title == "ObjCSymbol" }?.isBeta == false)
226+
XCTAssertEqual(swiftExternalNodes.map(\.type), ["article", "class"])
227+
XCTAssertEqual(occExternalNodes.map(\.type), ["article", "func"])
225228
}
226229

227230
func testNavigatorWithExternalNodesOnlyAddsCuratedNodesToNavigator() async throws {
@@ -282,6 +285,8 @@ class ExternalRenderNodeTests: XCTestCase {
282285
XCTAssertEqual(occExternalNodes.map(\.title), ["ObjCSymbol"])
283286
XCTAssert(swiftExternalNodes.allSatisfy(\.isExternal))
284287
XCTAssert(occExternalNodes.allSatisfy(\.isExternal))
288+
XCTAssertEqual(swiftExternalNodes.map(\.type), ["article"])
289+
XCTAssertEqual(occExternalNodes.map(\.type), ["func"])
285290
}
286291

287292
func testExternalRenderNodeVariantRepresentationWhenIsBeta() throws {

Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ class ExternalReferenceResolverTests: XCTestCase {
5252
}
5353
),
5454
renderReferenceDependencies: RenderReferenceDependencies(),
55-
sourceLanguages: [resolvedEntityLanguage]
55+
sourceLanguages: [resolvedEntityLanguage],
56+
symbolKind: nil
5657
)
5758
}
5859
}
@@ -641,7 +642,8 @@ class ExternalReferenceResolverTests: XCTestCase {
641642
estimatedTime: nil
642643
),
643644
renderReferenceDependencies: RenderReferenceDependencies(),
644-
sourceLanguages: [.swift]
645+
sourceLanguages: [.swift],
646+
symbolKind: .property
645647
)
646648
}
647649
}

Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ class TestMultiResultExternalReferenceResolver: ExternalDocumentationSource {
110110
images: entityInfo.topicImages?.map(\.0) ?? []
111111
),
112112
renderReferenceDependencies: dependencies,
113-
sourceLanguages: [entityInfo.language]
113+
sourceLanguages: [entityInfo.language],
114+
symbolKind: DocumentationNode.symbolKind(for: entityInfo.kind)
114115
)
115116
}
116117
}

Tests/SwiftDocCTests/Model/DocumentationNodeTests.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import Foundation
1212
import Markdown
1313
@testable import SwiftDocC
14+
import SymbolKit
1415
import XCTest
1516

1617
class DocumentationNodeTests: XCTestCase {
@@ -41,4 +42,38 @@ class DocumentationNodeTests: XCTestCase {
4142
XCTAssertEqual(anchorSection.reference, node.reference.withFragment(expectedTitle))
4243
}
4344
}
45+
46+
func testDocumentationKindToSymbolKindMapping() throws {
47+
// Testing all symbol kinds map to a documentation kind
48+
for symbolKind in SymbolGraph.Symbol.KindIdentifier.allCases {
49+
let documentationKind = DocumentationNode.kind(forKind: symbolKind)
50+
guard documentationKind != .unknown else {
51+
continue
52+
}
53+
54+
let roundtrippedSymbolKind = DocumentationNode.symbolKind(for: documentationKind)
55+
XCTAssertEqual(symbolKind, roundtrippedSymbolKind)
56+
}
57+
58+
// Testing that documentation kinds correctly map to a symbol kind
59+
// Sometimes there are multiple mappings from DocumentationKind -> SymbolKind, exclude those here and test them separately
60+
let documentationKinds = DocumentationNode.Kind.allKnownValues
61+
.filter({ ![.localVariable, .typeDef, .typeConstant, .`keyword`, .tag, .object].contains($0) })
62+
for documentationKind in documentationKinds {
63+
let symbolKind = DocumentationNode.symbolKind(for: documentationKind)
64+
if documentationKind.isSymbol {
65+
let symbolKind = try XCTUnwrap(DocumentationNode.symbolKind(for: documentationKind), "Expected a symbol kind equivalent for \(documentationKind)")
66+
let rountrippedDocumentationKind = DocumentationNode.kind(forKind: symbolKind)
67+
XCTAssertEqual(documentationKind, rountrippedDocumentationKind)
68+
} else {
69+
XCTAssertNil(symbolKind)
70+
}
71+
}
72+
73+
// Test the exception documentation kinds
74+
XCTAssertEqual(DocumentationNode.symbolKind(for: .localVariable), .var)
75+
XCTAssertEqual(DocumentationNode.symbolKind(for: .typeDef), .typealias)
76+
XCTAssertEqual(DocumentationNode.symbolKind(for: .typeConstant), .typeProperty)
77+
XCTAssertEqual(DocumentationNode.symbolKind(for: .object), .dictionary)
78+
}
4479
}

0 commit comments

Comments
 (0)