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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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 @@ -59,6 +59,24 @@ struct SymbolGraphLoader {
let bundle = self.bundle
let dataProvider = self.dataProvider

/// Computes the default availbiality based on the `inheritDefaultAvailability` option.
let defaultAvailabilities: ([DefaultAvailability.ModuleAvailability]?) -> [DefaultAvailability.ModuleAvailability]? = { defautAvailabilities in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need to be a closure? I can't see a requirement for this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that you mention it; why is the symbol graph loader responsible for modifying the decoded Info.plist values?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that you mention it; why is the symbol graph loader responsible for modifying the decoded Info.plist values?

Is during the symbol graph loading that each symbol gets assigned the default availability data. I could create an intermediary structure to not modify the input data, would that be better? CC: @d-ronnqvist

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I'm missing some reason why it wouldn't be possible;
I think it would be better to create the .available(version: nil) values when decoding the Info.plist so that they don't need to be modified after the fact.

Copy link
Contributor Author

@sofiaromorales sofiaromorales Sep 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to create the .available(version: nil) values when decoding the Info.plist so that they don't need to be modified after the fact.

The framework availability information must remain. Modifying the default availability information when decoding it would remove the availability at the top level page.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to create the .available(version: nil) values when decoding the Info.plist so that they don't need to be modified after the fact.

The framework availability information must remain. Modifying the default availability information when decoding it would remove the availability at the top level page.

To play devil's advocate for a second:
We don't really have a backwards compatibility issue here because the developer needs to specify a new configuration key to opt in to the new behavior.

From one perspective it might be preferable to display the version information on the module page but not the individual symbols' pages, but from another perspective it's more consistent if the configuration applies equally to all pages. At the very least "must" is a much to strong assertion.

If the developer wants to display version information on the module page they can accomplish that by explicitly specifying introduced versions in @Available directives in the module page's documentation extension file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More seriously;
If this is intentionally deferring a modification until after a certain read-access in order to get two different behaviors out of the same property then I don't quite like that.

If we want to different behaviors then I would prefer if we made that behavioral difference explicit.

One way to accomplish that could be to move access behind a function with a parameter. Another way to accomplish that could be to introduce a second property for the other behavior.

guard let defautAvailabilities else { return nil }
// Check the selected behaviour for inheritance of the default availability and remove the avaialbity
// version if it's set to `platformOnly`.
if bundle.info.inheritDefaultAvailability == .platformOnly {
return defautAvailabilities.map { defaultAvailability in
var defaultAvailability = defaultAvailability
switch defaultAvailability.versionInformation {
case .available(_): defaultAvailability.versionInformation = .available(version: nil)
case .unavailable: ()
}
return defaultAvailability
}
}
return defautAvailabilities
}

let loadGraphAtURL: (URL) -> Void = { symbolGraphURL in
// Bail out in case a symbol graph has already errored
guard loadError == nil else { return }
Expand All @@ -79,8 +97,9 @@ struct SymbolGraphLoader {
configureSymbolGraph?(&symbolGraph)

let (moduleName, isMainSymbolGraph) = Self.moduleNameFor(symbolGraph, at: symbolGraphURL)
let defaultAvailabilities = defaultAvailabilities(bundle.info.defaultAvailability?.modules[moduleName])
// If the bundle provides availability defaults add symbol availability data.
self.addDefaultAvailability(to: &symbolGraph, moduleName: moduleName)
self.addDefaultAvailability(to: &symbolGraph, moduleName: moduleName, defaultAvailabilities: defaultAvailabilities)

// main symbol graphs are ambiguous
var usesExtensionSymbolFormat: Bool? = nil
Expand Down Expand Up @@ -153,7 +172,7 @@ struct SymbolGraphLoader {
var defaultUnavailablePlatforms = [PlatformName]()
var defaultAvailableInformation = [DefaultAvailability.ModuleAvailability]()

if let defaultAvailabilities = bundle.info.defaultAvailability?.modules[unifiedGraph.moduleName] {
if let defaultAvailabilities = defaultAvailabilities(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 @@ -279,11 +298,11 @@ struct SymbolGraphLoader {

/// If the bundle defines default availability for the symbols in the given symbol graph
/// this method adds them to each of the symbols in the graph.
private func addDefaultAvailability(to symbolGraph: inout SymbolGraph, moduleName: String) {
private func addDefaultAvailability(to symbolGraph: inout SymbolGraph, moduleName: String, defaultAvailabilities: [DefaultAvailability.ModuleAvailability]?) {
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],
let platformName = symbolGraph.module.platform.name.map(PlatformName.init) {
if let defaultAvailabilities = defaultAvailabilities,
let platformName = symbolGraph.module.platform.name.map(PlatformName.init) {

// Prepare a default availability versions lookup for this module.
let defaultAvailabilityVersionByPlatform = defaultAvailabilities
Expand Down Expand Up @@ -404,9 +423,8 @@ extension SymbolGraph.Symbol.Availability.AvailabilityItem {
/// - Note: If the `defaultAvailability` argument doesn't have a valid
/// platform version that can be parsed as a `SemanticVersion`, returns `nil`.
init?(_ defaultAvailability: DefaultAvailability.ModuleAvailability) {
guard let introducedVersion = defaultAvailability.introducedVersion, let platformVersion = SymbolGraph.SemanticVersion(string: introducedVersion) else {
return nil
}
let introducedVersion = defaultAvailability.introducedVersion
let platformVersion = introducedVersion.map { SymbolGraph.SemanticVersion(string: $0) } ?? nil
let domain = SymbolGraph.Symbol.Availability.Domain(rawValue: defaultAvailability.platformName.rawValue)
self.init(domain: domain,
introducedVersion: platformVersion,
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
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 flag to enable or disable symbol availability inference from the module default availability.
/// If not specified the default befaviout will be ``InheritDefaultAvailabilityOptions.platformAndVersion``
public var inheritDefaultAvailability: InheritDefaultAvailabilityOptions?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't quite make up my mind about how I would prefer to expose this information in the public API.

Allow me to think out loud for a bit ... 🤔

Currently, this option/configuration only determines whether or not symbols without explicit availability (either from in-source attributes or @Available directives) will display the versions specified in the Info.plist as the "introduced" version on the render page, or if those symbols will only display the platform name without an introduced version.

The key question to ask ourselves is: "how do we expect this to change in the future?"

If we think that we'll add some other configuration that's independent of this version-behavior, then we would basically need to create a second enum to avoid needing to add enum cases that represent combinations of behaviors:

case platformAndVersion
case platformOnly
case platformAndVersionWithNewBehavior
case platformOnlyWithNewBehavior

In this case, the InheritDefaultAvailabilityOptions won't extend beyond its original two cases.

If both the new and the old configuration are booleans (or other two-value configuration) then we could represent them using an OptionSet to allow for arbitrary combinations of them.

On the other hand, if the new configuration isn't a boolean, then we would need to separate it regardless. If the two configuration still are related, then maybe we would want to use a structure to group them.

All that leads me to two mini concussions:

  • We possibly won't get much future extensibility from using an enum for this configuration.
  • Maybe there's a more specific name we could use for InheritDefaultAvailabilityOptions if it only determines this specific version behavior.

🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the many possibilities—names are just placeholders—that I can imagine for this are:

A boolean value

var displayInheritedVersionNumbers = true

This would make the current call site

if !bundle.info.displayInheritedVersionNumbers { ... }

If we add more configuration in the future that uses either of the other two possibilities, we could convert this to a computed property that reads from the option set or struct.

We could also use a "negative" boolean, for example var skipInheritedVersionNumbers = false to avoid the negation at the call site.

An option set

struct DefaultAvailabilityOptions: OptionSet {
    let rawValue: Int
            
    static let displayInheritedVersionNumbers = Self(rawValue: 1 << 0)
}
var defaultAvailabilityOptions: DefaultAvailabilityOptions

This would make the current call site

if !bundle.info.defaultAvailabilityOptions.contains(.displayInheritedVersionNumbers) { ... }

If we add more boolean configuration it could be defined as new values in the option set.

New non-boolean configuration would need to separate properties.

A struct with configuration as different properties

struct DefaultAvailabilityOptions {
    var displayInheritedVersionNumbers = true
}
var defaultAvailabilityOptions: DefaultAvailabilityOptions

This would make the current call site

if !bundle.info.defaultAvailabilityOptions.displayInheritedVersionNumbers { ... }

If we add more configuration in the future they could be additional members of this struct.


Each of these alternatives (and other) have tradeoffs between now and the future.

If we believe that future changes are not-that-likely then it doesn't matter all that much.

Regardless of what we do now, we can always deprecate it and make it derive its value from the future value. The only case where that doesn't work is if we want to reuse the name but change the type (for example form an enum to a struct).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These vague ideas are a bit hard to talk about in written form like this. Let me know if you would like to about it in person instead.

I have no real concerns with this code. Just hoping for a brief conversation about the public API before it's harder to change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same goes for this comment which is about the property list representation of the same configuration.


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 inheritDefaultAvailability = "CDInheritDefaultAvailability"

var argumentName: String? {
switch self {
Expand All @@ -60,6 +75,8 @@ extension DocumentationBundle {
return "--default-code-listing-language"
case .defaultModuleKind:
return "--fallback-default-module-kind"
case .inheritDefaultAvailability:
return nil
case .defaultAvailability, .featureFlags:
return nil
}
Expand Down Expand Up @@ -91,20 +108,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.
/// - inheritDefaultAvailability: The option to enable or disable symbol availability inheritance from the module default availability.
public init(
displayName: String,
identifier: String,
version: String?,
defaultCodeListingLanguage: String?,
defaultAvailability: DefaultAvailability?,
defaultModuleKind: String?
defaultModuleKind: String?,
inheritDefaultAvailability: InheritDefaultAvailabilityOptions?
) {
self.displayName = displayName
self.identifier = identifier
self.version = version
self.defaultCodeListingLanguage = defaultCodeListingLanguage
self.defaultAvailability = defaultAvailability
self.defaultModuleKind = defaultModuleKind
self.inheritDefaultAvailability = inheritDefaultAvailability
}

/// Creates documentation bundle information from the given Info.plist data, falling back to the values
Expand Down Expand Up @@ -236,6 +256,10 @@ extension DocumentationBundle {
self.defaultModuleKind = try decodeOrFallbackIfPresent(String.self, with: .defaultModuleKind)
self.defaultAvailability = try decodeOrFallbackIfPresent(DefaultAvailability.self, with: .defaultAvailability)
self.featureFlags = try decodeOrFallbackIfPresent(BundleFeatureFlags.self, with: .featureFlags)
let inheritDefaultAvailabilityRawValue = try decodeOrFallbackIfPresent(String.self, with: .inheritDefaultAvailability)
if let inheritDefaultAvailabilityRawValue {
self.inheritDefaultAvailability = InheritDefaultAvailabilityOptions(rawValue: inheritDefaultAvailabilityRawValue)
}
}

init(
Expand All @@ -245,7 +269,8 @@ extension DocumentationBundle {
defaultCodeListingLanguage: String? = nil,
defaultModuleKind: String? = nil,
defaultAvailability: DefaultAvailability? = nil,
featureFlags: BundleFeatureFlags? = nil
featureFlags: BundleFeatureFlags? = nil,
inheritDefaultAvailability: InheritDefaultAvailabilityOptions? = nil
) {
self.displayName = displayName
self.identifier = identifier
Expand All @@ -254,6 +279,7 @@ extension DocumentationBundle {
self.defaultModuleKind = defaultModuleKind
self.defaultAvailability = defaultAvailability
self.featureFlags = featureFlags
self.inheritDefaultAvailability = inheritDefaultAvailability
}
}
}
Expand All @@ -272,14 +298,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.
/// - inheritDefaultAvailability: Option to configure default availability inheritance 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] = [],
inheritDefaultAvailability: DocumentationBundle.InheritDefaultAvailabilityOptions? = nil
) {
// Iterate over all possible coding keys with a switch
// to build up the dictionary of fallback options.
Expand All @@ -304,6 +332,8 @@ extension BundleDiscoveryOptions {
value = fallbackDefaultModuleKind
case .featureFlags:
value = nil
case .inheritDefaultAvailability:
value = inheritDefaultAvailability
}

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 applyDefaultAvailabilityVersionToSymbols = bundle.info.inheritDefaultAvailability
guard availability.introducedVersion != nil || applyDefaultAvailabilityVersionToSymbols == .platformOnly else {
return nil
}
guard let name = availability.domain.map({ PlatformName(operatingSystemName: $0.rawValue) }),
Expand Down
Loading