From 610bddcd1d8dbff6b5d2025c0f73b81de83c41b5 Mon Sep 17 00:00:00 2001 From: Sofia Rodriguez Date: Wed, 11 Sep 2024 13:08:35 +0100 Subject: [PATCH 1/5] Make default availability inheritance behaviour customizable. Added a new info.plist key `CDInheritDefaultAvailability` that allows customization on DocC logic for using default availability as the availability information for symbols. `CDInheritDefaultAvailability` allows two values: - `platformAndVersion` (deafault): This value adds both the platform and the semantic version as a fallback value for symbols that don't define explicit availability information for the given platform. - `platformOnly`: This value only adds the platform name (removes the version) to the symbols that don't define an explicit availability annotation for the given platform. rdar://132980711 --- .../Symbol Graph/SymbolGraphLoader.swift | 36 +++- .../Workspace/DefaultAvailability.swift | 4 +- .../Workspace/DocumentationBundle+Info.swift | 36 +++- .../Rendering/RenderNodeTranslator.swift | 6 +- .../Rendering/DefaultAvailabilityTests.swift | 202 ++++++++++++++++++ 5 files changed, 269 insertions(+), 15 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift index 6dc01370ab..d55d5b5aa1 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift @@ -59,6 +59,26 @@ 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 + 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 + let applyDefaultAvailabilityVersionToSymbols = bundle.info.inheritDefaultAvailability, + applyDefaultAvailabilityVersionToSymbols == .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 } @@ -79,8 +99,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 @@ -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 = defaultAvailabilities(bundle.info.defaultAvailability?.modules[unifiedGraph.moduleName]) { let (unavailablePlatforms, availablePlatforms) = defaultAvailabilities.categorize(where: { $0.versionInformation == .unavailable }) defaultUnavailablePlatforms = unavailablePlatforms.map(\.platformName) defaultAvailableInformation = availablePlatforms @@ -279,11 +300,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 @@ -404,9 +425,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 != nil) ? SymbolGraph.SemanticVersion(string: introducedVersion!) : nil let domain = SymbolGraph.Symbol.Availability.Domain(rawValue: defaultAvailability.platformName.rawValue) self.init(domain: domain, introducedVersion: platformVersion, diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift b/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift index af56bd238b..9e944b56d4 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift @@ -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". @@ -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 } diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift index d68a209a07..0173268e38 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift @@ -11,6 +11,16 @@ import Foundation extension DocumentationBundle { + + /// Options to define the inherit default availability behaviour. + public enum InheritDefaultAvailabilityOptions: String, Encodable { + /// 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. @@ -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 = [.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? + enum CodingKeys: String, CodingKey, CaseIterable { case displayName = "CFBundleDisplayName" case identifier = "CFBundleIdentifier" @@ -47,6 +61,7 @@ extension DocumentationBundle { case defaultAvailability = "CDAppleDefaultAvailability" case defaultModuleKind = "CDDefaultModuleKind" case featureFlags = "CDExperimentalFeatureFlags" + case inheritDefaultAvailability = "CDInheritDefaultAvailability" var argumentName: String? { switch self { @@ -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 } @@ -91,13 +108,15 @@ 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 @@ -105,6 +124,7 @@ extension DocumentationBundle { 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 @@ -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( @@ -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 @@ -254,6 +279,7 @@ extension DocumentationBundle { self.defaultModuleKind = defaultModuleKind self.defaultAvailability = defaultAvailability self.featureFlags = featureFlags + self.inheritDefaultAvailability = inheritDefaultAvailability } } } @@ -272,6 +298,7 @@ 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, @@ -279,7 +306,8 @@ extension BundleDiscoveryOptions { 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. @@ -304,6 +332,8 @@ extension BundleDiscoveryOptions { value = fallbackDefaultModuleKind case .featureFlags: value = nil + case .inheritDefaultAvailability: + value = inheritDefaultAvailability } guard let unwrappedValue = value else { diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift index 9c19000123..6fb0bf02c5 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift @@ -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) }), diff --git a/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift b/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift index 6662c6a555..dac5566d40 100644 --- a/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift @@ -577,6 +577,208 @@ class DefaultAvailabilityTests: XCTestCase { module.filter({ $0.platformName.displayName == "iPadOS" }).first?.versionInformation, .available(version: "10.0") ) + } + + func testInheritDefaultAvailabilityOptions() throws { + func makeInfoPlist( + inheritDefaultAvailability: String, + defaultAvailability: String? = nil + ) -> String { + let defaultAvailability = defaultAvailability ?? """ + + name + iOS + version + 8.0 + + """ + return """ + + + \(inheritDefaultAvailability) + CDAppleDefaultAvailability + + MyModule + + \(defaultAvailability) + + + + + """ + } + func setupContext( + inheritDefaultAvailability: String = "", + defaultAvailability: String? = nil + ) throws -> DocumentationContext { + // Create an empty bundle + let targetURL = try createTemporaryDirectory(named: "test.docc") + // Create symbol graph + let symbolGraphURL = targetURL.appendingPathComponent("MyModule.symbols.json") + try symbolGraphString.write(to: symbolGraphURL, atomically: true, encoding: .utf8) + // Create info plist + let infoPlistURL = targetURL.appendingPathComponent("Info.plist") + let infoPlist = makeInfoPlist(inheritDefaultAvailability: inheritDefaultAvailability, defaultAvailability: defaultAvailability) + try infoPlist.write(to: infoPlistURL, atomically: true, encoding: .utf8) + // Load the bundle & reference resolve symbol graph docs + let (_, _, context) = try loadBundle(from: targetURL) + return context + } + + let symbols = """ + { + "kind": { + "displayName" : "Instance Property", + "identifier" : "swift.property" + }, + "identifier": { + "precise": "c:@F@SymbolWithAvailability", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "Foo" + ], + "names": { + "title": "Foo", + }, + "accessLevel": "public", + "availability" : [ + { + "domain" : "ios", + "introduced" : { + "major" : 10, + "minor" : 0 + } + } + ] + }, + { + "kind": { + "displayName" : "Instance Property", + "identifier" : "swift.property" + }, + "identifier": { + "precise": "c:@F@SymbolWithoutAvailability", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "Foo" + ], + "names": { + "title": "Bar", + }, + "accessLevel": "public" + } + """ + let symbolGraphString = makeSymbolGraphString( + moduleName: "MyModule", + symbols: symbols, + platform: """ + "operatingSystem" : { + "minimumVersion" : { + "major" : 10, + "minor" : 0 + }, + "name" : "ios" + } + """ + ) + + // Don't use default availability version for symbols. + var context = try setupContext(inheritDefaultAvailability: """ + CDInheritDefaultAvailability + platformOnly + """) + + // Verify we add the version number into the symbols that have availability annotation. + guard let availability = (context.documentationCache["c:@F@SymbolWithAvailability"]?.semantic as? Symbol)?.availability?.availability else { + XCTFail("Did not find availability for symbol 'c:@F@SymbolWithAvailability'") + return + } + XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "iOS" })) + XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iOS" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 10, minor: 0, patch: 0)) + // Verify we don't add the version number into the symbols that don't have availability annotation. + guard let availability = (context.documentationCache["c:@F@SymbolWithoutAvailability"]?.semantic as? Symbol)?.availability?.availability else { + XCTFail("Did not find availability for symbol 'c:@F@SymbolWithoutAvailability'") + return + } + XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "iOS" })) + XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iOS" })?.introducedVersion, nil) + + + // Add an extra default availability to test behaviour when mixin in source with default behaviour. + context = try setupContext( + inheritDefaultAvailability: """ + CDInheritDefaultAvailability + platformOnly + """, + defaultAvailability: """ + + name + iOS + version + 8.0 + + + name + watchOS + version + 5.0 + + """ + ) + + // Verify we add the version number into the symbols that have availability annotation. + guard let availability = (context.documentationCache["c:@F@SymbolWithAvailability"]?.semantic as? Symbol)?.availability?.availability else { + XCTFail("Did not find availability for symbol 'c:@F@SymbolWithAvailability'") + return + } + XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "iOS" })) + XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "watchOS" })) + XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iOS" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 10, minor: 0, patch: 0)) + XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "watchOS" })?.introducedVersion, nil) + + // Use default availability version for symbols. + + context = try setupContext(inheritDefaultAvailability: """ + CDInheritDefaultAvailability + platformAndVersion + """) + + // Verify we add the version number into the symbols that have availability annotation. + guard let availability = (context.documentationCache["c:@F@SymbolWithAvailability"]?.semantic as? Symbol)?.availability?.availability else { + XCTFail("Did not find availability for symbol 'c:@F@SymbolWithAvailability'") + return + } + XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "iOS" })) + XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iOS" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 10, minor: 0, patch: 0)) + // Verify we don't add the version number into the symbols that don't have availability annotation. + guard let availability = (context.documentationCache["c:@F@SymbolWithoutAvailability"]?.semantic as? Symbol)?.availability?.availability else { + XCTFail("Did not find availability for symbol 'c:@F@SymbolWithoutAvailability'") + return + } + XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "iOS" })) + XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iOS" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 8, minor: 0, patch: 0)) + + // Don't specify availability inherit behaviour. + + context = try setupContext() + + // Verify we add the version number into the symbols that have availability annotation. + guard let availability = (context.documentationCache["c:@F@SymbolWithAvailability"]?.semantic as? Symbol)?.availability?.availability else { + XCTFail("Did not find availability for symbol 'c:@F@SymbolWithAvailability'") + return + } + XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "iOS" })) + XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iOS" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 10, minor: 0, patch: 0)) + // Verify we don't add the version number into the symbols that don't have availability annotation. + guard let availability = (context.documentationCache["c:@F@SymbolWithoutAvailability"]?.semantic as? Symbol)?.availability?.availability else { + XCTFail("Did not find availability for symbol 'c:@F@SymbolWithoutAvailability'") + return + } + XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "iOS" })) + XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iOS" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 8, minor: 0, patch: 0)) } + } From 6055aa94f29b2e827aafa8598f531adbeccb25e7 Mon Sep 17 00:00:00 2001 From: Sofia Rodriguez Date: Wed, 11 Sep 2024 17:03:45 +0100 Subject: [PATCH 2/5] Fix minor styling issues. --- .../Infrastructure/Symbol Graph/SymbolGraphLoader.swift | 6 ++---- .../Infrastructure/Workspace/DocumentationBundle+Info.swift | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift index d55d5b5aa1..b987714bdf 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift @@ -64,9 +64,7 @@ struct SymbolGraphLoader { 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 - let applyDefaultAvailabilityVersionToSymbols = bundle.info.inheritDefaultAvailability, - applyDefaultAvailabilityVersionToSymbols == .platformOnly { + if bundle.info.inheritDefaultAvailability == .platformOnly { return defautAvailabilities.map { defaultAvailability in var defaultAvailability = defaultAvailability switch defaultAvailability.versionInformation { @@ -426,7 +424,7 @@ extension SymbolGraph.Symbol.Availability.AvailabilityItem { /// platform version that can be parsed as a `SemanticVersion`, returns `nil`. init?(_ defaultAvailability: DefaultAvailability.ModuleAvailability) { let introducedVersion = defaultAvailability.introducedVersion - let platformVersion = (introducedVersion != nil) ? SymbolGraph.SemanticVersion(string: introducedVersion!) : nil + 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, diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift index 0173268e38..87882076c3 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift @@ -13,7 +13,7 @@ import Foundation extension DocumentationBundle { /// Options to define the inherit default availability behaviour. - public enum InheritDefaultAvailabilityOptions: String, Encodable { + 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 @@ -108,7 +108,7 @@ 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. + /// - inheritDefaultAvailability: The option to enable or disable symbol availability inheritance from the module default availability. public init( displayName: String, identifier: String, From fbc3ead1a0800d974078770d90bf1a547825c01e Mon Sep 17 00:00:00 2001 From: Sofia Rodriguez Date: Mon, 16 Sep 2024 16:39:31 +0100 Subject: [PATCH 3/5] Implement `DefaultAvailabilityOptions`. `DefaultAvailabilityOptions` is a new info plist key that contains a dictionary of options to customise the logic of the default availbaility. The only option for now is `InheritVersionNumber` that lets you add the version into the symbols availability that don't define explicit availability annotation. The default is true. --- .../Symbol Graph/SymbolGraphLoader.swift | 2 +- .../DefaultAvailailabilityOptions.swift | 69 +++++++++++++++++++ .../Workspace/DocumentationBundle+Info.swift | 51 ++++++++------ .../Rendering/RenderNodeTranslator.swift | 4 +- .../Rendering/DefaultAvailabilityTests.swift | 21 ++++-- 5 files changed, 118 insertions(+), 29 deletions(-) create mode 100644 Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailailabilityOptions.swift diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift index b987714bdf..a3981a1dac 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift @@ -64,7 +64,7 @@ struct SymbolGraphLoader { 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 { + if !bundle.info.defaultAvailabilityOptions.options.contains(.inheritVersionNumber) { return defautAvailabilities.map { defaultAvailability in var defaultAvailability = defaultAvailability switch defaultAvailability.versionInformation { diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailailabilityOptions.swift b/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailailabilityOptions.swift new file mode 100644 index 0000000000..bba999fb06 --- /dev/null +++ b/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailailabilityOptions.swift @@ -0,0 +1,69 @@ +/* + 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 + +extension DocumentationBundle.Info { + + /// 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. + /// + /// ``` + /// CDDefaultAvailabilityOptions + /// + /// OptionName + /// + /// + /// ``` + 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(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) + } + + } + + public init() { + self.options = .inheritVersionNumber + } + + } +} + diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift index 87882076c3..b4a001e204 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift @@ -49,9 +49,9 @@ extension DocumentationBundle { /// The keys that must be present in an Info.plist file in order for doc compilation to proceed. static let requiredKeys: Set = [.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? + /// The default availability behaviour options. + public var defaultAvailabilityOptions = DefaultAvailabilityOptions() + enum CodingKeys: String, CodingKey, CaseIterable { case displayName = "CFBundleDisplayName" @@ -61,7 +61,7 @@ extension DocumentationBundle { case defaultAvailability = "CDAppleDefaultAvailability" case defaultModuleKind = "CDDefaultModuleKind" case featureFlags = "CDExperimentalFeatureFlags" - case inheritDefaultAvailability = "CDInheritDefaultAvailability" + case defaultAvailabilityOptions = "CDDefaultAvailabilityOptions" var argumentName: String? { switch self { @@ -75,9 +75,7 @@ extension DocumentationBundle { return "--default-code-listing-language" case .defaultModuleKind: return "--fallback-default-module-kind" - case .inheritDefaultAvailability: - return nil - case .defaultAvailability, .featureFlags: + case .defaultAvailability, .defaultAvailabilityOptions, .featureFlags: return nil } } @@ -108,7 +106,7 @@ 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. + /// - defaultAvailabilityOptions: The options to enable or disable symbol availability logic from the module default availability. public init( displayName: String, identifier: String, @@ -116,7 +114,7 @@ extension DocumentationBundle { defaultCodeListingLanguage: String?, defaultAvailability: DefaultAvailability?, defaultModuleKind: String?, - inheritDefaultAvailability: InheritDefaultAvailabilityOptions? + defaultAvailabilityOptions: DefaultAvailabilityOptions ) { self.displayName = displayName self.identifier = identifier @@ -124,7 +122,7 @@ extension DocumentationBundle { self.defaultCodeListingLanguage = defaultCodeListingLanguage self.defaultAvailability = defaultAvailability self.defaultModuleKind = defaultModuleKind - self.inheritDefaultAvailability = inheritDefaultAvailability + self.defaultAvailabilityOptions = defaultAvailabilityOptions } /// Creates documentation bundle information from the given Info.plist data, falling back to the values @@ -256,10 +254,7 @@ 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) - } + self.defaultAvailabilityOptions = try decodeOrFallbackIfPresent(DefaultAvailabilityOptions.self, with: .defaultAvailabilityOptions) ?? DefaultAvailabilityOptions() } init( @@ -270,7 +265,8 @@ extension DocumentationBundle { defaultModuleKind: String? = nil, defaultAvailability: DefaultAvailability? = nil, featureFlags: BundleFeatureFlags? = nil, - inheritDefaultAvailability: InheritDefaultAvailabilityOptions? = nil + inheritDefaultAvailability: InheritDefaultAvailabilityOptions? = nil, + defaultAvailabilityOptions: DefaultAvailabilityOptions = DefaultAvailabilityOptions() ) { self.displayName = displayName self.identifier = identifier @@ -279,7 +275,22 @@ extension DocumentationBundle { self.defaultModuleKind = defaultModuleKind self.defaultAvailability = defaultAvailability self.featureFlags = featureFlags - self.inheritDefaultAvailability = inheritDefaultAvailability + self.defaultAvailabilityOptions = defaultAvailabilityOptions + } + + public func encode(to encoder: any Encoder) throws { + var container: KeyedEncodingContainer = 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.encode(self.defaultAvailabilityOptions, forKey: DocumentationBundle.Info.CodingKeys.defaultAvailabilityOptions) + } } } } @@ -298,7 +309,7 @@ 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. + /// - defaultAvailabilityOptions: Options to configure default availability behaviour. public init( fallbackDisplayName: String? = nil, fallbackIdentifier: String? = nil, @@ -307,7 +318,7 @@ extension BundleDiscoveryOptions { fallbackDefaultModuleKind: String? = nil, fallbackDefaultAvailability: DefaultAvailability? = nil, additionalSymbolGraphFiles: [URL] = [], - inheritDefaultAvailability: DocumentationBundle.InheritDefaultAvailabilityOptions? = nil + defaultAvailabilityOptions: DocumentationBundle.Info.DefaultAvailabilityOptions? = nil ) { // Iterate over all possible coding keys with a switch // to build up the dictionary of fallback options. @@ -332,8 +343,8 @@ extension BundleDiscoveryOptions { value = fallbackDefaultModuleKind case .featureFlags: value = nil - case .inheritDefaultAvailability: - value = inheritDefaultAvailability + case .defaultAvailabilityOptions: + value = defaultAvailabilityOptions } guard let unwrappedValue = value else { diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift index 6fb0bf02c5..ce2c002a11 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift @@ -1243,8 +1243,8 @@ public struct RenderNodeTranslator: SemanticVisitor { .compactMap { availability -> AvailabilityRenderItem? in // 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 { + let applyDefaultAvailabilityVersionToSymbols = !bundle.info.defaultAvailabilityOptions.options.contains(.inheritVersionNumber) + guard availability.introducedVersion != nil || applyDefaultAvailabilityVersionToSymbols else { return nil } guard let name = availability.domain.map({ PlatformName(operatingSystemName: $0.rawValue) }), diff --git a/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift b/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift index dac5566d40..6bb7d27c3a 100644 --- a/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift @@ -687,8 +687,11 @@ class DefaultAvailabilityTests: XCTestCase { // Don't use default availability version for symbols. var context = try setupContext(inheritDefaultAvailability: """ - CDInheritDefaultAvailability - platformOnly + CDDefaultAvailabilityOptions + + InheritVersionNumber + + """) // Verify we add the version number into the symbols that have availability annotation. @@ -710,8 +713,11 @@ class DefaultAvailabilityTests: XCTestCase { // Add an extra default availability to test behaviour when mixin in source with default behaviour. context = try setupContext( inheritDefaultAvailability: """ - CDInheritDefaultAvailability - platformOnly + CDDefaultAvailabilityOptions + + InheritVersionNumber + + """, defaultAvailability: """ @@ -742,8 +748,11 @@ class DefaultAvailabilityTests: XCTestCase { // Use default availability version for symbols. context = try setupContext(inheritDefaultAvailability: """ - CDInheritDefaultAvailability - platformAndVersion + CDDefaultAvailabilityOptions + + InheritVersionNumber + + """) // Verify we add the version number into the symbols that have availability annotation. From 4ba25c1f644e5dbbce1de30b608e5c6098f755d7 Mon Sep 17 00:00:00 2001 From: Sofia Rodriguez Date: Wed, 18 Sep 2024 16:22:40 +0100 Subject: [PATCH 4/5] Modified default availability info during decoding based on the default availability options. --- .../Symbol Graph/SymbolGraphLoader.swift | 36 ++----- .../DefaultAvailailabilityOptions.swift | 100 ++++++++++-------- .../Workspace/DocumentationBundle+Info.swift | 36 +++++-- .../Rendering/RenderNodeTranslator.swift | 4 +- 4 files changed, 94 insertions(+), 82 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift index a3981a1dac..0b86192e4a 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift @@ -59,24 +59,6 @@ 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 - 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.defaultAvailabilityOptions.options.contains(.inheritVersionNumber) { - 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 } @@ -97,9 +79,8 @@ 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, defaultAvailabilities: defaultAvailabilities) + self.addDefaultAvailability(to: &symbolGraph, moduleName: moduleName) // main symbol graphs are ambiguous var usesExtensionSymbolFormat: Bool? = nil @@ -172,7 +153,7 @@ struct SymbolGraphLoader { var defaultUnavailablePlatforms = [PlatformName]() var defaultAvailableInformation = [DefaultAvailability.ModuleAvailability]() - if let defaultAvailabilities = defaultAvailabilities(bundle.info.defaultAvailability?.modules[unifiedGraph.moduleName]) { + if let defaultAvailabilities = bundle.info.defaultAvailability?.modules[unifiedGraph.moduleName] { let (unavailablePlatforms, availablePlatforms) = defaultAvailabilities.categorize(where: { $0.versionInformation == .unavailable }) defaultUnavailablePlatforms = unavailablePlatforms.map(\.platformName) defaultAvailableInformation = availablePlatforms @@ -298,11 +279,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, defaultAvailabilities: [DefaultAvailability.ModuleAvailability]?) { + 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 = defaultAvailabilities, - let platformName = symbolGraph.module.platform.name.map(PlatformName.init) { + if let defaultAvailabilities = bundle.info.defaultAvailability?.modules[moduleName], + let platformName = symbolGraph.module.platform.name.map(PlatformName.init) { // Prepare a default availability versions lookup for this module. let defaultAvailabilityVersionByPlatform = defaultAvailabilities @@ -420,11 +401,14 @@ 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) { 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) self.init(domain: domain, introducedVersion: platformVersion, diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailailabilityOptions.swift b/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailailabilityOptions.swift index bba999fb06..781d829430 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailailabilityOptions.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailailabilityOptions.swift @@ -10,60 +10,66 @@ import Foundation -extension DocumentationBundle.Info { +/// 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. +/// +/// ``` +/// CDDefaultAvailabilityOptions +/// +/// OptionName +/// +/// +/// ``` +public struct DefaultAvailabilityOptions: Codable, Equatable { - /// 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. - /// - /// ``` - /// CDDefaultAvailabilityOptions - /// - /// OptionName - /// - /// - /// ``` - 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 { + /// 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) - } + let rawValue: Int - /// String representation of the default availability options. - private enum CodingKeys: String, CodingKey { - case inheritVersionNumber = "InheritVersionNumber" - } + /// 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 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) - } - + } + + 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) } - - public init() { - self.options = .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 } - } } diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift index b4a001e204..7a9083ade6 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift @@ -50,7 +50,7 @@ extension DocumentationBundle { static let requiredKeys: Set = [.displayName, .identifier] /// The default availability behaviour options. - public var defaultAvailabilityOptions = DefaultAvailabilityOptions() + public var defaultAvailabilityOptions: DefaultAvailabilityOptions? enum CodingKeys: String, CodingKey, CaseIterable { @@ -114,7 +114,7 @@ extension DocumentationBundle { defaultCodeListingLanguage: String?, defaultAvailability: DefaultAvailability?, defaultModuleKind: String?, - defaultAvailabilityOptions: DefaultAvailabilityOptions + defaultAvailabilityOptions: DefaultAvailabilityOptions? ) { self.displayName = displayName self.identifier = identifier @@ -252,9 +252,31 @@ extension DocumentationBundle { self.defaultCodeListingLanguage = try decodeOrFallbackIfPresent(String.self, with: .defaultCodeListingLanguage) self.defaultModuleKind = try decodeOrFallbackIfPresent(String.self, with: .defaultModuleKind) - self.defaultAvailability = try decodeOrFallbackIfPresent(DefaultAvailability.self, with: .defaultAvailability) + self.defaultAvailabilityOptions = try decodeOrFallbackIfPresent(DefaultAvailabilityOptions.self, with: .defaultAvailabilityOptions) + var defaultAvailability = try decodeOrFallbackIfPresent(DefaultAvailability.self, with: .defaultAvailability) + + // Apply default availability options mutations. + if let defaultAvailabilityOptions { + // Remove the availability version if `inheritVersionNumber` is not part + // of the default availability options. + if !defaultAvailabilityOptions.shouldApplyOption(.inheritVersionNumber) { + for (key, var moduleAvailability) in defaultAvailability?.modules ?? [:] { + moduleAvailability = moduleAvailability.map { moduleAvailability in + var moduleAvailability = moduleAvailability + moduleAvailability.versionInformation = { + switch moduleAvailability.versionInformation { + case .available(_): return .available(version: nil) + default: return moduleAvailability.versionInformation + } + }() + return moduleAvailability + } + defaultAvailability?.modules[key] = moduleAvailability + } + } + } + self.defaultAvailability = defaultAvailability self.featureFlags = try decodeOrFallbackIfPresent(BundleFeatureFlags.self, with: .featureFlags) - self.defaultAvailabilityOptions = try decodeOrFallbackIfPresent(DefaultAvailabilityOptions.self, with: .defaultAvailabilityOptions) ?? DefaultAvailabilityOptions() } init( @@ -266,7 +288,7 @@ extension DocumentationBundle { defaultAvailability: DefaultAvailability? = nil, featureFlags: BundleFeatureFlags? = nil, inheritDefaultAvailability: InheritDefaultAvailabilityOptions? = nil, - defaultAvailabilityOptions: DefaultAvailabilityOptions = DefaultAvailabilityOptions() + defaultAvailabilityOptions: DefaultAvailabilityOptions? = nil ) { self.displayName = displayName self.identifier = identifier @@ -289,7 +311,7 @@ extension DocumentationBundle { 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.encode(self.defaultAvailabilityOptions, forKey: DocumentationBundle.Info.CodingKeys.defaultAvailabilityOptions) + try container.encodeIfPresent(self.defaultAvailabilityOptions, forKey: DocumentationBundle.Info.CodingKeys.defaultAvailabilityOptions) } } } @@ -318,7 +340,7 @@ extension BundleDiscoveryOptions { fallbackDefaultModuleKind: String? = nil, fallbackDefaultAvailability: DefaultAvailability? = nil, additionalSymbolGraphFiles: [URL] = [], - defaultAvailabilityOptions: DocumentationBundle.Info.DefaultAvailabilityOptions? = nil + defaultAvailabilityOptions: DefaultAvailabilityOptions? = nil ) { // Iterate over all possible coding keys with a switch // to build up the dictionary of fallback options. diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift index ce2c002a11..3ddc28bb6c 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift @@ -1243,8 +1243,8 @@ public struct RenderNodeTranslator: SemanticVisitor { .compactMap { availability -> AvailabilityRenderItem? in // Filter items with insufficient availability data unless the default availability behaviour // allows availability withound version information. - let applyDefaultAvailabilityVersionToSymbols = !bundle.info.defaultAvailabilityOptions.options.contains(.inheritVersionNumber) - guard availability.introducedVersion != nil || applyDefaultAvailabilityVersionToSymbols else { + 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) }), From 3150da7f05cd120f58d66d850dd36a6660a68a35 Mon Sep 17 00:00:00 2001 From: Sofia Rodriguez Date: Thu, 19 Sep 2024 00:05:38 +0100 Subject: [PATCH 5/5] Ude default availability options only for symbol availability. --- .../Symbol Graph/SymbolGraphLoader.swift | 25 ++++++++++- .../Workspace/DocumentationBundle+Info.swift | 25 +---------- .../Rendering/DefaultAvailabilityTests.swift | 45 +++++++++++++------ 3 files changed, 56 insertions(+), 39 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift index 0b86192e4a..a1f75e4ab6 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift @@ -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. /// @@ -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 @@ -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. diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift index 7a9083ade6..7fd105f601 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift @@ -253,29 +253,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) - var defaultAvailability = try decodeOrFallbackIfPresent(DefaultAvailability.self, with: .defaultAvailability) - - // Apply default availability options mutations. - if let defaultAvailabilityOptions { - // Remove the availability version if `inheritVersionNumber` is not part - // of the default availability options. - if !defaultAvailabilityOptions.shouldApplyOption(.inheritVersionNumber) { - for (key, var moduleAvailability) in defaultAvailability?.modules ?? [:] { - moduleAvailability = moduleAvailability.map { moduleAvailability in - var moduleAvailability = moduleAvailability - moduleAvailability.versionInformation = { - switch moduleAvailability.versionInformation { - case .available(_): return .available(version: nil) - default: return moduleAvailability.versionInformation - } - }() - return moduleAvailability - } - defaultAvailability?.modules[key] = moduleAvailability - } - } - } - self.defaultAvailability = defaultAvailability + self.defaultAvailability = try decodeOrFallbackIfPresent(DefaultAvailability.self, with: .defaultAvailability) + self.defaultAvailability = try decodeOrFallbackIfPresent(DefaultAvailability.self, with: .defaultAvailability) self.featureFlags = try decodeOrFallbackIfPresent(BundleFeatureFlags.self, with: .featureFlags) } diff --git a/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift b/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift index 6bb7d27c3a..04248fa220 100644 --- a/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift @@ -610,7 +610,7 @@ class DefaultAvailabilityTests: XCTestCase { func setupContext( inheritDefaultAvailability: String = "", defaultAvailability: String? = nil - ) throws -> DocumentationContext { + ) throws -> (DocumentationBundle, DocumentationContext) { // Create an empty bundle let targetURL = try createTemporaryDirectory(named: "test.docc") // Create symbol graph @@ -621,8 +621,8 @@ class DefaultAvailabilityTests: XCTestCase { let infoPlist = makeInfoPlist(inheritDefaultAvailability: inheritDefaultAvailability, defaultAvailability: defaultAvailability) try infoPlist.write(to: infoPlistURL, atomically: true, encoding: .utf8) // Load the bundle & reference resolve symbol graph docs - let (_, _, context) = try loadBundle(from: targetURL) - return context + let (_, bundle, context) = try loadBundle(from: targetURL) + return (bundle, context) } let symbols = """ @@ -686,13 +686,23 @@ class DefaultAvailabilityTests: XCTestCase { // Don't use default availability version for symbols. - var context = try setupContext(inheritDefaultAvailability: """ - CDDefaultAvailabilityOptions - - InheritVersionNumber - - - """) + var (bundle, context) = try setupContext( + inheritDefaultAvailability: """ + CDDefaultAvailabilityOptions + + InheritVersionNumber + + + """, + defaultAvailability: """ + + name + iOS + version + 8.0 + + """ + ) // Verify we add the version number into the symbols that have availability annotation. guard let availability = (context.documentationCache["c:@F@SymbolWithAvailability"]?.semantic as? Symbol)?.availability?.availability else { @@ -708,10 +718,17 @@ class DefaultAvailabilityTests: XCTestCase { } XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "iOS" })) XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iOS" })?.introducedVersion, nil) - + // Verify we keep the module availability information. + let identifier = ResolvedTopicReference(bundleIdentifier: "test", path: "/documentation/MyModule", fragment: nil, sourceLanguage: .swift) + let node = try context.entity(with: identifier) + var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: identifier) + let renderNode = translator.visit(node.semantic) as! RenderNode + XCTAssertEqual(renderNode.metadata.platforms?.count, 3) + XCTAssertEqual(renderNode.metadata.platforms?.first?.name, "iOS") + XCTAssertEqual(renderNode.metadata.platforms?.first?.introduced, "8.0") // Add an extra default availability to test behaviour when mixin in source with default behaviour. - context = try setupContext( + (_, context) = try setupContext( inheritDefaultAvailability: """ CDDefaultAvailabilityOptions @@ -747,7 +764,7 @@ class DefaultAvailabilityTests: XCTestCase { // Use default availability version for symbols. - context = try setupContext(inheritDefaultAvailability: """ + (_, context) = try setupContext(inheritDefaultAvailability: """ CDDefaultAvailabilityOptions InheritVersionNumber @@ -772,7 +789,7 @@ class DefaultAvailabilityTests: XCTestCase { // Don't specify availability inherit behaviour. - context = try setupContext() + (_, context) = try setupContext() // Verify we add the version number into the symbols that have availability annotation. guard let availability = (context.documentationCache["c:@F@SymbolWithAvailability"]?.semantic as? Symbol)?.availability?.availability else {