From ec28dd5b59f14f96f2f82625cd756b798d04f820 Mon Sep 17 00:00:00 2001 From: Sofia Rodriguez Date: Thu, 24 Oct 2024 13:53:04 +0100 Subject: [PATCH 1/3] Refactor symbol availability logic. This refactor was initiated by the need of stop using the module operating system name as an availability item. This change required a big refactoring of the logic, and now the symbol graph availability loading logic is like the following: 1. Every symbol graph is loaded and the symbols get only the availability information that's marked in the SDK. 2. Once all the SGFs are oaded, whe iterate over the symbols and we add the fallbkac and default availability if applies. --- .../Symbol Graph/SymbolGraphLoader.swift | 277 ++++----- .../Workspace/DefaultAvailability.swift | 3 + .../Workspace/DocumentationBundle+Info.swift | 2 +- .../SwiftDocC/Semantics/Symbol/Symbol.swift | 8 +- .../FilesAndFolders.swift | 31 +- .../SymbolGraphCreation.swift | 12 + .../SymbolGraph/SymbolGraphLoaderTests.swift | 149 ++++- .../Rendering/DefaultAvailabilityTests.swift | 526 ++++++++++++------ 8 files changed, 645 insertions(+), 363 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift index 4d1165d868..7e5b55e19d 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift @@ -22,6 +22,7 @@ struct SymbolGraphLoader { private var dataProvider: DocumentationContextDataProvider private var bundle: DocumentationBundle private var symbolGraphTransformer: ((inout SymbolGraph) -> ())? = nil + private var symbolsByPlatformRegisteredPerModule: [String: [String: [SymbolGraph.Symbol.Identifier]]] = [:] /// Creates a new loader, initialized with the given bundle. /// - Parameters: @@ -58,7 +59,6 @@ struct SymbolGraphLoader { var loadError: Error? let bundle = self.bundle let dataProvider = self.dataProvider - let loadGraphAtURL: (URL) -> Void = { symbolGraphURL in // Bail out in case a symbol graph has already errored guard loadError == nil else { return } @@ -79,8 +79,8 @@ struct SymbolGraphLoader { symbolGraphTransformer?(&symbolGraph) let (moduleName, isMainSymbolGraph) = Self.moduleNameFor(symbolGraph, at: symbolGraphURL) - // If the bundle provides availability defaults add symbol availability data. - self.addDefaultAvailability(to: &symbolGraph, moduleName: moduleName) + + // main symbol graphs are ambiguous var usesExtensionSymbolFormat: Bool? = nil @@ -97,6 +97,13 @@ struct SymbolGraphLoader { // Store the decoded graph in `loadedGraphs` loadingLock.sync { + // Track the operating system platforms found in the symbol graphs of this module. + if let modulePlatform = symbolGraph.module.platform.name { + for symbol in symbolGraph.symbols.values { + symbolsByPlatformRegisteredPerModule[moduleName, default: [:]][modulePlatform, default: []].append(symbol.identifier) + } + } + // self.addDefaultAvailability(to: &symbolGraph, moduleName: moduleName) loadedGraphs[symbolGraphURL] = (usesExtensionSymbolFormat, symbolGraph) } } catch { @@ -158,16 +165,10 @@ struct SymbolGraphLoader { defaultUnavailablePlatforms = unavailablePlatforms.map(\.platformName) defaultAvailableInformation = availablePlatforms } - - let platformsFoundInSymbolGraphs: [PlatformName] = unifiedGraph.moduleData.compactMap { - guard let platformName = $0.value.platform.name else { return nil } - return PlatformName(operatingSystemName: platformName) - } - addMissingAvailability( unifiedGraph: &unifiedGraph, unconditionallyUnavailablePlatformNames: defaultUnavailablePlatforms, - registeredPlatforms: platformsFoundInSymbolGraphs, + symbolsByPlatformName: symbolsByPlatformRegisteredPerModule[unifiedGraph.moduleName] ?? [:], defaultAvailabilities: defaultAvailableInformation ) } @@ -201,129 +202,130 @@ struct SymbolGraphLoader { return (symbolGraph, isMainSymbolGraph) } + + + /** + Fills lacking availability information with fallback logic and default availability, if available. + + This method adds to every symbol the fallback availability items. + After this, it adds the default availability information if the symbol is available in the given platform. - /// Adds the missing fallback and default availability information to the unified symbol graph - /// in case it didn't exists in the loaded symbol graphs. + - parameter unifiedGraph: The generated unified graph. + - parameter unconditionallyUnavailablePlatformNames: Platforms to not add as synthesized availability items. + - parameter symbolsByPlatformName: Symbols found in symbolgraph grouped by operating system platform name. + - parameter defaultAvailabilities: The module default availabilities defined in the Info.plist. + */ private func addMissingAvailability( unifiedGraph: inout UnifiedSymbolGraph, unconditionallyUnavailablePlatformNames: [PlatformName], - registeredPlatforms: [PlatformName], + symbolsByPlatformName: [String: [SymbolGraph.Symbol.Identifier]], defaultAvailabilities: [DefaultAvailability.ModuleAvailability] ) { - // The fallback platforms that are missing from the unified graph correspond to - // the fallback platforms that have not been registered yet, - // are not marked as unavailable, - // and the corresponding inheritance platform has a SGF (has been registered). - let missingFallbackPlatforms = DefaultAvailability.fallbackPlatforms.filter { - !registeredPlatforms.contains($0.key) && - !unconditionallyUnavailablePlatformNames.contains($0.key) && - registeredPlatforms.contains($0.value) - } - // Platforms that are defined in the Info.plist that had no corresponding SGF - // and are not being added as fallback of another platform. - let missingAvailabilities = defaultAvailabilities.filter { - !missingFallbackPlatforms.keys.contains($0.platformName) && - !registeredPlatforms.contains($0.platformName) + // The fallback platforms that are not marked as unavailable in the default availability. + let fallbackPlatforms = DefaultAvailability.fallbackPlatforms.filter { + !unconditionallyUnavailablePlatformNames.contains($0.key) } - unifiedGraph.symbols.values.forEach { symbol in + unifiedGraph.symbols.forEach { (symbolID, symbol) in + // The platforms the symbol is available at grouped by interface language. + var platformsAvailableByLanguage: [String: Set] = [:] for (selector, _) in symbol.mixins { - if var symbolAvailability = (symbol.mixins[selector]?["availability"] as? SymbolGraph.Symbol.Availability) { - guard !symbolAvailability.availability.isEmpty else { continue } - // For platforms with a fallback option (e.g., Catalyst and iOS), apply the explicit availability annotation of the fallback platform when it is not explicitly available on the primary platform. - DefaultAvailability.fallbackPlatforms.forEach { (fallbackPlatform, inheritedPlatform) in - guard - var inheritedAvailability = symbolAvailability.availability.first(where: { + for item in symbol.availability[selector] ?? [] where item.introducedVersion != nil { + platformsAvailableByLanguage[selector.interfaceLanguage, default: []].insert(item.domain?.rawValue) + } + } + // Add fallback availability. + for (selector, mixins) in symbol.mixins { + + // Platforms available for the given symbol in the given language variant. + var platformsAvailable = platformsAvailableByLanguage[selector.interfaceLanguage] ?? [] + + // The symbol availability for the given selector. + var symbolAvailability = mixins.getValueIfPresent(for: SymbolGraph.Symbol.Availability.self)?.availability ?? [] + + // Add availability of platforms with an inherited platform (e.g iOS and iPadOS). + if !symbolAvailability.isEmpty { + fallbackPlatforms.forEach { (fallbackPlatform, inheritedPlatform) in + // The availability item the fallbak platform fallbacks from. + guard var inheritedAvailability = symbolAvailability.first(where: { $0.matches(inheritedPlatform) - }), - let fallbackAvailabilityIntroducedVersion = symbolAvailability.availability.first(where: { - $0.matches(fallbackPlatform) - })?.introducedVersion, - let defaultAvailabilityIntroducedVersion = defaultAvailabilities.first(where: { $0.platformName == fallbackPlatform })?.introducedVersion - else { return } - // Ensure that the availability version is not overwritten if the symbol has an explicit availability annotation for that platform. - if SymbolGraph.SemanticVersion(string: defaultAvailabilityIntroducedVersion) == fallbackAvailabilityIntroducedVersion { - inheritedAvailability.domain = SymbolGraph.Symbol.Availability.Domain(rawValue: fallbackPlatform.rawValue) - symbolAvailability.availability.removeAll(where: { - $0.matches(fallbackPlatform) - }) - symbolAvailability.availability.append(inheritedAvailability) + }) else { + return } - } - // Add fallback availability. - for (fallbackPlatform, inheritedPlatform) in missingFallbackPlatforms { - if !symbolAvailability.contains(fallbackPlatform) { - for var fallbackAvailability in symbolAvailability.availability { - // Add the platform fallback to the availability mixin the platform is inheriting from. - // The added availability copies the entire availability information, - // including deprecated and obsolete versions. - if fallbackAvailability.matches(inheritedPlatform) { - fallbackAvailability.domain = SymbolGraph.Symbol.Availability.Domain(rawValue: fallbackPlatform.rawValue) - symbolAvailability.availability.append(fallbackAvailability) + // Check that the symbol does not have an explicit availability annotation for the fallback platform already. + if !platformsAvailable.contains(fallbackPlatform.rawValue) { + // Check that the symbol does not have some availability information for the fallback platform. + // If it does adds the introduced version from the inherited availability item. + if let availabilityForFallbackPlatformIdx = symbolAvailability.firstIndex(where: { + $0.domain?.rawValue == fallbackPlatform.rawValue + }) { + if symbolAvailability[availabilityForFallbackPlatformIdx].isUnconditionallyUnavailable { + return } + symbolAvailability[availabilityForFallbackPlatformIdx].introducedVersion = inheritedAvailability.introducedVersion + return + } + // The symbols does not contains any information for the fallback platform + inheritedAvailability.domain = SymbolGraph.Symbol.Availability.Domain(rawValue: fallbackPlatform.rawValue) + inheritedAvailability.deprecatedVersion = inheritedAvailability.deprecatedVersion + symbolAvailability.append(inheritedAvailability) + if inheritedAvailability.introducedVersion != nil { + platformsAvailable.insert(fallbackPlatform.rawValue) } } } - // Add the missing default platform availability. - missingAvailabilities.forEach { missingAvailability in - if !symbolAvailability.contains(missingAvailability.platformName) { - guard let defaultAvailability = AvailabilityItem(missingAvailability) else { return } - symbolAvailability.availability.append(defaultAvailability) - } - } - symbol.mixins[selector]![SymbolGraph.Symbol.Availability.mixinKey] = symbolAvailability } - } - } - } - - /// 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) { - 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) { - - // Prepare a default availability versions lookup for this module. - let defaultAvailabilityVersionByPlatform = defaultAvailabilities - .reduce(into: [PlatformName: SymbolGraph.SemanticVersion](), { result, defaultAvailability in - if let introducedVersion = defaultAvailability.introducedVersion, let version = SymbolGraph.SemanticVersion(string: introducedVersion) { - result[defaultAvailability.platformName] = version - } - }) - - // Map all symbols and add default availability for any missing platforms - let symbolsWithFilledIntroducedVersions = symbolGraph.symbols.mapValues { symbol -> SymbolGraph.Symbol in - var symbol = symbol - let defaultModuleVersion = defaultAvailabilityVersionByPlatform[platformName] - // The availability item for each symbol of the given module. - let modulePlatformAvailabilityItem = AvailabilityItem(domain: SymbolGraph.Symbol.Availability.Domain(rawValue: platformName.rawValue), introducedVersion: defaultModuleVersion, deprecatedVersion: nil, obsoletedVersion: nil, message: nil, renamed: nil, isUnconditionallyDeprecated: false, isUnconditionallyUnavailable: false, willEventuallyBeDeprecated: false) - // Check if the symbol has existing availabilities from source - if var availability = symbol.mixins[SymbolGraph.Symbol.Availability.mixinKey] as? SymbolGraph.Symbol.Availability { - - // Fill introduced versions when missing. - availability.availability = availability.availability.map { - $0.fillingMissingIntroducedVersion( - from: defaultAvailabilityVersionByPlatform, - fallbackPlatform: DefaultAvailability.fallbackPlatforms[platformName]?.rawValue - ) - } - // Add the module availability information to each of the symbols availability mixin. - if !availability.contains(platformName) { - availability.availability.append(modulePlatformAvailabilityItem) + + // Add the module default availability information. + defaultAvailabilities.forEach { defaultAvailability in + + // Check that if there was a symbolgraph for this platform, the symbol was present on it, + // if not it means that the symbol is not available for the default platform. + guard symbolsByPlatformName[defaultAvailability.platformName.rawValue]?.contains(where: { + symbolID == $0.precise + }) != false else { + return } - symbol.mixins[SymbolGraph.Symbol.Availability.mixinKey] = availability - } else { - // ObjC doesn't propagate symbol availability to their children properties, - // so only add the default availability to the Swift variant of the symbols. - if !(selector?.interfaceLanguage == InterfaceLanguage.objc.name.lowercased()) { - symbol.mixins[SymbolGraph.Symbol.Availability.mixinKey] = SymbolGraph.Symbol.Availability(availability: [modulePlatformAvailabilityItem]) + + // Check that the symbol does not has explicit availability for this platform already. + if !platformsAvailable.contains(defaultAvailability.platformName.rawValue) { + // If the missing availability corresponds to a fallback platform, and there's default availability for the platform that this one fallbacks from, don't add it. + if let fallbackPlatform = fallbackPlatforms.first(where: { $0.key == defaultAvailability.platformName }), platformsAvailable.contains(fallbackPlatform.value.rawValue) { + return + } + guard var defaultAvailabilityItem = AvailabilityItem(defaultAvailability) else { return } + + // Check if the symbol already has this availability item. + if let idx = symbolAvailability.firstIndex(where: { + $0.domain?.rawValue == defaultAvailability.platformName.rawValue + }) { + // If the symbol is marked as unavailable don't add the default availability. + if symbolAvailability[idx].isUnconditionallyUnavailable || (symbolAvailability[idx].obsoletedVersion != nil) { + return + } + defaultAvailabilityItem.deprecatedVersion = symbolAvailability[idx].deprecatedVersion + defaultAvailabilityItem.isUnconditionallyDeprecated = symbolAvailability[idx].isUnconditionallyDeprecated + symbolAvailability.remove(at: idx) + } + symbolAvailability.append(defaultAvailabilityItem) + + // If the default availability has fallback platforms, add them now. + for (fallbackPlatform, inheritedPlatform) in fallbackPlatforms { + // Check that the fallback platform has not been added already to the symbol, + // and that it does not has it's own default availability information. + if ( + inheritedPlatform == defaultAvailability.platformName && + !platformsAvailable.contains(fallbackPlatform.rawValue) && + !defaultAvailabilities.contains(where: {$0.platformName.rawValue == fallbackPlatform.rawValue}) + ) { + defaultAvailabilityItem.domain = SymbolGraph.Symbol.Availability.Domain(rawValue: fallbackPlatform.rawValue) + symbolAvailability.append(defaultAvailabilityItem) + } + } } } - return symbol + symbol.mixins[selector]![SymbolGraph.Symbol.Availability.mixinKey] = SymbolGraph.Symbol.Availability(availability: symbolAvailability) } - symbolGraph.symbols = symbolsWithFilledIntroducedVersions } } @@ -420,55 +422,6 @@ extension SymbolGraph.Symbol.Availability.AvailabilityItem { isUnconditionallyUnavailable: false, willEventuallyBeDeprecated: false) } - - /** - Fills lacking availability information with defaults, if available. - - If this item does not have an `introducedVersion`, attempt to fill it - in from the `defaults`. If the defaults do not have a version for - this item's domain/platform, also try the `fallbackPlatform`. - - - parameter defaults: Default module availabilities for each platform mentioned in a documentation bundle's `Info.plist` - - parameter fallbackPlatform: An optional fallback platform name if this item's domain isn't found in the `defaults`. - */ - func fillingMissingIntroducedVersion(from defaults: [PlatformName: SymbolGraph.SemanticVersion], - fallbackPlatform: String?) -> SymbolGraph.Symbol.Availability.AvailabilityItem { - // If this availability item doesn't have a domain, do nothing. - guard let domain = self.domain else { - return self - } - - var newValue = self - // To ensure the uniformity of platform availability names derived from SGFs, - // we replace the original domain value with a value from the platform's name - // since the platform name maps aliases to the canonical name. - let platformName = PlatformName(operatingSystemName: domain.rawValue) - newValue.domain = SymbolGraph.Symbol.Availability.Domain(rawValue: platformName.rawValue) - - // If a symbol is unconditionally unavailable for a given domain, - // don't add an introduced version here as it may cause it to - // incorrectly display availability information - guard !isUnconditionallyUnavailable else { - return newValue - } - - // If this had an explicit introduced version from source, don't replace it. - guard introducedVersion == nil else { - return newValue - } - - let fallbackPlatformName = fallbackPlatform.map(PlatformName.init(operatingSystemName:)) - - // Try to find a default version string for this availability - // item's platform (a.k.a. domain) - guard let platformVersion = defaults[platformName] ?? - fallbackPlatformName.flatMap({ defaults[$0] }) else { - return newValue - } - - newValue.introducedVersion = platformVersion - return newValue - } } private extension SymbolGraph.Symbol.Availability { diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift b/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift index 0bb06b29e8..5aa77809a0 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift @@ -131,6 +131,9 @@ public struct DefaultAvailability: Codable, Equatable { /// Fallback availability information for platforms we either don't emit SGFs for /// or have the same availability information as another platform. + /// + /// The key corresponds to the fallback platform and the value to the platform it's + /// fallbacking from. package static let fallbackPlatforms: [PlatformName: PlatformName] = [ .catalyst: .iOS, .iPadOS: .iOS, diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift index 35dd5ae686..ade84064c0 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift @@ -40,7 +40,7 @@ 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] - enum CodingKeys: String, CodingKey, CaseIterable { + package enum CodingKeys: String, CodingKey, CaseIterable { case displayName = "CFBundleDisplayName" case identifier = "CFBundleIdentifier" case defaultCodeListingLanguage = "CDDefaultCodeListingLanguage" diff --git a/Sources/SwiftDocC/Semantics/Symbol/Symbol.swift b/Sources/SwiftDocC/Semantics/Symbol/Symbol.swift index f2a06d59e1..ae554289e7 100644 --- a/Sources/SwiftDocC/Semantics/Symbol/Symbol.swift +++ b/Sources/SwiftDocC/Semantics/Symbol/Symbol.swift @@ -552,10 +552,12 @@ extension Symbol { func mergeAvailabilities(unifiedSymbol: UnifiedSymbolGraph.Symbol) { for (selector, mixins) in unifiedSymbol.mixins { let trait = DocumentationDataVariantsTrait(for: selector) - if let unifiedSymbolAvailability = mixins[SymbolGraph.Symbol.Availability.mixinKey] as? SymbolGraph.Symbol.Availability { + guard let availabilityVariantTrait = availabilityVariants[trait] else { + return + } + if let unifiedSymbolAvailability = mixins.getValueIfPresent(for: SymbolGraph.Symbol.Availability.self) { unifiedSymbolAvailability.availability.forEach { availabilityItem in - guard let availabilityVariantTrait = availabilityVariants[trait] else { return } - if (availabilityVariantTrait.availability.contains(where: { $0.domain?.rawValue == availabilityItem.domain?.rawValue })) { + guard availabilityVariantTrait.availability.firstIndex(where: { $0.domain?.rawValue == availabilityItem.domain?.rawValue }) == nil else { return } availabilityVariants[trait]?.availability.append(availabilityItem) diff --git a/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift b/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift index f1e7f4417b..e30b6be991 100644 --- a/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift +++ b/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift @@ -94,25 +94,37 @@ public struct InfoPlist: File, DataRepresentable { /// The information that the Into.plist file contains. public let content: Content - public init(displayName: String? = nil, identifier: String? = nil) { + public init(displayName: String? = nil, identifier: String? = nil, defaultAvailability: [String: [DefaultAvailability.ModuleAvailability]]? = nil) { self.content = Content( displayName: displayName, - identifier: identifier + identifier: identifier, + defaultAvailability: defaultAvailability ) } public struct Content: Codable, Equatable { public let displayName: String? public let identifier: String? + public let defaultAvailability: [String: [DefaultAvailability.ModuleAvailability]]? - fileprivate init(displayName: String?, identifier: String?) { + fileprivate init(displayName: String?, identifier: String?, defaultAvailability: [String: [DefaultAvailability.ModuleAvailability]]?) { self.displayName = displayName self.identifier = identifier + self.defaultAvailability = defaultAvailability } - - enum CodingKeys: String, CodingKey { - case displayName = "CFBundleDisplayName" - case identifier = "CFBundleIdentifier" + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: DocumentationBundle.Info.CodingKeys.self) + displayName = try container.decodeIfPresent(String.self, forKey: .displayName) + identifier = try container.decodeIfPresent(String.self, forKey: .identifier) + defaultAvailability = try container.decodeIfPresent([String : [DefaultAvailability.ModuleAvailability]].self, forKey: .defaultAvailability) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: DocumentationBundle.Info.CodingKeys.self) + try container.encodeIfPresent(displayName, forKey: .displayName) + try container.encodeIfPresent(identifier, forKey: .identifier) + try container.encodeIfPresent(defaultAvailability, forKey: .defaultAvailability) } } @@ -120,10 +132,7 @@ public struct InfoPlist: File, DataRepresentable { let encoder = PropertyListEncoder() encoder.outputFormat = .xml - return try encoder.encode([ - Content.CodingKeys.displayName.rawValue: content.displayName, - Content.CodingKeys.identifier.rawValue: content.identifier, - ]) + return try encoder.encode(content) } } diff --git a/Sources/SwiftDocCTestUtilities/SymbolGraphCreation.swift b/Sources/SwiftDocCTestUtilities/SymbolGraphCreation.swift index b6bd0f405a..7613f5f31d 100644 --- a/Sources/SwiftDocCTestUtilities/SymbolGraphCreation.swift +++ b/Sources/SwiftDocCTestUtilities/SymbolGraphCreation.swift @@ -86,6 +86,7 @@ extension XCTestCase { accessLevel: SymbolGraph.Symbol.AccessControl = .init(rawValue: "public"), // Defined internally in SwiftDocC location: (position: SymbolGraph.LineList.SourceRange.Position, url: URL)? = (defaultSymbolPosition, defaultSymbolURL), signature: SymbolGraph.Symbol.FunctionSignature? = nil, + availability: [SymbolGraph.Symbol.Availability.AvailabilityItem]? = nil, otherMixins: [any Mixin] = [] ) -> SymbolGraph.Symbol { precondition(!pathComponents.isEmpty, "Need at least one path component to name the symbol") @@ -97,6 +98,9 @@ extension XCTestCase { if let signature { mixins.append(signature) } + if let availability { + mixins.append(SymbolGraph.Symbol.Availability(availability: availability)) + } return SymbolGraph.Symbol( identifier: SymbolGraph.Symbol.Identifier(precise: id, interfaceLanguage: language.id), @@ -115,6 +119,14 @@ extension XCTestCase { ) } + package func makeAvailabilityItem( + domainName: String, + introduced: SymbolGraph.SemanticVersion?, + deprecated: SymbolGraph.SemanticVersion? = nil + ) -> SymbolGraph.Symbol.Availability.AvailabilityItem { + return SymbolGraph.Symbol.Availability.AvailabilityItem(domain: .init(rawValue: domainName), introducedVersion: introduced, deprecatedVersion: deprecated, obsoletedVersion: nil, message: nil, renamed: nil, isUnconditionallyDeprecated: false, isUnconditionallyUnavailable: false, willEventuallyBeDeprecated: false) + } + package func makeSymbolNames(name: String) -> SymbolGraph.Symbol.Names { SymbolGraph.Symbol.Names( title: name, diff --git a/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphLoaderTests.swift b/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphLoaderTests.swift index 1102f81803..8e80fe620c 100644 --- a/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphLoaderTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphLoaderTests.swift @@ -161,7 +161,7 @@ class SymbolGraphLoaderTests: XCTestCase { // Update one symbol's availability to use as a verification if we're loading iOS or Catalyst symbol graph catalystSymbolGraph.symbols["s:5MyKit0A5ClassC"]!.mixins[SymbolGraph.Symbol.Availability.mixinKey]! = SymbolGraph.Symbol.Availability(availability: [ .init(domain: SymbolGraph.Symbol.Availability.Domain(rawValue: "Mac Catalyst"), introducedVersion: .init(major: 1, minor: 0, patch: 0), deprecatedVersion: nil, obsoletedVersion: nil, message: nil, renamed: nil, isUnconditionallyDeprecated: false, isUnconditionallyUnavailable: false, willEventuallyBeDeprecated: false), - .init(domain: SymbolGraph.Symbol.Availability.Domain(rawValue: "iOS"), introducedVersion: .init(major: 7, minor: 0, patch: 0), deprecatedVersion: nil, obsoletedVersion: nil, message: nil, renamed: nil, isUnconditionallyDeprecated: false, isUnconditionallyUnavailable: false, willEventuallyBeDeprecated: false), + .init(domain: SymbolGraph.Symbol.Availability.Domain(rawValue: "iOS"), introducedVersion: nil, deprecatedVersion: nil, obsoletedVersion: nil, message: nil, renamed: nil, isUnconditionallyDeprecated: false, isUnconditionallyUnavailable: false, willEventuallyBeDeprecated: false), ]) let catalystSymbolGraphURL = bundleURL.appendingPathComponent(catalystSymbolGraphName) @@ -671,13 +671,14 @@ class SymbolGraphLoaderTests: XCTestCase { } """, platform: """ + "environment" : "macabi", "operatingSystem" : { "minimumVersion" : { "major" : 6, "minor" : 5, "patch" : 0 }, - "name" : "macCatalyst" + "name" : "ios", } """ ) @@ -775,12 +776,13 @@ class SymbolGraphLoaderTests: XCTestCase { } """, platform: """ + "environment" : "macabi", "operatingSystem" : { "minimumVersion" : { "major" : 6, "minor" : 5 }, - "name" : "macCatalyst" + "name" : "ios" } """ ) @@ -931,6 +933,7 @@ class SymbolGraphLoaderTests: XCTestCase { XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "iPadOS" })) XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iOS" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 8, minor: 0, patch: 0)) XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "macCatalyst" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 7, minor: 0, patch: 0)) + XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iPadOS" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 6, minor: 0, patch: 0)) } func testUnconditionallyunavailablePlatforms() throws { @@ -995,7 +998,7 @@ class SymbolGraphLoaderTests: XCTestCase { }, "accessLevel": "public", "availability" : [{ - "domain" : "maccatalyst", + "domain" : "macCatalyst", "introduced" : { "major" : 12, "minor" : 0 @@ -1401,7 +1404,7 @@ class SymbolGraphLoaderTests: XCTestCase { "accessLevel" : "public", "availability" : [ { - "domain" : "maccatalyst", + "domain" : "macCatalyst", "introduced" : { "major" : 15, "minor" : 2, @@ -1453,7 +1456,7 @@ class SymbolGraphLoaderTests: XCTestCase { // 'Mac Catalyst' (info.plist) and 'maccatalyst' (SGF). XCTAssertTrue(availability.count == 2) XCTAssertTrue(availability.filter({ $0.domain?.rawValue == "macCatalyst" }).count == 1) - XCTAssertTrue(availability.filter({ $0.domain?.rawValue == "maccatalyst" }).count == 0) + XCTAssertTrue(availability.filter({ $0.domain?.rawValue == "Mac Catalyst" }).count == 0) } func testFallbackOverrideDefaultAvailability() throws { @@ -1518,17 +1521,7 @@ class SymbolGraphLoaderTests: XCTestCase { "names": { "title": "Foo", }, - "accessLevel": "public", - "availability" : [ - { - "domain" : "iOS", - "introduced" : { - "major" : 12, - "minor" : 0, - "patch" : 0 - } - } - ] + "accessLevel": "public" } """, platform: """ @@ -1595,8 +1588,7 @@ class SymbolGraphLoaderTests: XCTestCase { "names": { "title": "Foo", }, - "accessLevel": "public", - "availability" : [] + "accessLevel": "public" } """, platform: """ @@ -1652,6 +1644,125 @@ class SymbolGraphLoaderTests: XCTestCase { XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "macCatalyst" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 1, minor: 0, patch: 0)) } + func testNotAvailableSymbol() throws { + // Symbol from SG + let symbolGraphStringiOS = makeSymbolGraphString( + moduleName: "MyModule", + symbols: """ + { + "kind": { + "displayName" : "Instance Property", + "identifier" : "swift.property" + }, + "identifier": { + "precise": "c:@F@A", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "Foo" + ], + "names": { + "title": "Foo", + }, + "accessLevel": "public", + "availability" : [ + { + "domain" : "macOS", + "introduced" : { + "major" : 12, + "minor" : 0, + "patch" : 0 + } + } + ] + } + """, + platform: """ + "operatingSystem" : { + "minimumVersion" : { + "major" : 12, + "minor" : 0, + "patch" : 0 + }, + "name" : "macosx" + } + """ + ) + let symbolGraphStringMacOS = makeSymbolGraphString( + moduleName: "MyModule", + symbols: """ + { + "kind": { + "displayName" : "Instance Property", + "identifier" : "swift.property" + }, + "identifier": { + "precise": "c:@F@B", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "Foo" + ], + "names": { + "title": "Bar", + }, + "accessLevel": "public", + "availability" : [ + { + "domain" : "iOS", + "introduced" : { + "major" : 12, + "minor" : 0, + "patch" : 0 + } + } + ] + } + """, + platform: """ + "operatingSystem" : { + "minimumVersion" : { + "major" : 6, + "minor" : 5, + "patch" : 0 + }, + "name" : "ios" + } + """ + ) + let infoPlist = """ + + + CDAppleDefaultAvailability + + MyModule + + + name + iOS + version + 1.0 + + + + + + """ + // Create an empty bundle + let targetURL = try createTemporaryDirectory(named: "test.docc") + // Store files + try symbolGraphStringiOS.write(to: targetURL.appendingPathComponent("MyModule-ios.symbols.json"), atomically: true, encoding: .utf8) + try symbolGraphStringMacOS.write(to: targetURL.appendingPathComponent("MyModule-macos.symbols.json"), atomically: true, encoding: .utf8) + try infoPlist.write(to: targetURL.appendingPathComponent("Info.plist"), atomically: true, encoding: .utf8) + // Load the bundle & reference resolve symbol graph docs + let (_, _, context) = try loadBundle(from: targetURL) + let availability = try XCTUnwrap((context.documentationCache["c:@F@A"]?.semantic as? Symbol)?.availability?.availability) + // Verify we don't fallback to iOS for 'Foo' even if there's default availability. + XCTAssertNil(availability.first(where: { $0.domain?.rawValue == "iOS" })) + } + + + // MARK: - Helpers private func makeSymbolGraphLoader( diff --git a/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift b/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift index 1531398355..d0c8e8c708 100644 --- a/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift @@ -86,7 +86,7 @@ class DefaultAvailabilityTests: XCTestCase { var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) let renderNode = translator.visit(node.semantic) as! RenderNode - XCTAssertEqual(renderNode.metadata.platforms?.map({ "\($0.name ?? "") \($0.introduced ?? "")" }).sorted(), ["Mac Catalyst ", "iOS ", "iPadOS ", "macOS 10.15.1"]) + XCTAssertEqual(renderNode.metadata.platforms?.map({ "\($0.name ?? "") \($0.introduced ?? "")" }).sorted(), expectedDefaultAvailability) } // Test if the default availability is NOT used for symbols with explicit availability @@ -545,177 +545,369 @@ class DefaultAvailabilityTests: XCTestCase { ) } - func testInheritDefaultAvailabilityOptions() throws { - func makeInfoPlist( - defaultAvailability: String - ) -> String { - return """ - - - CDAppleDefaultAvailability - - MyModule - - \(defaultAvailability) - - - - - """ - } - func setupContext( - defaultAvailability: String - ) throws -> (DocumentationBundle, 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(defaultAvailability: defaultAvailability) - try infoPlist.write(to: infoPlistURL, atomically: true, encoding: .utf8) - // Load the bundle & reference resolve symbol graph docs - let (_, bundle, context) = try loadBundle(from: targetURL) - return (bundle, 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" - } - """ + private func symbolAvailability( + defaultAvailability: [DefaultAvailability.ModuleAvailability] = [], + symbolGraphOperatingSystemPlatformName: String, + symbols: [SymbolGraph.Symbol] + ) throws -> [SymbolGraph.Symbol.Availability.AvailabilityItem] { + let catalog = Folder( + name: "unit-test.docc", + content: [ + InfoPlist(defaultAvailability: [ + "ModuleName": defaultAvailability + ]), + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: SymbolGraph.OperatingSystem(name: symbolGraphOperatingSystemPlatformName), environment: nil), + symbols: symbols, + relationships: [] + )), + ] ) - - // Don't use default availability version. - - var (bundle, context) = try setupContext( - defaultAvailability: """ - - name - iOS - - """ + let (_, context) = try loadBundle(catalog: catalog) + let reference = try XCTUnwrap(context.soleRootModuleReference).appendingPath("SymbolName") + let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) + let availability = try XCTUnwrap(symbol.availability?.availability) + return availability + } + + func testSymbolGraphPlatformNameWithDifferentNameInDefaultAvailability() throws { + let availability = try symbolAvailability( + defaultAvailability: [.init(platformName: .init(operatingSystemName: "Platform Name"), platformVersion: "1.2.3")], + symbolGraphOperatingSystemPlatformName: "platform_name", + symbols: [makeSymbol(id: "platform-1-symbol", kind: .class, pathComponents: ["SymbolName"])] ) - // 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) - // Verify we remove the version from the module availability information. - var identifier = ResolvedTopicReference(bundleIdentifier: "test", path: "/documentation/MyModule", fragment: nil, sourceLanguage: .swift) - var node = try context.entity(with: identifier) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: identifier) - var renderNode = translator.visit(node.semantic) as! RenderNode - XCTAssertEqual(renderNode.metadata.platforms?.count, 1) - XCTAssertEqual(renderNode.metadata.platforms?.first?.name, "iOS") - XCTAssertEqual(renderNode.metadata.platforms?.first?.introduced, nil) - - // Add an extra default availability to test behaviour when mixin in source with default behaviour. - (bundle, context) = try setupContext(defaultAvailability: """ - - name - iOS - version - 8.0 - - - name - watchOS - - """ + XCTAssertEqual(availability.map { "\($0.domain?.rawValue ?? "") \($0.introducedVersion?.description ?? "")" }.sorted(), [ + // This is from the Info.plist value + "Platform Name 1.2.3", + // This shouldn't be displayed + // "platform_name " + ]) + } + + func testSymbolAvailabilityPlatformNameWithDifferentNameInDefaultAvailability() throws { + let availability = try symbolAvailability( + defaultAvailability: [.init(platformName: .init(operatingSystemName: "Platform Name"), platformVersion: "1.2.3")], + symbolGraphOperatingSystemPlatformName: "platform_name", + symbols: [makeSymbol(id: "platform-1-symbol", kind: .class, pathComponents: ["SymbolName"], availability: [makeAvailabilityItem(domainName: "platform_name", introduced: SymbolGraph.SemanticVersion(string: "1.2.3"))])] ) - // 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) - - guard let availability = (context.documentationCache["c:@F@SymbolWithoutAvailability"]?.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: 8, minor: 0, patch: 0)) - XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "watchOS" })?.introducedVersion, nil) + XCTAssertEqual(availability.map { "\($0.domain?.rawValue ?? "") \($0.introducedVersion?.description ?? "")" }.sorted(), [ + // This is from the Info.plist value + "Platform Name 1.2.3", + // This is from the symbol + "platform_name 1.2.3", + // This shouldn't be displayed + // "platform_name " + ]) + } + + func testSymbolGraphPlatformNameWithDifferentNameInDefaultAvailabilityWithoutVersion() throws { + let availability = try symbolAvailability( + defaultAvailability: [.init(platformName: .init(operatingSystemName: "Platform Name"), platformVersion: nil)], + symbolGraphOperatingSystemPlatformName: "platform_name", + symbols: [makeSymbol(id: "platform-1-symbol", kind: .class, pathComponents: ["SymbolName"])] + ) + XCTAssertEqual(availability.map { "\($0.domain?.rawValue ?? "") \($0.introducedVersion?.description ?? "")" }.sorted(), [ + // This is from the Info.plist value + "Platform Name ", + // This shouldn't be displayed + // "platform_name " + ]) + } + + func testSymbolAvailabilityPlatformNameWithDifferentNameInDefaultAvailabilityWithoutVersion() throws { + let availability = try symbolAvailability( + defaultAvailability: [.init(platformName: .init(operatingSystemName: "Platform Name"), platformVersion: nil)], + symbolGraphOperatingSystemPlatformName: "platform_name", + symbols: [makeSymbol(id: "platform-1-symbol", kind: .class, pathComponents: ["SymbolName"], availability: [makeAvailabilityItem(domainName: "platform_name", introduced: nil)])] + ) + XCTAssertEqual(availability.map { "\($0.domain?.rawValue ?? "") \($0.introducedVersion?.description ?? "")" }.sorted(), [ + // This is from the Info.plist value + "Platform Name ", + // This from the symbol + "platform_name " + ]) + } + + func testSymbolAvailabilityDoesNotDisplayOperatingSystemPlatformFromSymbolGraph() throws { + let availability = try symbolAvailability( + defaultAvailability: [.init(platformName: .init(operatingSystemName: "macOS"), platformVersion: "1.2.3")], + symbolGraphOperatingSystemPlatformName: "ios", + symbols: [makeSymbol(id: "platform-1-symbol", kind: .class, pathComponents: ["SymbolName"])] + ) + XCTAssertEqual(availability.map(\.testDescription).sorted(), [ + // Shouldn't display these + // "iOS ", + // "iPadOS ", + // "macCatalyst " + // This is from the Info.plist value + "macOS 1.2.3" + ]) + } + + func testSymbolAvailabilityDoesNotDisplayOperatingSystemPlatformFromSymbolGraphButDoesDisplayItsOwn() throws { + let availability = try symbolAvailability( + defaultAvailability: [.init(platformName: .init(operatingSystemName: "macOS"), platformVersion: "1.2.3")], + symbolGraphOperatingSystemPlatformName: "ios", + symbols: [makeSymbol(id: "platform-1-symbol", kind: .class, pathComponents: ["SymbolName"], availability: [makeAvailabilityItem(domainName: "iOS", introduced: SymbolGraph.SemanticVersion(string: "1.2.3"))])] + ) + XCTAssertEqual(availability.map(\.testDescription).sorted(), [ + // This is from the symbol + "iOS 1.2.3", + "iPadOS 1.2.3", + "macCatalyst 1.2.3", + // This is from the Info.plist value + "macOS 1.2.3", + ]) + } + + func testSymbolAvailabilityDoesNotDisplayOperatingSystemPlatformFromSymbolGraphButDoesDisplayItsOwnWithoutVersion() throws { + let availability = try symbolAvailability( + defaultAvailability: [.init(platformName: .init(operatingSystemName: "macOS"), platformVersion: "1.2.3")], + symbolGraphOperatingSystemPlatformName: "ios", + symbols: [makeSymbol(id: "platform-1-symbol", kind: .class, pathComponents: ["SymbolName"], availability: [makeAvailabilityItem(domainName: "iOS", introduced: nil)])] + ) + XCTAssertEqual(availability.map(\.testDescription).sorted(), [ + // This is from the symbol + "iOS ", + "iPadOS ", + "macCatalyst ", + // This is from the Info.plist value + "macOS 1.2.3", + ]) + } + + func testSymbolDoesNotDisplayOperatingSystemPlatformFromSymbolGraphButDoesDisplayItsOwnWithoutVersionWithputDefaultAvailability() throws { + let availability = try symbolAvailability( + symbolGraphOperatingSystemPlatformName: "ios", + symbols: [makeSymbol(id: "platform-1-symbol", kind: .class, pathComponents: ["SymbolName"], availability: [makeAvailabilityItem(domainName: "iOS", introduced: nil)])] + ) + XCTAssertEqual(availability.map(\.testDescription).sorted(), [ + // This is from the symbol + "iOS ", + "iPadOS ", + "macCatalyst " + ]) + } + + func testSymbolAvailabilityDoesNotDisplayKnownOperatingSystemPlatformFromSymbolGraph() throws { + let availability = try symbolAvailability( + symbolGraphOperatingSystemPlatformName: "ios", + symbols: [makeSymbol(id: "platform-1-symbol", kind: .class, pathComponents: ["SymbolName"])] + ) + XCTAssertEqual(availability.map(\.testDescription).sorted(), [ + // Shouldn't display these + // "iOS ", + // "iPadOS ", + // "macCatalyst " + ]) + } + + func testDoesNotDisplayUnknownOperatingSystemPlatformFromSymbolGraph() throws { + let availability = try symbolAvailability( + symbolGraphOperatingSystemPlatformName: "platform_name", + symbols: [makeSymbol(id: "platform-1-symbol", kind: .class, pathComponents: ["SymbolName"])] + ) + XCTAssertEqual(availability.map(\.testDescription).sorted(), [ + // Shouldn't display this + // "platform_name ", + ]) + } + + func testFallbackAvailabilityBehaviourForDefaultAvailabilityPlatformWithoutVersion() throws { + let catalog = Folder( + name: "unit-test.docc", + content: [ + InfoPlist(defaultAvailability: [ + "ModuleName": [ + .init(platformName: .iOS, platformVersion: nil) + ] + ]), + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: SymbolGraph.OperatingSystem(name: "ios"), environment: nil), + symbols: [ + makeSymbol(id: "ios-symbol", kind: .class, pathComponents: ["SymbolName"]) + ], + relationships: [] + )), + ] + ) + + let (_, context) = try loadBundle(catalog: catalog) + let reference = try XCTUnwrap(context.soleRootModuleReference).appendingPath("SymbolName") + let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) + let availability = try XCTUnwrap(symbol.availability?.availability) + + XCTAssertEqual(availability.map(\.testDescription).sorted(), [ + // These are from the Info.plist value (and platform fallbacks) + "iOS ", + "iPadOS ", + "macCatalyst ", + ]) + } + + func testFallbackAvailabilityWithVersionFromSpecificSymbol() throws { + let catalog = Folder( + name: "unit-test.docc", + content: [ + InfoPlist(defaultAvailability: [ + "ModuleName": [ + .init(platformName: .iOS, platformVersion: nil) + ] + ]), + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: SymbolGraph.OperatingSystem(name: "ios"), environment: nil), + symbols: [ + makeSymbol(id: "ios-symbol", kind: .class, pathComponents: ["SymbolName"], availability: [ + makeAvailabilityItem(domainName: "iOS", introduced: .init(major: 1, minor: 2, patch: 3)) + ]) + ], + relationships: [] + )), + ] + ) + + let (_, context) = try loadBundle(catalog: catalog) + let reference = try XCTUnwrap(context.soleRootModuleReference).appendingPath("SymbolName") + let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) + let availability = try XCTUnwrap(symbol.availability?.availability) + XCTAssertEqual(availability.map(\.testDescription).sorted(), [ + // These are from the platform fallbacks. + "iOS 1.2.3", + "iPadOS 1.2.3", + "macCatalyst 1.2.3", + ]) + } + + func testFallbackAvailabilityWithDifferentSymbolGraphPlatform() throws { + let catalog = Folder( + name: "unit-test.docc", + content: [ + InfoPlist(defaultAvailability: [ + "ModuleName": [ + .init(platformName: .iOS, platformVersion: nil) + ] + ]), + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: SymbolGraph.OperatingSystem(name: "macos"), environment: nil), + symbols: [ + makeSymbol(id: "mac-symbol", kind: .class, pathComponents: ["SymbolName"], otherMixins: [ + SymbolGraph.Symbol.Availability(availability: [ + makeAvailabilityItem(domainName: "macOS", introduced: .init(major: 1, minor: 2, patch: 3)) + ]) + ]) + ], + relationships: [] + )), + ] + ) + + let (_, context) = try loadBundle(catalog: catalog) + let reference = try XCTUnwrap(context.soleRootModuleReference).appendingPath("SymbolName") + let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) + let availability = try XCTUnwrap(symbol.availability?.availability) + + XCTAssertEqual(availability.map(\.testDescription).sorted(), [ + // These are from the Info.plist value (and platform fallbacks) + "iOS ", + "iPadOS ", + "macCatalyst ", + // This if from the specific symbol + "macOS 1.2.3", + ]) + } + + func testDoesNotDisplaySymbolsThatAreNotAvailableInAPlatform() throws { + let catalog = Folder( + name: "unit-test.docc", + content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: SymbolGraph.OperatingSystem(name: "macos"), environment: nil), + symbols: [ + makeSymbol(id: "mac-symbol", kind: .class, pathComponents: ["SymbolName"], otherMixins: [ + SymbolGraph.Symbol.Availability(availability: [ + makeAvailabilityItem(domainName: "macOS", introduced: .init(major: 1, minor: 2, patch: 3)) + ]) + ]) + ], + relationships: [] + )), + JSONFile(name: "OtherModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: SymbolGraph.OperatingSystem(name: "ios"), environment: nil), + symbols: [], + relationships: [] + )), + ] + ) + let (_, context) = try loadBundle(catalog: catalog) + let reference = try XCTUnwrap(context.soleRootModuleReference).appendingPath("SymbolName") + let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) + let availability = try XCTUnwrap(symbol.availability?.availability) + + XCTAssertEqual(availability.map(\.testDescription).sorted(), [ + "macOS 1.2.3", + // Shouldn't display these + // "iOS ", + // "iPadOS ", + // "macCatalyst " + ]) + } + + func testDoesNotDisplaySymbolsThatAreNotAvailableInAPlatformButDoesDisplayFromInfoPlist() throws { + let catalog = Folder( + name: "unit-test.docc", + content: [ + InfoPlist(defaultAvailability: [ + "ModuleName": [ + .init(platformName: .tvOS, platformVersion: "1.2.3") + ] + ]), + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: SymbolGraph.OperatingSystem(name: "macos"), environment: nil), + symbols: [ + makeSymbol(id: "mac-symbol", kind: .class, pathComponents: ["SymbolName"], otherMixins: [ + SymbolGraph.Symbol.Availability(availability: [ + makeAvailabilityItem(domainName: "macOS", introduced: .init(major: 1, minor: 2, patch: 3)) + ]) + ]) + ], + relationships: [] + )), + JSONFile(name: "OtherModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: SymbolGraph.OperatingSystem(name: "ios"), environment: nil), + symbols: [], + relationships: [] + )), + ] + ) + let (_, context) = try loadBundle(catalog: catalog) + let reference = try XCTUnwrap(context.soleRootModuleReference).appendingPath("SymbolName") + let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) + let availability = try XCTUnwrap(symbol.availability?.availability) - // Verify the module availability shows as expected. - identifier = ResolvedTopicReference(bundleIdentifier: "test", path: "/documentation/MyModule", fragment: nil, sourceLanguage: .swift) - node = try context.entity(with: identifier) - translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: identifier) - renderNode = translator.visit(node.semantic) as! RenderNode - XCTAssertEqual(renderNode.metadata.platforms?.count, 4) - var moduleAvailability = try XCTUnwrap(renderNode.metadata.platforms?.first(where: {$0.name == "iOS"})) - XCTAssertEqual(moduleAvailability.introduced, "8.0") - moduleAvailability = try XCTUnwrap(renderNode.metadata.platforms?.first(where: {$0.name == "watchOS"})) - XCTAssertEqual(moduleAvailability.introduced, nil) + XCTAssertEqual(availability.map(\.testDescription).sorted(), [ + "macOS 1.2.3", + "tvOS 1.2.3" + // Shouldn't display these + // "iOS ", + // "iPadOS ", + // "macCatalyst " + ]) + } + +} + +private extension SymbolGraph.Symbol.Availability.AvailabilityItem { + var testDescription: String { + "\(domain?.rawValue ?? "") \(introducedVersion?.description ?? "")" } } From e9152bba839e992f754333995b9379403558acfe Mon Sep 17 00:00:00 2001 From: Sofia Rodriguez Date: Mon, 11 Nov 2024 17:02:22 +0000 Subject: [PATCH 2/3] Refactor unit tests for testing symbol availability. --- .../Rendering/DefaultAvailabilityTests.swift | 92 ++----------------- .../Rendering/SymbolAvailabilityTests.swift | 78 ++++++++++++++-- 2 files changed, 78 insertions(+), 92 deletions(-) diff --git a/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift b/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift index 27323e3e9a..384c2048c0 100644 --- a/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift @@ -679,7 +679,7 @@ class DefaultAvailabilityTests: XCTestCase { ]) } - func testSymbolDoesNotDisplayOperatingSystemPlatformFromSymbolGraphButDoesDisplayItsOwnWithoutVersionWithputDefaultAvailability() throws { + func testSymbolDoesNotDisplayOperatingSystemPlatformFromSymbolGraphButDoesDisplayItsOwnWithoutVersionWithoutDefaultAvailability() throws { let availability = try symbolAvailability( symbolGraphOperatingSystemPlatformName: "ios", symbols: [makeSymbol(id: "platform-1-symbol", kind: .class, pathComponents: ["SymbolName"], availability: [makeAvailabilityItem(domainName: "iOS", introduced: nil)])] @@ -716,65 +716,13 @@ class DefaultAvailabilityTests: XCTestCase { ]) } - func testFallbackAvailabilityBehaviourForDefaultAvailabilityPlatformWithoutVersion() throws { - let catalog = Folder( - name: "unit-test.docc", - content: [ - InfoPlist(defaultAvailability: [ - "ModuleName": [ - .init(platformName: .iOS, platformVersion: nil) - ] - ]), - JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( - moduleName: "ModuleName", - platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: SymbolGraph.OperatingSystem(name: "ios"), environment: nil), - symbols: [ - makeSymbol(id: "ios-symbol", kind: .class, pathComponents: ["SymbolName"]) - ], - relationships: [] - )), - ] - ) - - let (_, context) = try loadBundle(catalog: catalog) - let reference = try XCTUnwrap(context.soleRootModuleReference).appendingPath("SymbolName") - let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) - let availability = try XCTUnwrap(symbol.availability?.availability) - - XCTAssertEqual(availability.map(\.testDescription).sorted(), [ - // These are from the Info.plist value (and platform fallbacks) - "iOS ", - "iPadOS ", - "macCatalyst ", - ]) - } - func testFallbackAvailabilityWithVersionFromSpecificSymbol() throws { - let catalog = Folder( - name: "unit-test.docc", - content: [ - InfoPlist(defaultAvailability: [ - "ModuleName": [ - .init(platformName: .iOS, platformVersion: nil) - ] - ]), - JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( - moduleName: "ModuleName", - platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: SymbolGraph.OperatingSystem(name: "ios"), environment: nil), - symbols: [ - makeSymbol(id: "ios-symbol", kind: .class, pathComponents: ["SymbolName"], availability: [ - makeAvailabilityItem(domainName: "iOS", introduced: .init(major: 1, minor: 2, patch: 3)) - ]) - ], - relationships: [] - )), - ] + let availability = try symbolAvailability( + defaultAvailability: [.init(platformName: .init(operatingSystemName: "iOS"), platformVersion: "1.2.3")], + symbolGraphOperatingSystemPlatformName: "ios", + symbols: [makeSymbol(id: "platform-1-symbol", kind: .class, pathComponents: ["SymbolName"], availability: [makeAvailabilityItem(domainName: "iOS", introduced: SymbolGraph.SemanticVersion(string: "1.2.3"))])] ) - let (_, context) = try loadBundle(catalog: catalog) - let reference = try XCTUnwrap(context.soleRootModuleReference).appendingPath("SymbolName") - let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) - let availability = try XCTUnwrap(symbol.availability?.availability) XCTAssertEqual(availability.map(\.testDescription).sorted(), [ // These are from the platform fallbacks. "iOS 1.2.3", @@ -784,34 +732,12 @@ class DefaultAvailabilityTests: XCTestCase { } func testFallbackAvailabilityWithDifferentSymbolGraphPlatform() throws { - let catalog = Folder( - name: "unit-test.docc", - content: [ - InfoPlist(defaultAvailability: [ - "ModuleName": [ - .init(platformName: .iOS, platformVersion: nil) - ] - ]), - JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( - moduleName: "ModuleName", - platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: SymbolGraph.OperatingSystem(name: "macos"), environment: nil), - symbols: [ - makeSymbol(id: "mac-symbol", kind: .class, pathComponents: ["SymbolName"], otherMixins: [ - SymbolGraph.Symbol.Availability(availability: [ - makeAvailabilityItem(domainName: "macOS", introduced: .init(major: 1, minor: 2, patch: 3)) - ]) - ]) - ], - relationships: [] - )), - ] + let availability = try symbolAvailability( + defaultAvailability: [.init(platformName: .init(operatingSystemName: "iOS"), platformVersion: nil)], + symbolGraphOperatingSystemPlatformName: "ios", + symbols: [makeSymbol(id: "platform-1-symbol", kind: .class, pathComponents: ["SymbolName"], availability: [makeAvailabilityItem(domainName: "macOS", introduced: SymbolGraph.SemanticVersion(string: "1.2.3"))])] ) - let (_, context) = try loadBundle(catalog: catalog) - let reference = try XCTUnwrap(context.soleRootModuleReference).appendingPath("SymbolName") - let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) - let availability = try XCTUnwrap(symbol.availability?.availability) - XCTAssertEqual(availability.map(\.testDescription).sorted(), [ // These are from the Info.plist value (and platform fallbacks) "iOS ", diff --git a/Tests/SwiftDocCTests/Rendering/SymbolAvailabilityTests.swift b/Tests/SwiftDocCTests/Rendering/SymbolAvailabilityTests.swift index 2ae7279726..29e7bdafae 100644 --- a/Tests/SwiftDocCTests/Rendering/SymbolAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Rendering/SymbolAvailabilityTests.swift @@ -16,10 +16,24 @@ import SwiftDocCTestUtilities class SymbolAvailabilityTests: XCTestCase { + private func symbolGraphJSONFile( + symbolGraphOperatingSystemPlatformName: String, + symbols: [SymbolGraph.Symbol] + ) -> JSONFile { + JSONFile( + name: "ModuleName-\(symbolGraphOperatingSystemPlatformName).symbols.json", + content: makeSymbolGraph( + moduleName: "ModuleName", + platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: SymbolGraph.OperatingSystem(name: symbolGraphOperatingSystemPlatformName), environment: nil), + symbols: symbols, + relationships: [] + ) + ) + } + private func symbolAvailability( defaultAvailability: [DefaultAvailability.ModuleAvailability] = [], - symbolGraphOperatingSystemPlatformName: String, - symbols: [SymbolGraph.Symbol], + symbolGraphs: [(operatingSystemPlatformName: String, symbols: [SymbolGraph.Symbol])], symbolName: String ) throws -> [SymbolGraph.Symbol.Availability.AvailabilityItem] { let catalog = Folder( @@ -28,13 +42,7 @@ class SymbolAvailabilityTests: XCTestCase { InfoPlist(defaultAvailability: [ "ModuleName": defaultAvailability ]), - JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( - moduleName: "ModuleName", - platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: SymbolGraph.OperatingSystem(name: symbolGraphOperatingSystemPlatformName), environment: nil), - symbols: symbols, - relationships: [] - )), - ] + ] + symbolGraphs.map({ symbolGraphJSONFile(symbolGraphOperatingSystemPlatformName: $0.operatingSystemPlatformName, symbols: $0.symbols) }) ) let (_, context) = try loadBundle(catalog: catalog) let reference = try XCTUnwrap(context.soleRootModuleReference).appendingPath(symbolName) @@ -112,4 +120,56 @@ class SymbolAvailabilityTests: XCTestCase { ]) } + func testNonExistingSymbolInOperatingSystemPlatform() throws { + let symbolGraphs: [(operatingSystemPlatformName: String, symbols: [SymbolGraph.Symbol])] = [( + operatingSystemPlatformName: "iOS", + symbols: [ + makeSymbol( + id: "platform-1-symbol", + kind: .class, + pathComponents: ["SymbolNameiOS"], + availability: [makeAvailabilityItem(domainName: "iOS", introduced: SymbolGraph.SemanticVersion(string: "1.2.3"))] + ) + ] + ), ( + operatingSystemPlatformName: "macOS", + symbols: [ + makeSymbol( + id: "platform-2-symbol", + kind: .class, + pathComponents: ["SymbolNamemacOS"], + availability: [makeAvailabilityItem(domainName: "macOS", introduced: SymbolGraph.SemanticVersion(string: "1.2.3"))] + ) + ] + )] + + // Test that if a symbol exists in a symbol graph, but it does not exists in another symbol graph, + // the symbol is only available in that single platform. + XCTAssertEqual( + try symbolAvailability(defaultAvailability: [], symbolGraphs: symbolGraphs, symbolName: "SymbolNameiOS").map(\.testDescription).sorted(), [ + "iOS 1.2.3 - ", + "iPadOS 1.2.3 - ", + "macCatalyst 1.2.3 - ", + // Shouldn't display this + // "macOS 1.2.3 - ", + // because the symbol does not exists in the macOS SGF. + ]) + + XCTAssertEqual( + try symbolAvailability(defaultAvailability: [], symbolGraphs: symbolGraphs, symbolName: "SymbolNamemacOS").map(\.testDescription).sorted(), [ + "macOS 1.2.3 - ", + // Shouldn't display these + // "iOS 1.2.3 - " + // "iPadOS 1.2.3 - " + // "macCatalyst 1.2.3 - " + // because the symbol does not exists in the iOS SGF. + ]) + } + +} + +private extension SymbolGraph.Symbol.Availability.AvailabilityItem { + var testDescription: String { + "\(self.domain?.rawValue ?? "") \(self.introducedVersion?.description ?? "") - \(self.deprecatedVersion?.description ?? "")" + } } From 8b2cb42e45c6fc2369f46ddc7ca2fae9094808c3 Mon Sep 17 00:00:00 2001 From: Sofia Rodriguez Date: Tue, 12 Nov 2024 15:47:33 +0000 Subject: [PATCH 3/3] Refactor fallback platforms data structure. Instead of making a `[(FallbackPlatform, InheritedPlaform)]. The new struture to hold this information is in the way of `[InheritedPlatfor:[FallbackPlatforms]]`. --- .../Symbol Graph/SymbolGraphLoader.swift | 67 ++++++++++--------- .../Workspace/DefaultAvailability.swift | 27 ++++---- .../Actions/Convert/ConvertAction.swift | 6 +- .../Model/SemaToRenderNodeTests.swift | 6 +- 4 files changed, 58 insertions(+), 48 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift index 11fecb1f3a..509c925f17 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift @@ -223,8 +223,11 @@ struct SymbolGraphLoader { defaultAvailabilities: [DefaultAvailability.ModuleAvailability] ) { // The fallback platforms that are not marked as unavailable in the default availability. - let fallbackPlatforms = DefaultAvailability.fallbackPlatforms.filter { - !unconditionallyUnavailablePlatformNames.contains($0.key) + var fallbackPlatforms = DefaultAvailability.fallbackPlatforms + fallbackPlatforms.forEach { (inheritedPlatform, platforms) in + fallbackPlatforms[inheritedPlatform] = platforms.filter({ + !unconditionallyUnavailablePlatformNames.contains($0) + }) } unifiedGraph.symbols.forEach { (symbolID, symbol) in @@ -246,32 +249,33 @@ struct SymbolGraphLoader { // Add availability of platforms with an inherited platform (e.g iOS and iPadOS). if !symbolAvailability.isEmpty { - fallbackPlatforms.forEach { (fallbackPlatform, inheritedPlatform) in + fallbackPlatforms.forEach { (inheritedPlatform, fallbackPlatforms) in // The availability item the fallbak platform fallbacks from. guard var inheritedAvailability = symbolAvailability.first(where: { $0.matches(inheritedPlatform) }) else { return } - // Check that the symbol does not have an explicit availability annotation for the fallback platform already. - if !platformsAvailable.contains(fallbackPlatform.rawValue) { - // Check that the symbol does not have some availability information for the fallback platform. - // If it does adds the introduced version from the inherited availability item. - if let availabilityForFallbackPlatformIdx = symbolAvailability.firstIndex(where: { - $0.domain?.rawValue == fallbackPlatform.rawValue - }) { - if symbolAvailability[availabilityForFallbackPlatformIdx].isUnconditionallyUnavailable { - return + for fallbackPlatform in fallbackPlatforms { + // Check that the symbol does not have an explicit availability annotation for the fallback platform already. + if !platformsAvailable.contains(fallbackPlatform.rawValue) { + // Check that the symbol does not have some availability information for the fallback platform. + // If it does adds the introduced version from the inherited availability item. + if let availabilityForFallbackPlatformIdx = symbolAvailability.firstIndex(where: { + $0.domain?.rawValue == fallbackPlatform.rawValue + }) { + if symbolAvailability[availabilityForFallbackPlatformIdx].isUnconditionallyUnavailable { + continue + } + symbolAvailability[availabilityForFallbackPlatformIdx].introducedVersion = inheritedAvailability.introducedVersion + continue + } + // The symbols does not contains any information for the fallback platform + inheritedAvailability.domain = SymbolGraph.Symbol.Availability.Domain(rawValue: fallbackPlatform.rawValue) + symbolAvailability.append(inheritedAvailability) + if inheritedAvailability.introducedVersion != nil { + platformsAvailable.insert(fallbackPlatform.rawValue) } - symbolAvailability[availabilityForFallbackPlatformIdx].introducedVersion = inheritedAvailability.introducedVersion - return - } - // The symbols does not contains any information for the fallback platform - inheritedAvailability.domain = SymbolGraph.Symbol.Availability.Domain(rawValue: fallbackPlatform.rawValue) - inheritedAvailability.deprecatedVersion = inheritedAvailability.deprecatedVersion - symbolAvailability.append(inheritedAvailability) - if inheritedAvailability.introducedVersion != nil { - platformsAvailable.insert(fallbackPlatform.rawValue) } } } @@ -291,7 +295,7 @@ struct SymbolGraphLoader { // Check that the symbol does not has explicit availability for this platform already. if !platformsAvailable.contains(defaultAvailability.platformName.rawValue) { // If the missing availability corresponds to a fallback platform, and there's default availability for the platform that this one fallbacks from, don't add it. - if let fallbackPlatform = fallbackPlatforms.first(where: { $0.key == defaultAvailability.platformName }), platformsAvailable.contains(fallbackPlatform.value.rawValue) { + if let fallbackPlatform = fallbackPlatforms.first(where: { $0.value.contains(defaultAvailability.platformName)}), platformsAvailable.contains(fallbackPlatform.key.rawValue) { return } guard var defaultAvailabilityItem = AvailabilityItem(defaultAvailability) else { return } @@ -311,16 +315,19 @@ struct SymbolGraphLoader { symbolAvailability.append(defaultAvailabilityItem) // If the default availability has fallback platforms, add them now. - for (fallbackPlatform, inheritedPlatform) in fallbackPlatforms { + for (inheritedPlatform, fallbackPlatforms) in fallbackPlatforms { // Check that the fallback platform has not been added already to the symbol, // and that it does not has it's own default availability information. - if ( - inheritedPlatform == defaultAvailability.platformName && - !platformsAvailable.contains(fallbackPlatform.rawValue) && - !defaultAvailabilities.contains(where: {$0.platformName.rawValue == fallbackPlatform.rawValue}) - ) { - defaultAvailabilityItem.domain = SymbolGraph.Symbol.Availability.Domain(rawValue: fallbackPlatform.rawValue) - symbolAvailability.append(defaultAvailabilityItem) + if inheritedPlatform == defaultAvailability.platformName { + for fallbackPlatform in fallbackPlatforms { + if ( + !platformsAvailable.contains(fallbackPlatform.rawValue) && + !defaultAvailabilities.contains(where: {$0.platformName.rawValue == fallbackPlatform.rawValue}) + ) { + defaultAvailabilityItem.domain = SymbolGraph.Symbol.Availability.Domain(rawValue: fallbackPlatform.rawValue) + symbolAvailability.append(defaultAvailabilityItem) + } + } } } } diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift b/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift index 5aa77809a0..1d5ecebeb3 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift @@ -134,10 +134,7 @@ public struct DefaultAvailability: Codable, Equatable { /// /// The key corresponds to the fallback platform and the value to the platform it's /// fallbacking from. - package static let fallbackPlatforms: [PlatformName: PlatformName] = [ - .catalyst: .iOS, - .iPadOS: .iOS, - ] + package static let fallbackPlatforms: [PlatformName: [PlatformName]] = [.iOS: [.catalyst, .iPadOS]] /// Creates a default availability module. /// - Parameter modules: A map of modules and the default platform availability for symbols in that module. @@ -145,17 +142,19 @@ public struct DefaultAvailability: Codable, Equatable { self.modules = modules.mapValues { platformAvailabilities -> [DefaultAvailability.ModuleAvailability] in // If a module doesn't contain default availability information for any of the fallback platforms, // infer it from the corresponding mapped value. - platformAvailabilities + DefaultAvailability.fallbackPlatforms.compactMap { (platform, fallbackPlatform) in - if !platformAvailabilities.contains(where: { $0.platformName == platform }), - let fallbackAvailability = platformAvailabilities.first(where: { $0.platformName == fallbackPlatform }), - let fallbackIntroducedVersion = fallbackAvailability.introducedVersion - { - return DefaultAvailability.ModuleAvailability( - platformName: platform, - platformVersion: fallbackIntroducedVersion - ) + platformAvailabilities + DefaultAvailability.fallbackPlatforms.flatMap { (fallbackPlatform, platform) in + platform.compactMap { pla in + if !platformAvailabilities.contains(where: { $0.platformName == pla }), + let fallbackAvailability = platformAvailabilities.first(where: { $0.platformName == fallbackPlatform }), + let fallbackIntroducedVersion = fallbackAvailability.introducedVersion + { + return DefaultAvailability.ModuleAvailability( + platformName: pla, + platformVersion: fallbackIntroducedVersion + ) + } + return nil } - return nil } } } diff --git a/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift b/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift index d20ea2a62a..745ee1a6b6 100644 --- a/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift +++ b/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift @@ -143,8 +143,10 @@ public struct ConvertAction: AsyncAction { // Inject current platform versions if provided if var currentPlatforms { // Add missing platforms if their fallback platform is present. - for (platform, fallbackPlatform) in DefaultAvailability.fallbackPlatforms where currentPlatforms[platform.displayName] == nil { - currentPlatforms[platform.displayName] = currentPlatforms[fallbackPlatform.displayName] + for (fallbackPlatform, platforms) in DefaultAvailability.fallbackPlatforms { + for platform in platforms where currentPlatforms[platform.displayName] == nil { + currentPlatforms[platform.displayName] = currentPlatforms[fallbackPlatform.displayName] + } } configuration.externalMetadata.currentPlatforms = currentPlatforms } diff --git a/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift b/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift index 8372a64656..86074ad3f7 100644 --- a/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift +++ b/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift @@ -1917,8 +1917,10 @@ Document var configuration = DocumentationContext.Configuration() // Add missing platforms if their fallback platform is present. var currentPlatforms = currentPlatforms ?? [:] - for (platform, fallbackPlatform) in DefaultAvailability.fallbackPlatforms where currentPlatforms[platform.displayName] == nil { - currentPlatforms[platform.displayName] = currentPlatforms[fallbackPlatform.displayName] + for (fallbackPlatform, platforms) in DefaultAvailability.fallbackPlatforms { + for platform in platforms where currentPlatforms[platform.displayName] == nil { + currentPlatforms[platform.displayName] = currentPlatforms[fallbackPlatform.displayName] + } } configuration.externalMetadata.currentPlatforms = currentPlatforms