diff --git a/Sources/SwiftDocC/Semantics/ReferenceResolver.swift b/Sources/SwiftDocC/Semantics/ReferenceResolver.swift index 3453ade5e..f0f461e3c 100644 --- a/Sources/SwiftDocC/Semantics/ReferenceResolver.swift +++ b/Sources/SwiftDocC/Semantics/ReferenceResolver.swift @@ -493,6 +493,7 @@ struct ReferenceResolver: SemanticVisitor { deprecatedSummaryVariants: newDeprecatedSummaryVariants, mixinsVariants: symbol.mixinsVariants, declarationVariants: symbol.declarationVariants, + alternateDeclarationVariants: symbol.alternateDeclarationVariants, defaultImplementationsVariants: symbol.defaultImplementationsVariants, relationshipsVariants: symbol.relationshipsVariants, abstractSectionVariants: newAbstractVariants, diff --git a/Sources/SwiftDocC/Semantics/Symbol/Symbol.swift b/Sources/SwiftDocC/Semantics/Symbol/Symbol.swift index 054e9f35f..c4eb502f9 100644 --- a/Sources/SwiftDocC/Semantics/Symbol/Symbol.swift +++ b/Sources/SwiftDocC/Semantics/Symbol/Symbol.swift @@ -265,6 +265,7 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups deprecatedSummaryVariants: DocumentationDataVariants, mixinsVariants: DocumentationDataVariants<[String: Mixin]>, declarationVariants: DocumentationDataVariants<[[PlatformName?]: SymbolGraph.Symbol.DeclarationFragments]> = .init(defaultVariantValue: [:]), + alternateDeclarationVariants: DocumentationDataVariants<[[PlatformName?]: [SymbolGraph.Symbol.DeclarationFragments]]> = .init(defaultVariantValue: [:]), defaultImplementationsVariants: DocumentationDataVariants = .init(defaultVariantValue: .init()), relationshipsVariants: DocumentationDataVariants = .init(), abstractSectionVariants: DocumentationDataVariants, @@ -303,6 +304,7 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups self.deprecatedSummaryVariants = deprecatedSummaryVariants self.declarationVariants = declarationVariants + self.alternateDeclarationVariants = alternateDeclarationVariants self.mixinsVariants = mixinsVariants @@ -320,8 +322,10 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups case let spi as SymbolGraph.Symbol.SPI: self.isSPIVariants[trait] = spi.isSPI case let alternateDeclarations as SymbolGraph.Symbol.AlternateDeclarations: - self.alternateDeclarationVariants[trait] = [[platformNameVariants[trait]]: alternateDeclarations.declarations] - + // If alternate declarations weren't set explicitly use the ones from the mixins. + if !self.alternateDeclarationVariants.hasVariant(for: trait) { + self.alternateDeclarationVariants[trait] = [[platformNameVariants[trait]]: alternateDeclarations.declarations] + } case let attribute as SymbolGraph.Symbol.Minimum: attributes[.minimum] = attribute.value case let attribute as SymbolGraph.Symbol.Maximum: @@ -443,7 +447,7 @@ extension Symbol { /// When building multi-platform documentation symbols might have more than one declaration /// depending on variances in their implementation across platforms (e.g. use `NSPoint` vs `CGPoint` parameter in a method). /// This method finds matching symbols between graphs and merges their declarations in case there are differences. - func mergeDeclaration(mergingDeclaration: SymbolGraph.Symbol.DeclarationFragments, identifier: String, symbolAvailability: SymbolGraph.Symbol.Availability?, selector: UnifiedSymbolGraph.Selector) throws { + func mergeDeclaration(mergingDeclaration: SymbolGraph.Symbol.DeclarationFragments, identifier: String, symbolAvailability: SymbolGraph.Symbol.Availability?, alternateDeclarations: SymbolGraph.Symbol.AlternateDeclarations?, selector: UnifiedSymbolGraph.Selector) throws { let trait = DocumentationDataVariantsTrait(for: selector) let platformName = selector.platform @@ -472,6 +476,35 @@ extension Symbol { declarationVariants[trait]?[[nil]] = mergingDeclaration } } + + if let alternateDeclarations { + let mergingAlternateDeclarations = alternateDeclarations.declarations + if let platformName, + let existingKey = alternateDeclarationVariants[trait]?.first( + where: { pair in + return pair.value.map { $0.declarationFragments } == mergingAlternateDeclarations.map { $0.declarationFragments } + } + )?.key + { + guard !existingKey.contains(nil) else { + throw DocumentationContext.ContextError.unexpectedEmptyPlatformName(identifier) + } + + let platform = PlatformName(operatingSystemName: platformName) + if !existingKey.contains(platform) { + // Matches one of the existing declarations, append to the existing key. + let currentDeclaration = alternateDeclarationVariants[trait]?.removeValue(forKey: existingKey)! + alternateDeclarationVariants[trait]?[existingKey + [platform]] = currentDeclaration + } + } else { + // Add new declaration + if let name = platformName { + alternateDeclarationVariants[trait]?[[PlatformName.init(operatingSystemName: name)]] = mergingAlternateDeclarations + } else { + alternateDeclarationVariants[trait]?[[nil]] = mergingAlternateDeclarations + } + } + } // Merge the new symbol with the existing availability. If a value already exist, only override if it's for this platform. if let symbolAvailability, @@ -503,8 +536,9 @@ extension Symbol { for (selector, mixins) in unifiedSymbol.mixins { if let mergingDeclaration = mixins[SymbolGraph.Symbol.DeclarationFragments.mixinKey] as? SymbolGraph.Symbol.DeclarationFragments { let availability = mixins[SymbolGraph.Symbol.Availability.mixinKey] as? SymbolGraph.Symbol.Availability + let alternateDeclarations = mixins[SymbolGraph.Symbol.AlternateDeclarations.mixinKey] as? SymbolGraph.Symbol.AlternateDeclarations - try mergeDeclaration(mergingDeclaration: mergingDeclaration, identifier: unifiedSymbol.uniqueIdentifier, symbolAvailability: availability, selector: selector) + try mergeDeclaration(mergingDeclaration: mergingDeclaration, identifier: unifiedSymbol.uniqueIdentifier, symbolAvailability: availability, alternateDeclarations: alternateDeclarations, selector: selector) } } } diff --git a/Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift b/Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift index 154dd173a..a6a24be58 100644 --- a/Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift +++ b/Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift @@ -149,6 +149,6 @@ class DeclarationsRenderSectionTests: XCTestCase { let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first) XCTAssertEqual(declarationsSection.declarations.count, 2) - XCTAssert(declarationsSection.declarations.allSatisfy({ $0.platforms == [.macOS] })) + XCTAssert(declarationsSection.declarations.allSatisfy({ $0.platforms == [.iOS, .macOS] })) } } diff --git a/Tests/SwiftDocCTests/Test Bundles/AlternateDeclarations.docc/AlternateDeclarations.ios.symbols.json b/Tests/SwiftDocCTests/Test Bundles/AlternateDeclarations.docc/AlternateDeclarations.ios.symbols.json new file mode 100644 index 000000000..870eb0c76 --- /dev/null +++ b/Tests/SwiftDocCTests/Test Bundles/AlternateDeclarations.docc/AlternateDeclarations.ios.symbols.json @@ -0,0 +1,368 @@ +{ + "metadata": { + "formatVersion": { + "major": 0, + "minor": 6, + "patch": 0 + }, + "generator": "Swift 5.9" + }, + "module": { + "name": "AlternateDeclarations", + "platform": { + "architecture": "arm64", + "vendor": "apple", + "operatingSystem": { + "name": "ios", + "minimumVersion": { + "major": 18, + "minor": 0 + } + } + } + }, + "symbols": [ + { + "kind": { + "identifier": "swift.class", + "displayName": "Class" + }, + "identifier": { + "precise": "c:objc(cs)MyClass", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "MyClass" + ], + "names": { + "title": "MyClass", + "navigator": [ + { + "kind": "identifier", + "spelling": "MyClass" + } + ], + "subHeading": [ + { + "kind": "keyword", + "spelling": "class" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "MyClass" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "class" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "MyClass" + } + ], + "accessLevel": "open" + }, + { + "kind": { + "identifier": "swift.method", + "displayName": "Instance Method" + }, + "identifier": { + "precise": "c:objc(cs)MyClass(im)presentWithCompletion:", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "MyClass", + "present(completion:)" + ], + "names": { + "title": "present(completion:)", + "subHeading": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "present" + }, + { + "kind": "text", + "spelling": "(" + }, + { + "kind": "externalParam", + "spelling": "completion" + }, + { + "kind": "text", + "spelling": ": ((" + }, + { + "kind": "typeIdentifier", + "spelling": "Bool", + "preciseIdentifier": "s:Sb" + }, + { + "kind": "text", + "spelling": ") -> " + }, + { + "kind": "typeIdentifier", + "spelling": "Void", + "preciseIdentifier": "s:s4Voida" + }, + { + "kind": "text", + "spelling": ")!)" + } + ] + }, + "functionSignature": { + "parameters": [ + { + "name": "completion", + "declarationFragments": [ + { + "kind": "identifier", + "spelling": "completion" + }, + { + "kind": "text", + "spelling": ": ((" + }, + { + "kind": "typeIdentifier", + "spelling": "Bool", + "preciseIdentifier": "s:Sb" + }, + { + "kind": "text", + "spelling": ") -> " + }, + { + "kind": "typeIdentifier", + "spelling": "Void", + "preciseIdentifier": "s:s4Voida" + }, + { + "kind": "text", + "spelling": ")!" + } + ] + } + ], + "returns": [ + { + "kind": "typeIdentifier", + "spelling": "Void", + "preciseIdentifier": "s:s4Voida" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "present" + }, + { + "kind": "text", + "spelling": "(" + }, + { + "kind": "externalParam", + "spelling": "completion" + }, + { + "kind": "text", + "spelling": ": ((" + }, + { + "kind": "typeIdentifier", + "spelling": "Bool", + "preciseIdentifier": "s:Sb" + }, + { + "kind": "text", + "spelling": ") -> " + }, + { + "kind": "typeIdentifier", + "spelling": "Void", + "preciseIdentifier": "s:s4Voida" + }, + { + "kind": "text", + "spelling": ")!)" + } + ], + "accessLevel": "open" + }, + { + "kind": { + "identifier": "swift.method", + "displayName": "Instance Method" + }, + "identifier": { + "precise": "c:objc(cs)MyClass(im)presentWithCompletion:", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "MyClass", + "present()" + ], + "names": { + "title": "present()", + "subHeading": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "present" + }, + { + "kind": "text", + "spelling": "() " + }, + { + "kind": "keyword", + "spelling": "async" + }, + { + "kind": "text", + "spelling": " -> " + }, + { + "kind": "typeIdentifier", + "spelling": "Bool", + "preciseIdentifier": "s:Sb" + } + ] + }, + "functionSignature": { + "returns": [ + { + "kind": "typeIdentifier", + "spelling": "Bool", + "preciseIdentifier": "s:Sb" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "present" + }, + { + "kind": "text", + "spelling": "() " + }, + { + "kind": "keyword", + "spelling": "async" + }, + { + "kind": "text", + "spelling": " -> " + }, + { + "kind": "typeIdentifier", + "spelling": "Bool", + "preciseIdentifier": "s:Sb" + } + ], + "accessLevel": "open" + } + ], + "relationships": [ + { + "kind": "conformsTo", + "source": "c:objc(cs)MyClass", + "target": "s:SQ", + "targetFallback": "Swift.Equatable" + }, + { + "kind": "memberOf", + "source": "c:objc(cs)MyClass(im)presentWithCompletion:", + "target": "c:objc(cs)MyClass" + }, + { + "kind": "inheritsFrom", + "source": "c:objc(cs)MyClass", + "target": "c:objc(cs)NSObject", + "targetFallback": "ObjectiveC.NSObject" + }, + { + "kind": "conformsTo", + "source": "c:objc(cs)MyClass", + "target": "s:SH", + "targetFallback": "Swift.Hashable" + }, + { + "kind": "conformsTo", + "source": "c:objc(cs)MyClass", + "target": "s:s23CustomStringConvertibleP", + "targetFallback": "Swift.CustomStringConvertible" + }, + { + "kind": "conformsTo", + "source": "c:objc(cs)MyClass", + "target": "s:s7CVarArgP", + "targetFallback": "Swift.CVarArg" + }, + { + "kind": "conformsTo", + "source": "c:objc(cs)MyClass", + "target": "c:objc(pl)NSObject", + "targetFallback": "ObjectiveC.NSObjectProtocol" + }, + { + "kind": "memberOf", + "source": "c:objc(cs)MyClass(im)presentWithCompletion:", + "target": "c:objc(cs)MyClass" + }, + { + "kind": "conformsTo", + "source": "c:objc(cs)MyClass", + "target": "s:s28CustomDebugStringConvertibleP", + "targetFallback": "Swift.CustomDebugStringConvertible" + } + ] +} diff --git a/Tests/SwiftDocCTests/Test Bundles/AlternateDeclarations.docc/AlternateDeclarations.symbols.json b/Tests/SwiftDocCTests/Test Bundles/AlternateDeclarations.docc/AlternateDeclarations.macos.symbols.json similarity index 100% rename from Tests/SwiftDocCTests/Test Bundles/AlternateDeclarations.docc/AlternateDeclarations.symbols.json rename to Tests/SwiftDocCTests/Test Bundles/AlternateDeclarations.docc/AlternateDeclarations.macos.symbols.json