Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -224,35 +224,37 @@ struct SymbolGraphLoader {
for (selector, _) in symbol.mixins {
if var symbolAvailability = (symbol.mixins[selector]?["availability"] as? SymbolGraph.Symbol.Availability) {
guard !symbolAvailability.availability.isEmpty else { continue }
// For platforms with a fallback option (e.g., Catalyst and iOS), apply the explicit availability annotation of the fallback platform when it is not explicitly available on the primary platform.
DefaultAvailability.fallbackPlatforms.forEach { (fallbackPlatform, inheritedPlatform) in
// For platforms with a fallback option (e.g. Catalyst and iPadOS),
// if the availability is not explicitly available for the platform,
// apply the explicit availability annotation of the fallback platform.
DefaultAvailability.fallbackPlatforms.forEach { (platform, fallback) in
guard
var inheritedAvailability = symbolAvailability.availability.first(where: {
$0.matches(inheritedPlatform)
var fallbackAvailability = symbolAvailability.availability.first(where: {
$0.matches(fallback)
}),
let fallbackAvailabilityIntroducedVersion = symbolAvailability.availability.first(where: {
$0.matches(fallbackPlatform)
let platformAvailabilityIntroducedVersion = symbolAvailability.availability.first(where: {
$0.matches(platform)
})?.introducedVersion,
let defaultAvailabilityIntroducedVersion = defaultAvailabilities.first(where: { $0.platformName == fallbackPlatform })?.introducedVersion
let defaultAvailabilityIntroducedVersion = defaultAvailabilities.first(where: { $0.platformName == platform })?.introducedVersion
else { return }
// Ensure that the availability version is not overwritten if the symbol has an explicit availability annotation for that platform.
if SymbolGraph.SemanticVersion(string: defaultAvailabilityIntroducedVersion) == fallbackAvailabilityIntroducedVersion {
inheritedAvailability.domain = SymbolGraph.Symbol.Availability.Domain(rawValue: fallbackPlatform.rawValue)
if SymbolGraph.SemanticVersion(string: defaultAvailabilityIntroducedVersion) == platformAvailabilityIntroducedVersion {
fallbackAvailability.domain = SymbolGraph.Symbol.Availability.Domain(rawValue: platform.rawValue)
symbolAvailability.availability.removeAll(where: {
$0.matches(fallbackPlatform)
$0.matches(platform)
})
symbolAvailability.availability.append(inheritedAvailability)
symbolAvailability.availability.append(fallbackAvailability)
}
}
// Add fallback availability.
for (fallbackPlatform, inheritedPlatform) in missingFallbackPlatforms {
if !symbolAvailability.contains(fallbackPlatform) {
for (platform, fallback) in missingFallbackPlatforms {
if !symbolAvailability.contains(platform) {
for var fallbackAvailability in symbolAvailability.availability {
// Add the platform fallback to the availability mixin the platform is inheriting from.
// The added availability copies the entire availability information,
// including deprecated and obsolete versions.
if fallbackAvailability.matches(inheritedPlatform) {
fallbackAvailability.domain = SymbolGraph.Symbol.Availability.Domain(rawValue: fallbackPlatform.rawValue)
if fallbackAvailability.matches(fallback) {
fallbackAvailability.domain = SymbolGraph.Symbol.Availability.Domain(rawValue: platform.rawValue)
symbolAvailability.availability.append(fallbackAvailability)
}
}
Expand Down
40 changes: 33 additions & 7 deletions Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -838,16 +838,42 @@ public struct RenderNodeTranslator: SemanticVisitor {
}
}

if let availability = article.metadata?.availability, !availability.isEmpty {
let renderAvailability = availability.compactMap({
let currentPlatform = PlatformName(metadataPlatform: $0.platform).flatMap { name in
if let availabilities = article.metadata?.availability, !availabilities.isEmpty {
let platforms = availabilities.map { PlatformName(metadataPlatform: $0.platform) }
// Render availabilities for declared platforms
let renderAvailabilities = zip(availabilities, platforms).compactMap { availability, platform -> AvailabilityRenderItem? in
let currentVersion = platform.flatMap {
context.configuration.externalMetadata.currentPlatforms?[$0.displayName]
}
return .init(availability, current: currentVersion)
}
// Render availabilities for fallback platforms
let fallbackRenderAvailabilities = DefaultAvailability.fallbackPlatforms.compactMap { platform, fallback -> AvailabilityRenderItem? in
// Skip if the platform already has explicit availability,
// or if the fallback platform is not available.
guard !platforms.contains(platform),
let fallbackIndex = platforms.firstIndex(of: fallback) else {
return nil
}

// Clone the fallback platform's availability with the new platform name.
// The `availabilities` array is mapped to the `platforms` array,
// so the indices of elements across them are guaranteed to be consistent.
let fallbackAvailability = Metadata.Availability(from: availabilities[fallbackIndex].originalMarkup, for: context.inputs)!
fallbackAvailability.platform = Metadata.Availability.Platform(rawValue: platform.rawValue)!
// Use the fallback platform's version to correctly determine beta status
let currentVersion = platforms[fallbackIndex].flatMap { name in
context.configuration.externalMetadata.currentPlatforms?[name.displayName]
}
return .init($0, current: currentPlatform)
}).sorted(by: AvailabilityRenderOrder.compare)

if !renderAvailability.isEmpty {
node.metadata.platformsVariants = .init(defaultValue: renderAvailability)
return .init(fallbackAvailability, current: currentVersion)
}

let allRenderAvailabilities = (renderAvailabilities + fallbackRenderAvailabilities)
.sorted(by: AvailabilityRenderOrder.compare)

if !allRenderAvailabilities.isEmpty {
node.metadata.platformsVariants = .init(defaultValue: allRenderAvailabilities)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,9 @@ struct DeclarationsSectionTranslator: RenderSectionTranslator {
func expandPlatformsWithFallbacks(_ platforms: [PlatformName?]) -> [PlatformName?] {
guard !platforms.isEmpty else { return platforms }

// Add fallback platforms if their primary platform is present but the fallback is missing
let fallbacks = DefaultAvailability.fallbackPlatforms.compactMap { fallback, primary in
platforms.contains(primary) && !platforms.contains(fallback) ? fallback : nil
// Add fallback platforms if the platform is missing but the fallback is present
let fallbacks = DefaultAvailability.fallbackPlatforms.compactMap { platform, fallback in
platforms.contains(fallback) && !platforms.contains(platform) ? platform : nil
}
return platforms + fallbacks
}
Expand Down
31 changes: 22 additions & 9 deletions Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,18 @@ class PlatformAvailabilityTests: XCTestCase {
let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article)
var translator = RenderNodeTranslator(context: context, identifier: reference)
let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode)
let availability = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue)
XCTAssertEqual(availability.count, 1)
let iosAvailability = try XCTUnwrap(availability.first)
XCTAssertEqual(iosAvailability.name, "iOS")
let availabilities = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue)
// iOS introduces iPadOS and Mac Catalyst as fallback platforms
XCTAssertEqual(availabilities.count, 3)
XCTAssertEqual(availabilities.compactMap { $0.name }, ["iOS", "iPadOS", "Mac Catalyst"])
let iosAvailability = try XCTUnwrap(availabilities.first)
XCTAssertEqual(iosAvailability.introduced, "16.0")
XCTAssert(iosAvailability.isBeta != true)
// Ensure that the fallback platforms have the same version and beta status as iOS
for availability in availabilities.dropFirst() {
XCTAssertEqual(availability.introduced, iosAvailability.introduced)
XCTAssertEqual(availability.isBeta, iosAvailability.isBeta)
}
}

/// Ensure that adding `@Available` directives in an extension file overrides the symbol's availability.
Expand Down Expand Up @@ -81,7 +87,8 @@ class PlatformAvailabilityTests: XCTestCase {
var translator = RenderNodeTranslator(context: context, identifier: reference)
let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode)
let availability = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue)
XCTAssertEqual(availability.count, 3)
// iOS introduces iPadOS and Mac Catalyst as fallback platforms
XCTAssertEqual(availability.count, 5)

XCTAssert(availability.contains(where: { item in
item.name == "iOS" && item.introduced == "15.0"
Expand Down Expand Up @@ -173,12 +180,17 @@ class PlatformAvailabilityTests: XCTestCase {
let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article)
var translator = RenderNodeTranslator(context: context, identifier: reference)
let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode)
let availability = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue)
XCTAssertEqual(availability.count, 1)
let iosAvailability = try XCTUnwrap(availability.first)
let availabilities = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue)
// iOS introduces iPadOS and Mac Catalyst as fallback platforms
XCTAssertEqual(availabilities.count, 3)
let iosAvailability = try XCTUnwrap(availabilities.first)
XCTAssertEqual(iosAvailability.name, "iOS")
XCTAssertEqual(iosAvailability.introduced, "16.0")
XCTAssert(iosAvailability.isBeta == true)
// Ensure that fallback platforms are also marked as beta
for availability in availabilities.dropFirst() {
XCTAssert(availability.isBeta == true)
}
}

func testMultipleBetaPlatformAvailabilityFromArticle() async throws {
Expand All @@ -197,7 +209,8 @@ class PlatformAvailabilityTests: XCTestCase {
var translator = RenderNodeTranslator(context: context, identifier: reference)
let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode)
let availability = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue)
XCTAssertEqual(availability.count, 3)
// iOS introduces iPadOS and Mac Catalyst as fallback platforms
XCTAssertEqual(availability.count, 5)

XCTAssert(availability.contains(where: { item in
item.name == "iOS" && item.introduced == "15.0"
Expand Down