Skip to content

Commit ec28dd5

Browse files
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.
1 parent 4316b76 commit ec28dd5

File tree

8 files changed

+645
-363
lines changed

8 files changed

+645
-363
lines changed

Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift

Lines changed: 115 additions & 162 deletions
Large diffs are not rendered by default.

Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ public struct DefaultAvailability: Codable, Equatable {
131131

132132
/// Fallback availability information for platforms we either don't emit SGFs for
133133
/// or have the same availability information as another platform.
134+
///
135+
/// The key corresponds to the fallback platform and the value to the platform it's
136+
/// fallbacking from.
134137
package static let fallbackPlatforms: [PlatformName: PlatformName] = [
135138
.catalyst: .iOS,
136139
.iPadOS: .iOS,

Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ extension DocumentationBundle {
4040
/// The keys that must be present in an Info.plist file in order for doc compilation to proceed.
4141
static let requiredKeys: Set<CodingKeys> = [.displayName, .identifier]
4242

43-
enum CodingKeys: String, CodingKey, CaseIterable {
43+
package enum CodingKeys: String, CodingKey, CaseIterable {
4444
case displayName = "CFBundleDisplayName"
4545
case identifier = "CFBundleIdentifier"
4646
case defaultCodeListingLanguage = "CDDefaultCodeListingLanguage"

Sources/SwiftDocC/Semantics/Symbol/Symbol.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -552,10 +552,12 @@ extension Symbol {
552552
func mergeAvailabilities(unifiedSymbol: UnifiedSymbolGraph.Symbol) {
553553
for (selector, mixins) in unifiedSymbol.mixins {
554554
let trait = DocumentationDataVariantsTrait(for: selector)
555-
if let unifiedSymbolAvailability = mixins[SymbolGraph.Symbol.Availability.mixinKey] as? SymbolGraph.Symbol.Availability {
555+
guard let availabilityVariantTrait = availabilityVariants[trait] else {
556+
return
557+
}
558+
if let unifiedSymbolAvailability = mixins.getValueIfPresent(for: SymbolGraph.Symbol.Availability.self) {
556559
unifiedSymbolAvailability.availability.forEach { availabilityItem in
557-
guard let availabilityVariantTrait = availabilityVariants[trait] else { return }
558-
if (availabilityVariantTrait.availability.contains(where: { $0.domain?.rawValue == availabilityItem.domain?.rawValue })) {
560+
guard availabilityVariantTrait.availability.firstIndex(where: { $0.domain?.rawValue == availabilityItem.domain?.rawValue }) == nil else {
559561
return
560562
}
561563
availabilityVariants[trait]?.availability.append(availabilityItem)

Sources/SwiftDocCTestUtilities/FilesAndFolders.swift

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,36 +94,45 @@ public struct InfoPlist: File, DataRepresentable {
9494
/// The information that the Into.plist file contains.
9595
public let content: Content
9696

97-
public init(displayName: String? = nil, identifier: String? = nil) {
97+
public init(displayName: String? = nil, identifier: String? = nil, defaultAvailability: [String: [DefaultAvailability.ModuleAvailability]]? = nil) {
9898
self.content = Content(
9999
displayName: displayName,
100-
identifier: identifier
100+
identifier: identifier,
101+
defaultAvailability: defaultAvailability
101102
)
102103
}
103104

104105
public struct Content: Codable, Equatable {
105106
public let displayName: String?
106107
public let identifier: String?
108+
public let defaultAvailability: [String: [DefaultAvailability.ModuleAvailability]]?
107109

108-
fileprivate init(displayName: String?, identifier: String?) {
110+
fileprivate init(displayName: String?, identifier: String?, defaultAvailability: [String: [DefaultAvailability.ModuleAvailability]]?) {
109111
self.displayName = displayName
110112
self.identifier = identifier
113+
self.defaultAvailability = defaultAvailability
111114
}
112-
113-
enum CodingKeys: String, CodingKey {
114-
case displayName = "CFBundleDisplayName"
115-
case identifier = "CFBundleIdentifier"
115+
116+
public init(from decoder: any Decoder) throws {
117+
let container = try decoder.container(keyedBy: DocumentationBundle.Info.CodingKeys.self)
118+
displayName = try container.decodeIfPresent(String.self, forKey: .displayName)
119+
identifier = try container.decodeIfPresent(String.self, forKey: .identifier)
120+
defaultAvailability = try container.decodeIfPresent([String : [DefaultAvailability.ModuleAvailability]].self, forKey: .defaultAvailability)
121+
}
122+
123+
public func encode(to encoder: any Encoder) throws {
124+
var container = encoder.container(keyedBy: DocumentationBundle.Info.CodingKeys.self)
125+
try container.encodeIfPresent(displayName, forKey: .displayName)
126+
try container.encodeIfPresent(identifier, forKey: .identifier)
127+
try container.encodeIfPresent(defaultAvailability, forKey: .defaultAvailability)
116128
}
117129
}
118130

119131
public func data() throws -> Data {
120132
let encoder = PropertyListEncoder()
121133
encoder.outputFormat = .xml
122134

123-
return try encoder.encode([
124-
Content.CodingKeys.displayName.rawValue: content.displayName,
125-
Content.CodingKeys.identifier.rawValue: content.identifier,
126-
])
135+
return try encoder.encode(content)
127136
}
128137
}
129138

Sources/SwiftDocCTestUtilities/SymbolGraphCreation.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ extension XCTestCase {
8686
accessLevel: SymbolGraph.Symbol.AccessControl = .init(rawValue: "public"), // Defined internally in SwiftDocC
8787
location: (position: SymbolGraph.LineList.SourceRange.Position, url: URL)? = (defaultSymbolPosition, defaultSymbolURL),
8888
signature: SymbolGraph.Symbol.FunctionSignature? = nil,
89+
availability: [SymbolGraph.Symbol.Availability.AvailabilityItem]? = nil,
8990
otherMixins: [any Mixin] = []
9091
) -> SymbolGraph.Symbol {
9192
precondition(!pathComponents.isEmpty, "Need at least one path component to name the symbol")
@@ -97,6 +98,9 @@ extension XCTestCase {
9798
if let signature {
9899
mixins.append(signature)
99100
}
101+
if let availability {
102+
mixins.append(SymbolGraph.Symbol.Availability(availability: availability))
103+
}
100104

101105
return SymbolGraph.Symbol(
102106
identifier: SymbolGraph.Symbol.Identifier(precise: id, interfaceLanguage: language.id),
@@ -115,6 +119,14 @@ extension XCTestCase {
115119
)
116120
}
117121

122+
package func makeAvailabilityItem(
123+
domainName: String,
124+
introduced: SymbolGraph.SemanticVersion?,
125+
deprecated: SymbolGraph.SemanticVersion? = nil
126+
) -> SymbolGraph.Symbol.Availability.AvailabilityItem {
127+
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)
128+
}
129+
118130
package func makeSymbolNames(name: String) -> SymbolGraph.Symbol.Names {
119131
SymbolGraph.Symbol.Names(
120132
title: name,

Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphLoaderTests.swift

Lines changed: 130 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ class SymbolGraphLoaderTests: XCTestCase {
161161
// Update one symbol's availability to use as a verification if we're loading iOS or Catalyst symbol graph
162162
catalystSymbolGraph.symbols["s:5MyKit0A5ClassC"]!.mixins[SymbolGraph.Symbol.Availability.mixinKey]! = SymbolGraph.Symbol.Availability(availability: [
163163
.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),
164-
.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),
164+
.init(domain: SymbolGraph.Symbol.Availability.Domain(rawValue: "iOS"), introducedVersion: nil, deprecatedVersion: nil, obsoletedVersion: nil, message: nil, renamed: nil, isUnconditionallyDeprecated: false, isUnconditionallyUnavailable: false, willEventuallyBeDeprecated: false),
165165
])
166166

167167
let catalystSymbolGraphURL = bundleURL.appendingPathComponent(catalystSymbolGraphName)
@@ -671,13 +671,14 @@ class SymbolGraphLoaderTests: XCTestCase {
671671
}
672672
""",
673673
platform: """
674+
"environment" : "macabi",
674675
"operatingSystem" : {
675676
"minimumVersion" : {
676677
"major" : 6,
677678
"minor" : 5,
678679
"patch" : 0
679680
},
680-
"name" : "macCatalyst"
681+
"name" : "ios",
681682
}
682683
"""
683684
)
@@ -775,12 +776,13 @@ class SymbolGraphLoaderTests: XCTestCase {
775776
}
776777
""",
777778
platform: """
779+
"environment" : "macabi",
778780
"operatingSystem" : {
779781
"minimumVersion" : {
780782
"major" : 6,
781783
"minor" : 5
782784
},
783-
"name" : "macCatalyst"
785+
"name" : "ios"
784786
}
785787
"""
786788
)
@@ -931,6 +933,7 @@ class SymbolGraphLoaderTests: XCTestCase {
931933
XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "iPadOS" }))
932934
XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iOS" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 8, minor: 0, patch: 0))
933935
XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "macCatalyst" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 7, minor: 0, patch: 0))
936+
XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iPadOS" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 6, minor: 0, patch: 0))
934937
}
935938

936939
func testUnconditionallyunavailablePlatforms() throws {
@@ -995,7 +998,7 @@ class SymbolGraphLoaderTests: XCTestCase {
995998
},
996999
"accessLevel": "public",
9971000
"availability" : [{
998-
"domain" : "maccatalyst",
1001+
"domain" : "macCatalyst",
9991002
"introduced" : {
10001003
"major" : 12,
10011004
"minor" : 0
@@ -1401,7 +1404,7 @@ class SymbolGraphLoaderTests: XCTestCase {
14011404
"accessLevel" : "public",
14021405
"availability" : [
14031406
{
1404-
"domain" : "maccatalyst",
1407+
"domain" : "macCatalyst",
14051408
"introduced" : {
14061409
"major" : 15,
14071410
"minor" : 2,
@@ -1453,7 +1456,7 @@ class SymbolGraphLoaderTests: XCTestCase {
14531456
// 'Mac Catalyst' (info.plist) and 'maccatalyst' (SGF).
14541457
XCTAssertTrue(availability.count == 2)
14551458
XCTAssertTrue(availability.filter({ $0.domain?.rawValue == "macCatalyst" }).count == 1)
1456-
XCTAssertTrue(availability.filter({ $0.domain?.rawValue == "maccatalyst" }).count == 0)
1459+
XCTAssertTrue(availability.filter({ $0.domain?.rawValue == "Mac Catalyst" }).count == 0)
14571460
}
14581461

14591462
func testFallbackOverrideDefaultAvailability() throws {
@@ -1518,17 +1521,7 @@ class SymbolGraphLoaderTests: XCTestCase {
15181521
"names": {
15191522
"title": "Foo",
15201523
},
1521-
"accessLevel": "public",
1522-
"availability" : [
1523-
{
1524-
"domain" : "iOS",
1525-
"introduced" : {
1526-
"major" : 12,
1527-
"minor" : 0,
1528-
"patch" : 0
1529-
}
1530-
}
1531-
]
1524+
"accessLevel": "public"
15321525
}
15331526
""",
15341527
platform: """
@@ -1595,8 +1588,7 @@ class SymbolGraphLoaderTests: XCTestCase {
15951588
"names": {
15961589
"title": "Foo",
15971590
},
1598-
"accessLevel": "public",
1599-
"availability" : []
1591+
"accessLevel": "public"
16001592
}
16011593
""",
16021594
platform: """
@@ -1652,6 +1644,125 @@ class SymbolGraphLoaderTests: XCTestCase {
16521644
XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "macCatalyst" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 1, minor: 0, patch: 0))
16531645
}
16541646

1647+
func testNotAvailableSymbol() throws {
1648+
// Symbol from SG
1649+
let symbolGraphStringiOS = makeSymbolGraphString(
1650+
moduleName: "MyModule",
1651+
symbols: """
1652+
{
1653+
"kind": {
1654+
"displayName" : "Instance Property",
1655+
"identifier" : "swift.property"
1656+
},
1657+
"identifier": {
1658+
"precise": "c:@F@A",
1659+
"interfaceLanguage": "swift"
1660+
},
1661+
"pathComponents": [
1662+
"Foo"
1663+
],
1664+
"names": {
1665+
"title": "Foo",
1666+
},
1667+
"accessLevel": "public",
1668+
"availability" : [
1669+
{
1670+
"domain" : "macOS",
1671+
"introduced" : {
1672+
"major" : 12,
1673+
"minor" : 0,
1674+
"patch" : 0
1675+
}
1676+
}
1677+
]
1678+
}
1679+
""",
1680+
platform: """
1681+
"operatingSystem" : {
1682+
"minimumVersion" : {
1683+
"major" : 12,
1684+
"minor" : 0,
1685+
"patch" : 0
1686+
},
1687+
"name" : "macosx"
1688+
}
1689+
"""
1690+
)
1691+
let symbolGraphStringMacOS = makeSymbolGraphString(
1692+
moduleName: "MyModule",
1693+
symbols: """
1694+
{
1695+
"kind": {
1696+
"displayName" : "Instance Property",
1697+
"identifier" : "swift.property"
1698+
},
1699+
"identifier": {
1700+
"precise": "c:@F@B",
1701+
"interfaceLanguage": "swift"
1702+
},
1703+
"pathComponents": [
1704+
"Foo"
1705+
],
1706+
"names": {
1707+
"title": "Bar",
1708+
},
1709+
"accessLevel": "public",
1710+
"availability" : [
1711+
{
1712+
"domain" : "iOS",
1713+
"introduced" : {
1714+
"major" : 12,
1715+
"minor" : 0,
1716+
"patch" : 0
1717+
}
1718+
}
1719+
]
1720+
}
1721+
""",
1722+
platform: """
1723+
"operatingSystem" : {
1724+
"minimumVersion" : {
1725+
"major" : 6,
1726+
"minor" : 5,
1727+
"patch" : 0
1728+
},
1729+
"name" : "ios"
1730+
}
1731+
"""
1732+
)
1733+
let infoPlist = """
1734+
<plist version="1.0">
1735+
<dict>
1736+
<key>CDAppleDefaultAvailability</key>
1737+
<dict>
1738+
<key>MyModule</key>
1739+
<array>
1740+
<dict>
1741+
<key>name</key>
1742+
<string>iOS</string>
1743+
<key>version</key>
1744+
<string>1.0</string>
1745+
</dict>
1746+
</array>
1747+
</dict>
1748+
</dict>
1749+
</plist>
1750+
"""
1751+
// Create an empty bundle
1752+
let targetURL = try createTemporaryDirectory(named: "test.docc")
1753+
// Store files
1754+
try symbolGraphStringiOS.write(to: targetURL.appendingPathComponent("MyModule-ios.symbols.json"), atomically: true, encoding: .utf8)
1755+
try symbolGraphStringMacOS.write(to: targetURL.appendingPathComponent("MyModule-macos.symbols.json"), atomically: true, encoding: .utf8)
1756+
try infoPlist.write(to: targetURL.appendingPathComponent("Info.plist"), atomically: true, encoding: .utf8)
1757+
// Load the bundle & reference resolve symbol graph docs
1758+
let (_, _, context) = try loadBundle(from: targetURL)
1759+
let availability = try XCTUnwrap((context.documentationCache["c:@F@A"]?.semantic as? Symbol)?.availability?.availability)
1760+
// Verify we don't fallback to iOS for 'Foo' even if there's default availability.
1761+
XCTAssertNil(availability.first(where: { $0.domain?.rawValue == "iOS" }))
1762+
}
1763+
1764+
1765+
16551766
// MARK: - Helpers
16561767

16571768
private func makeSymbolGraphLoader(

0 commit comments

Comments
 (0)