Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make default availability inheritance behaviour customizable. #1024

Closed
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 @@ -47,6 +47,27 @@ struct SymbolGraphLoader {

/// The symbol graph decoding strategy to use.
private(set) var decodingStrategy: DecodingConcurrencyStrategy = .concurrentlyEachFileInBatches

/// The symbol default availability.
func symbolDefaultAvailability(_ moduleAvailability: [DefaultAvailability.ModuleAvailability]?) -> [DefaultAvailability.ModuleAvailability]? {
// Apply default availability options mutations.
guard let moduleAvailability else { return nil }
if let defaultAvailabilityOptions = bundle.info.defaultAvailabilityOptions {
// Remove the availability version if `inheritVersionNumber` is not part
// of the default availability options.
if !defaultAvailabilityOptions.shouldApplyOption(.inheritVersionNumber) {
return moduleAvailability.map { defaultAvailability in
var defaultAvailability = defaultAvailability
switch defaultAvailability.versionInformation {
case .available(_): defaultAvailability.versionInformation = .available(version: nil)
case .unavailable: ()
}
return defaultAvailability
}
}
}
return moduleAvailability
}

/// Loads all symbol graphs in the given bundle.
///
Expand Down Expand Up @@ -153,7 +174,7 @@ struct SymbolGraphLoader {
var defaultUnavailablePlatforms = [PlatformName]()
var defaultAvailableInformation = [DefaultAvailability.ModuleAvailability]()

if let defaultAvailabilities = bundle.info.defaultAvailability?.modules[unifiedGraph.moduleName] {
if let defaultAvailabilities = symbolDefaultAvailability(bundle.info.defaultAvailability?.modules[unifiedGraph.moduleName]) {
let (unavailablePlatforms, availablePlatforms) = defaultAvailabilities.categorize(where: { $0.versionInformation == .unavailable })
defaultUnavailablePlatforms = unavailablePlatforms.map(\.platformName)
defaultAvailableInformation = availablePlatforms
Expand Down Expand Up @@ -282,7 +303,7 @@ struct SymbolGraphLoader {
private func addDefaultAvailability(to symbolGraph: inout SymbolGraph, moduleName: String) {
let selector = UnifiedSymbolGraph.Selector(forSymbolGraph: symbolGraph)
// Check if there are defined default availabilities for the current module
if let defaultAvailabilities = bundle.info.defaultAvailability?.modules[moduleName],
if let defaultAvailabilities = symbolDefaultAvailability(bundle.info.defaultAvailability?.modules[moduleName]),
let platformName = symbolGraph.module.platform.name.map(PlatformName.init) {

// Prepare a default availability versions lookup for this module.
Expand Down Expand Up @@ -401,10 +422,12 @@ extension SymbolGraph.SemanticVersion {
extension SymbolGraph.Symbol.Availability.AvailabilityItem {
/// Create an availability item with a `domain` and an `introduced` version.
/// - parameter defaultAvailability: Default availability information for symbols that lack availability authored in code.
/// - Note: If the `defaultAvailability` argument doesn't have a valid
/// platform version that can be parsed as a `SemanticVersion`, returns `nil`.
/// - Note: If the `defaultAvailability` argument has a introduced version that can't
/// be parsed as a `SemanticVersion`, returns `nil`.
init?(_ defaultAvailability: DefaultAvailability.ModuleAvailability) {
guard let introducedVersion = defaultAvailability.introducedVersion, let platformVersion = SymbolGraph.SemanticVersion(string: introducedVersion) else {
let introducedVersion = defaultAvailability.introducedVersion
let platformVersion = introducedVersion.map { SymbolGraph.SemanticVersion(string: $0) } ?? nil
if platformVersion == nil && introducedVersion != nil {
return nil
}
let domain = SymbolGraph.Symbol.Availability.Domain(rawValue: defaultAvailability.platformName.rawValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public struct DefaultAvailability: Codable, Equatable {
/// Unavailable or Available with an introduced version.
enum VersionInformation: Hashable {
case unavailable
case available(version: String)
case available(version: String?)
}

/// The name of the platform, e.g. "macOS".
Expand All @@ -71,7 +71,7 @@ public struct DefaultAvailability: Codable, Equatable {
public var introducedVersion: String? {
switch versionInformation {
case .available(let introduced):
return introduced.description
return introduced?.description
case .unavailable:
return nil
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2024 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation

/// A collection of options that customaise the default availability behaviour.
///
/// Default availability options are applied to all the modules contained in the documentation bundle.
///
/// This information can be authored in the bundle's Info.plist file, as a dictionary of option name and boolean pairs.
///
/// ```
/// <key>CDDefaultAvailabilityOptions</key>
/// <dict>
/// <key>OptionName</key>
/// <bool/>
/// </dict>
/// ```
public struct DefaultAvailabilityOptions: Codable, Equatable {

/// A set of non-standard behaviors that apply to this node.
fileprivate(set) var options: Options

/// Options that specify behaviors of the default availability logic.
struct Options: OptionSet {

let rawValue: Int

/// Enable or disable symbol availability version inference from the module default availability.
static let inheritVersionNumber = Options(rawValue: 1 << 0)
}

/// String representation of the default availability options.
private enum CodingKeys: String, CodingKey {
case inheritVersionNumber = "InheritVersionNumber"
}

public init() {
self.options = .inheritVersionNumber
}

public init(from decoder: any Decoder) throws {
self.init()
let values = try decoder.container(keyedBy: CodingKeys.self)
if try values.decodeIfPresent(Bool.self, forKey: .inheritVersionNumber) == false {
options.remove(.inheritVersionNumber)
}
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if !options.contains(.inheritVersionNumber) {
try container.encode(false, forKey: .inheritVersionNumber)
}
}

/// Convenient method to determine if an option has to be applied depending
/// on it's exsistence inside the options set.
func shouldApplyOption(_ option: Options) -> Bool {
switch option {
case .inheritVersionNumber:
return options.contains(.inheritVersionNumber)
default:
return false
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@
import Foundation

extension DocumentationBundle {

/// Options to define the inherit default availability behaviour.
public enum InheritDefaultAvailabilityOptions: String, Codable {
/// The platforms with the designated versions defined in the default availability will be used by the symbols as availability information.
/// This is the default behaviour.
case platformAndVersion
/// Only the platforms defined in the default availability will be passed to the symbols.
case platformOnly
}

/// Information about a documentation bundle that's unrelated to its documentation content.
///
/// This information is meant to be decoded from the bundle's Info.plist file.
Expand Down Expand Up @@ -39,6 +49,10 @@ extension DocumentationBundle {
/// The keys that must be present in an Info.plist file in order for doc compilation to proceed.
static let requiredKeys: Set<CodingKeys> = [.displayName, .identifier]

/// The default availability behaviour options.
public var defaultAvailabilityOptions: DefaultAvailabilityOptions?


enum CodingKeys: String, CodingKey, CaseIterable {
case displayName = "CFBundleDisplayName"
case identifier = "CFBundleIdentifier"
Expand All @@ -47,6 +61,7 @@ extension DocumentationBundle {
case defaultAvailability = "CDAppleDefaultAvailability"
case defaultModuleKind = "CDDefaultModuleKind"
case featureFlags = "CDExperimentalFeatureFlags"
case defaultAvailabilityOptions = "CDDefaultAvailabilityOptions"

var argumentName: String? {
switch self {
Expand All @@ -60,7 +75,7 @@ extension DocumentationBundle {
return "--default-code-listing-language"
case .defaultModuleKind:
return "--fallback-default-module-kind"
case .defaultAvailability, .featureFlags:
case .defaultAvailability, .defaultAvailabilityOptions, .featureFlags:
return nil
}
}
Expand Down Expand Up @@ -91,20 +106,23 @@ extension DocumentationBundle {
/// - defaultCodeListingLanguage: The default language identifier for code listings in the bundle.
/// - defaultAvailability: The default availability for the various modules in the bundle.
/// - defaultModuleKind: The default kind for the various modules in the bundle.
/// - defaultAvailabilityOptions: The options to enable or disable symbol availability logic from the module default availability.
public init(
displayName: String,
identifier: String,
version: String?,
defaultCodeListingLanguage: String?,
defaultAvailability: DefaultAvailability?,
defaultModuleKind: String?
defaultModuleKind: String?,
defaultAvailabilityOptions: DefaultAvailabilityOptions?
) {
self.displayName = displayName
self.identifier = identifier
self.version = version
self.defaultCodeListingLanguage = defaultCodeListingLanguage
self.defaultAvailability = defaultAvailability
self.defaultModuleKind = defaultModuleKind
self.defaultAvailabilityOptions = defaultAvailabilityOptions
}

/// Creates documentation bundle information from the given Info.plist data, falling back to the values
Expand Down Expand Up @@ -234,6 +252,8 @@ extension DocumentationBundle {

self.defaultCodeListingLanguage = try decodeOrFallbackIfPresent(String.self, with: .defaultCodeListingLanguage)
self.defaultModuleKind = try decodeOrFallbackIfPresent(String.self, with: .defaultModuleKind)
self.defaultAvailabilityOptions = try decodeOrFallbackIfPresent(DefaultAvailabilityOptions.self, with: .defaultAvailabilityOptions)
self.defaultAvailability = try decodeOrFallbackIfPresent(DefaultAvailability.self, with: .defaultAvailability)
self.defaultAvailability = try decodeOrFallbackIfPresent(DefaultAvailability.self, with: .defaultAvailability)
self.featureFlags = try decodeOrFallbackIfPresent(BundleFeatureFlags.self, with: .featureFlags)
}
Expand All @@ -245,7 +265,9 @@ extension DocumentationBundle {
defaultCodeListingLanguage: String? = nil,
defaultModuleKind: String? = nil,
defaultAvailability: DefaultAvailability? = nil,
featureFlags: BundleFeatureFlags? = nil
featureFlags: BundleFeatureFlags? = nil,
inheritDefaultAvailability: InheritDefaultAvailabilityOptions? = nil,
defaultAvailabilityOptions: DefaultAvailabilityOptions? = nil
) {
self.displayName = displayName
self.identifier = identifier
Expand All @@ -254,6 +276,22 @@ extension DocumentationBundle {
self.defaultModuleKind = defaultModuleKind
self.defaultAvailability = defaultAvailability
self.featureFlags = featureFlags
self.defaultAvailabilityOptions = defaultAvailabilityOptions
}

public func encode(to encoder: any Encoder) throws {
var container: KeyedEncodingContainer<DocumentationBundle.Info.CodingKeys> = encoder.container(keyedBy: DocumentationBundle.Info.CodingKeys.self)

try container.encode(self.displayName, forKey: DocumentationBundle.Info.CodingKeys.displayName)
try container.encode(self.identifier, forKey: DocumentationBundle.Info.CodingKeys.identifier)
try container.encodeIfPresent(self.version, forKey: DocumentationBundle.Info.CodingKeys.version)
try container.encodeIfPresent(self.defaultCodeListingLanguage, forKey: DocumentationBundle.Info.CodingKeys.defaultCodeListingLanguage)
try container.encodeIfPresent(self.defaultAvailability, forKey: DocumentationBundle.Info.CodingKeys.defaultAvailability)
try container.encodeIfPresent(self.defaultModuleKind, forKey: DocumentationBundle.Info.CodingKeys.defaultModuleKind)
try container.encodeIfPresent(self.featureFlags, forKey: DocumentationBundle.Info.CodingKeys.featureFlags)
if defaultAvailabilityOptions != .init() {
try container.encodeIfPresent(self.defaultAvailabilityOptions, forKey: DocumentationBundle.Info.CodingKeys.defaultAvailabilityOptions)
}
}
}
}
Expand All @@ -272,14 +310,16 @@ extension BundleDiscoveryOptions {
/// - fallbackDefaultModuleKind: A fallback default module kind for the bundle.
/// - fallbackDefaultAvailability: A fallback default availability for the bundle.
/// - additionalSymbolGraphFiles: Additional symbol graph files to augment any discovered bundles.
/// - defaultAvailabilityOptions: Options to configure default availability behaviour.
public init(
fallbackDisplayName: String? = nil,
fallbackIdentifier: String? = nil,
fallbackVersion: String? = nil,
fallbackDefaultCodeListingLanguage: String? = nil,
fallbackDefaultModuleKind: String? = nil,
fallbackDefaultAvailability: DefaultAvailability? = nil,
additionalSymbolGraphFiles: [URL] = []
additionalSymbolGraphFiles: [URL] = [],
defaultAvailabilityOptions: DefaultAvailabilityOptions? = nil
) {
// Iterate over all possible coding keys with a switch
// to build up the dictionary of fallback options.
Expand All @@ -304,6 +344,8 @@ extension BundleDiscoveryOptions {
value = fallbackDefaultModuleKind
case .featureFlags:
value = nil
case .defaultAvailabilityOptions:
value = defaultAvailabilityOptions
}

guard let unwrappedValue = value else {
Expand Down
6 changes: 4 additions & 2 deletions Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1241,8 +1241,10 @@ public struct RenderNodeTranslator: SemanticVisitor {
node.metadata.platformsVariants = VariantCollection<[AvailabilityRenderItem]?>(from: symbol.availabilityVariants) { _, availability in
availability.availability
.compactMap { availability -> AvailabilityRenderItem? in
// Filter items with insufficient availability data
guard availability.introducedVersion != nil else {
// Filter items with insufficient availability data unless the default availability behaviour
// allows availability withound version information.
let omitDefaultAvailabilityVersionFromSymbols = bundle.info.defaultAvailabilityOptions?.shouldApplyOption(.inheritVersionNumber) == false
guard availability.introducedVersion != nil || omitDefaultAvailabilityVersionFromSymbols else {
return nil
}
guard let name = availability.domain.map({ PlatformName(operatingSystemName: $0.rawValue) }),
Expand Down
Loading