diff --git a/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/DeclarationsSectionTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/DeclarationsSectionTranslator.swift index f679cf6c58..2ea4adb542 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/DeclarationsSectionTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/DeclarationsSectionTranslator.swift @@ -13,7 +13,8 @@ import SymbolKit typealias OverloadDeclaration = ( declaration: [SymbolGraph.Symbol.DeclarationFragments.Fragment], - reference: ResolvedTopicReference + reference: ResolvedTopicReference, + conformance: ConformanceSection? ) /// Translates a symbol's declaration into a render node's Declarations section. @@ -141,7 +142,9 @@ struct DeclarationsSectionTranslator: RenderSectionTranslator { commonFragments: commonFragments) otherDeclarations.append(.init( tokens: translatedDeclaration, - identifier: overloadDeclaration.reference.absoluteString)) + identifier: overloadDeclaration.reference.absoluteString, + conformance: overloadDeclaration.conformance + )) // Add a topic reference to the overload renderNodeTranslator.collectedTopicReferences.append( @@ -160,6 +163,8 @@ struct DeclarationsSectionTranslator: RenderSectionTranslator { return nil } + let conformance = renderNodeTranslator.contentRenderer.conformanceSectionFor(overloadReference, collectedConstraints: [:]) + let declarationFragments = overload.declarationVariants[trait]?.values .first? .declarationFragments @@ -168,7 +173,7 @@ struct DeclarationsSectionTranslator: RenderSectionTranslator { "Overloaded symbols must have declaration fragments." ) return declarationFragments.map({ - (declaration: $0, reference: overloadReference) + (declaration: $0, reference: overloadReference, conformance: conformance) }) } @@ -204,13 +209,13 @@ struct DeclarationsSectionTranslator: RenderSectionTranslator { // Pre-process the declarations by splitting text fragments apart to increase legibility let mainDeclaration = declaration.declarationFragments.flatMap(preProcessFragment(_:)) let processedOverloadDeclarations = overloadDeclarations.map({ - OverloadDeclaration($0.declaration.flatMap(preProcessFragment(_:)), $0.reference) + OverloadDeclaration($0.declaration.flatMap(preProcessFragment(_:)), $0.reference, $0.conformance) }) // Collect the "common fragments" so we can highlight the ones that are different // in each declaration let commonFragments = commonFragments( - for: (mainDeclaration, renderNode.identifier), + for: (mainDeclaration, renderNode.identifier, nil), overloadDeclarations: processedOverloadDeclarations) renderedTokens = translateDeclaration( diff --git a/Sources/SwiftDocC/Model/Rendering/Symbol/DeclarationsRenderSection.swift b/Sources/SwiftDocC/Model/Rendering/Symbol/DeclarationsRenderSection.swift index bf7ab620ca..af90cff090 100644 --- a/Sources/SwiftDocC/Model/Rendering/Symbol/DeclarationsRenderSection.swift +++ b/Sources/SwiftDocC/Model/Rendering/Symbol/DeclarationsRenderSection.swift @@ -184,11 +184,14 @@ public struct DeclarationRenderSection: Codable, Equatable { /// The symbol's identifier. public let identifier: String - + + public let conformance: ConformanceSection? + /// Creates a new other declaration for a symbol that is connected to this one, e.g. an overload. - public init(tokens: [Token], identifier: String) { + public init(tokens: [Token], identifier: String, conformance: ConformanceSection? = nil) { self.tokens = tokens self.identifier = identifier + self.conformance = conformance } } /// The displayable declarations for this symbol's overloads. diff --git a/Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderNode.spec.json b/Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderNode.spec.json index c93a09de13..4ced315007 100644 --- a/Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderNode.spec.json +++ b/Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderNode.spec.json @@ -2591,6 +2591,9 @@ }, "identifier": { "type": "string" + }, + "conformance": { + "$ref" : "#/components/schemas/ConformanceSection" } } }, diff --git a/Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift b/Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift index 8dfb245c74..6e2dfe9ed2 100644 --- a/Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift +++ b/Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift @@ -356,6 +356,50 @@ class DeclarationsRenderSectionTests: XCTestCase { XCTAssert(declarations.tokens.allSatisfy({ $0.highlight == nil })) } } + + func testOverloadConformanceDataIsSavedWithDeclarations() throws { + enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) + + let symbolGraphFile = Bundle.module.url( + forResource: "ConformanceOverloads", + withExtension: "symbols.json", + subdirectory: "Test Resources" + )! + + let tempURL = try createTempFolder(content: [ + Folder(name: "unit-test.docc", content: [ + InfoPlist(displayName: "ConformanceOverloads", identifier: "com.test.example"), + CopyOfFile(original: symbolGraphFile), + ]) + ]) + + let (_, bundle, context) = try loadBundle(from: tempURL) + + // MyClass + // - myFunc() where T: Equatable + // - myFunc() where T: Hashable // <- overload group + let reference = ResolvedTopicReference( + bundleIdentifier: bundle.identifier, + path: "/documentation/ConformanceOverloads/MyClass/myFunc()", + sourceLanguage: .swift + ) + let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) + var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) + let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first) + XCTAssertEqual(declarationsSection.declarations.count, 1) + let declarations = try XCTUnwrap(declarationsSection.declarations.first) + + let otherDeclarations = try XCTUnwrap(declarations.otherDeclarations) + XCTAssertEqual(otherDeclarations.declarations.count, 1) + + XCTAssertEqual(otherDeclarations.declarations.first?.conformance?.constraints, [ + .codeVoice(code: "T"), + .text(" conforms to "), + .codeVoice(code: "Equatable"), + .text("."), + ]) + } } /// Render a list of declaration tokens as a plain-text decoration and as a plain-text rendering of which characters are highlighted. diff --git a/Tests/SwiftDocCTests/Test Resources/ConformanceOverloads.symbols.json b/Tests/SwiftDocCTests/Test Resources/ConformanceOverloads.symbols.json new file mode 100644 index 0000000000..999b4962a2 --- /dev/null +++ b/Tests/SwiftDocCTests/Test Resources/ConformanceOverloads.symbols.json @@ -0,0 +1,292 @@ +{ + "metadata": { + "formatVersion": { + "major": 0, + "minor": 6, + "patch": 0 + }, + "generator": "Apple Swift version 6.0 (swiftlang-6.0.0.5.15 clang-1600.0.22.6)" + }, + "module": { + "name": "ConformanceOverloads", + "platform": { + "architecture": "arm64", + "vendor": "apple", + "operatingSystem": { + "name": "macosx", + "minimumVersion": { + "major": 12, + "minor": 4 + } + } + } + }, + "symbols": [ + { + "kind": { + "identifier": "swift.class", + "displayName": "Class" + }, + "identifier": { + "precise": "s:9ConformanceOverloads7MyClassC", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "MyClass" + ], + "names": { + "title": "MyClass", + "navigator": [ + { + "kind": "identifier", + "spelling": "MyClass" + } + ], + "subHeading": [ + { + "kind": "keyword", + "spelling": "class" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "MyClass" + } + ] + }, + "swiftGenerics": { + "parameters": [ + { + "name": "T", + "index": 0, + "depth": 0 + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "class" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "MyClass" + }, + { + "kind": "text", + "spelling": "<" + }, + { + "kind": "genericParameter", + "spelling": "T" + }, + { + "kind": "text", + "spelling": ">" + } + ], + "accessLevel": "public" + }, + { + "kind": { + "identifier": "swift.method", + "displayName": "Instance Method" + }, + "identifier": { + "precise": "s:9ConformanceOverloads7MyClassCAASHRzlE6myFuncyyF", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "MyClass", + "myFunc()" + ], + "names": { + "title": "myFunc()", + "subHeading": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "myFunc" + }, + { + "kind": "text", + "spelling": "()" + } + ] + }, + "functionSignature": { + "returns": [ + { + "kind": "text", + "spelling": "()" + } + ] + }, + "swiftGenerics": { + "parameters": [ + { + "name": "T", + "index": 0, + "depth": 0 + } + ], + "constraints": [ + { + "kind": "conformance", + "lhs": "T", + "rhs": "Hashable", + "rhsPrecise": "s:SH" + } + ] + }, + "swiftExtension": { + "extendedModule": "ConformanceOverloads", + "typeKind": "swift.class", + "constraints": [ + { + "kind": "conformance", + "lhs": "T", + "rhs": "Hashable", + "rhsPrecise": "s:SH" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "myFunc" + }, + { + "kind": "text", + "spelling": "()" + } + ], + "accessLevel": "public" + }, + { + "kind": { + "identifier": "swift.method", + "displayName": "Instance Method" + }, + "identifier": { + "precise": "s:9ConformanceOverloads7MyClassCAASQRzlE6myFuncyyF", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "MyClass", + "myFunc()" + ], + "names": { + "title": "myFunc()", + "subHeading": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "myFunc" + }, + { + "kind": "text", + "spelling": "()" + } + ] + }, + "functionSignature": { + "returns": [ + { + "kind": "text", + "spelling": "()" + } + ] + }, + "swiftGenerics": { + "parameters": [ + { + "name": "T", + "index": 0, + "depth": 0 + } + ], + "constraints": [ + { + "kind": "conformance", + "lhs": "T", + "rhs": "Equatable", + "rhsPrecise": "s:SQ" + } + ] + }, + "swiftExtension": { + "extendedModule": "ConformanceOverloads", + "typeKind": "swift.class", + "constraints": [ + { + "kind": "conformance", + "lhs": "T", + "rhs": "Equatable", + "rhsPrecise": "s:SQ" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "myFunc" + }, + { + "kind": "text", + "spelling": "()" + } + ], + "accessLevel": "public" + } + ], + "relationships": [ + { + "kind": "memberOf", + "source": "s:9ConformanceOverloads7MyClassCAASHRzlE6myFuncyyF", + "target": "s:9ConformanceOverloads7MyClassC" + }, + { + "kind": "memberOf", + "source": "s:9ConformanceOverloads7MyClassCAASQRzlE6myFuncyyF", + "target": "s:9ConformanceOverloads7MyClassC" + } + ] +}