Skip to content

Commit f361d5f

Browse files
Allow default availability entries without version number. (#1037)
Allow default availability entries without version number. This change allows the definition of entries in the Default Availability section of the Info.plist file without specifying an introduced version. The goal of this change is to avoid enforcing a potentially incorrect version for symbols with unknown introduction versions while still indicating their availability on a given platform. Additionally, it allows for communicating that a framework is available on other platforms, such as Linux. rdar://132980711
1 parent 3e05bd7 commit f361d5f

File tree

5 files changed

+201
-16
lines changed

5 files changed

+201
-16
lines changed

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -401,10 +401,12 @@ extension SymbolGraph.SemanticVersion {
401401
extension SymbolGraph.Symbol.Availability.AvailabilityItem {
402402
/// Create an availability item with a `domain` and an `introduced` version.
403403
/// - parameter defaultAvailability: Default availability information for symbols that lack availability authored in code.
404-
/// - Note: If the `defaultAvailability` argument doesn't have a valid
405-
/// platform version that can be parsed as a `SemanticVersion`, returns `nil`.
404+
/// - Note: If the `defaultAvailability` argument has a introduced version that can't
405+
/// be parsed as a `SemanticVersion`, returns `nil`.
406406
init?(_ defaultAvailability: DefaultAvailability.ModuleAvailability) {
407-
guard let introducedVersion = defaultAvailability.introducedVersion, let platformVersion = SymbolGraph.SemanticVersion(string: introducedVersion) else {
407+
let introducedVersion = defaultAvailability.introducedVersion
408+
let platformVersion = introducedVersion.flatMap { SymbolGraph.SemanticVersion(string: $0) }
409+
if platformVersion == nil && introducedVersion != nil {
408410
return nil
409411
}
410412
let domain = SymbolGraph.Symbol.Availability.Domain(rawValue: defaultAvailability.platformName.rawValue)

Sources/SwiftDocC/Infrastructure/Workspace/DefaultAvailability.swift

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ import Foundation
3030
/// </dict>
3131
/// <dict>
3232
/// <key>name</key>
33+
/// <string>Platform Name</string>
34+
/// </dict>
35+
/// <dict>
36+
/// <key>name</key>
3337
/// <string>Other Platform Name</string>
3438
/// <key>unavailable</key>
3539
/// <true/>
@@ -48,10 +52,10 @@ public struct DefaultAvailability: Codable, Equatable {
4852
}
4953

5054
/// The different availability states that can be declared.
51-
/// Unavailable or Available with an introduced version.
55+
/// Unavailable or Available with a potential introduced version.
5256
enum VersionInformation: Hashable {
5357
case unavailable
54-
case available(version: String)
58+
case available(version: String?)
5559
}
5660

5761
/// The name of the platform, e.g. "macOS".
@@ -71,7 +75,7 @@ public struct DefaultAvailability: Codable, Equatable {
7175
public var introducedVersion: String? {
7276
switch versionInformation {
7377
case .available(let introduced):
74-
return introduced.description
78+
return introduced?.description
7579
case .unavailable:
7680
return nil
7781
}
@@ -82,7 +86,7 @@ public struct DefaultAvailability: Codable, Equatable {
8286
/// - Parameters:
8387
/// - platformName: A platform name, such as "iOS" or "macOS"; see ``PlatformName``.
8488
/// - platformVersion: A 2- or 3-component version string, such as `"13.0"` or `"13.1.2"`.
85-
public init(platformName: PlatformName, platformVersion: String) {
89+
public init(platformName: PlatformName, platformVersion: String?) {
8690
self.platformName = platformName
8791
self.versionInformation = .available(version: platformVersion)
8892
}
@@ -103,10 +107,14 @@ public struct DefaultAvailability: Codable, Equatable {
103107
versionInformation = .unavailable
104108
return
105109
}
106-
let introducedVersion = try values.decode(String.self, forKey: .platformVersion)
110+
let introducedVersion = try values.decodeIfPresent(String.self, forKey: .platformVersion)
107111
versionInformation = .available(version: introducedVersion)
108-
guard let version = Version(versionString: introducedVersion), (2...3).contains(version.count) else {
109-
throw DocumentationBundle.PropertyListError.invalidVersionString(introducedVersion)
112+
// If the default availability contains a version, validate it's a
113+
// semantic version.
114+
if let introducedVersion {
115+
guard let version = Version(versionString: introducedVersion), (2...3).contains(version.count) else {
116+
throw DocumentationBundle.PropertyListError.invalidVersionString(introducedVersion)
117+
}
110118
}
111119
}
112120

Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,8 +1249,10 @@ public struct RenderNodeTranslator: SemanticVisitor {
12491249

12501250
return availability.availability
12511251
.compactMap { availability -> AvailabilityRenderItem? in
1252-
// Filter items with insufficient availability data
1253-
guard availability.introducedVersion != nil else {
1252+
// Filter items with insufficient availability data.
1253+
// Allow availability without version information, but only if
1254+
// both, introduced and deprecated, are nil.
1255+
if availability.introducedVersion == nil && availability.deprecatedVersion != nil {
12541256
return nil
12551257
}
12561258
guard let name = availability.domain.map({ PlatformName(operatingSystemName: $0.rawValue) }),
@@ -1817,10 +1819,9 @@ public struct RenderNodeTranslator: SemanticVisitor {
18171819
let renderedAvailability = moduleAvailability
18181820
.filter({ $0.versionInformation != .unavailable })
18191821
.compactMap({ availability -> AvailabilityRenderItem? in
1820-
guard let availabilityIntroducedVersion = availability.introducedVersion else { return nil }
18211822
return AvailabilityRenderItem(
18221823
name: availability.platformName.displayName,
1823-
introduced: availabilityIntroducedVersion,
1824+
introduced: availability.introducedVersion,
18241825
isBeta: currentPlatforms.map({ isModuleBeta(moduleAvailability: availability, currentPlatforms: $0) }) ?? false
18251826
)
18261827
})

Sources/SwiftDocC/Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ public struct AvailabilityRenderItem: Codable, Hashable, Equatable {
157157
/// - name: A platform name.
158158
/// - introduced: A version string.
159159
/// - isBeta: If `true`, the symbol is introduced in a beta version of the platform.
160-
init(name: String, introduced: String, isBeta: Bool) {
160+
init(name: String, introduced: String?, isBeta: Bool) {
161161
self.name = name
162162
self.introduced = introduced
163163
self.isBeta = isBeta

Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift

Lines changed: 175 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class DefaultAvailabilityTests: XCTestCase {
8585
var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference)
8686
let renderNode = translator.visit(node.semantic) as! RenderNode
8787

88-
XCTAssertEqual(renderNode.metadata.platforms?.map({ "\($0.name ?? "") \($0.introduced ?? "")" }).sorted(), [expectedDefaultAvailability.last ?? ""])
88+
XCTAssertEqual(renderNode.metadata.platforms?.map({ "\($0.name ?? "") \($0.introduced ?? "")" }).sorted(), ["Mac Catalyst ", "iOS ", "iPadOS ", "macOS 10.15.1"])
8989
}
9090

9191
// Test if the default availability is NOT used for symbols with explicit availability
@@ -579,4 +579,178 @@ class DefaultAvailabilityTests: XCTestCase {
579579
)
580580

581581
}
582+
583+
func testInheritDefaultAvailabilityOptions() throws {
584+
func makeInfoPlist(
585+
defaultAvailability: String
586+
) -> String {
587+
return """
588+
<plist version="1.0">
589+
<dict>
590+
<key>CDAppleDefaultAvailability</key>
591+
<dict>
592+
<key>MyModule</key>
593+
<array>
594+
\(defaultAvailability)
595+
</array>
596+
</dict>
597+
</dict>
598+
</plist>
599+
"""
600+
}
601+
func setupContext(
602+
defaultAvailability: String
603+
) throws -> (DocumentationBundle, DocumentationContext) {
604+
// Create an empty bundle
605+
let targetURL = try createTemporaryDirectory(named: "test.docc")
606+
// Create symbol graph
607+
let symbolGraphURL = targetURL.appendingPathComponent("MyModule.symbols.json")
608+
try symbolGraphString.write(to: symbolGraphURL, atomically: true, encoding: .utf8)
609+
// Create info plist
610+
let infoPlistURL = targetURL.appendingPathComponent("Info.plist")
611+
let infoPlist = makeInfoPlist(defaultAvailability: defaultAvailability)
612+
try infoPlist.write(to: infoPlistURL, atomically: true, encoding: .utf8)
613+
// Load the bundle & reference resolve symbol graph docs
614+
let (_, bundle, context) = try loadBundle(from: targetURL)
615+
return (bundle, context)
616+
}
617+
618+
let symbols = """
619+
{
620+
"kind": {
621+
"displayName" : "Instance Property",
622+
"identifier" : "swift.property"
623+
},
624+
"identifier": {
625+
"precise": "c:@F@SymbolWithAvailability",
626+
"interfaceLanguage": "swift"
627+
},
628+
"pathComponents": [
629+
"Foo"
630+
],
631+
"names": {
632+
"title": "Foo",
633+
},
634+
"accessLevel": "public",
635+
"availability" : [
636+
{
637+
"domain" : "ios",
638+
"introduced" : {
639+
"major" : 10,
640+
"minor" : 0
641+
}
642+
}
643+
]
644+
},
645+
{
646+
"kind": {
647+
"displayName" : "Instance Property",
648+
"identifier" : "swift.property"
649+
},
650+
"identifier": {
651+
"precise": "c:@F@SymbolWithoutAvailability",
652+
"interfaceLanguage": "swift"
653+
},
654+
"pathComponents": [
655+
"Foo"
656+
],
657+
"names": {
658+
"title": "Bar",
659+
},
660+
"accessLevel": "public"
661+
}
662+
"""
663+
let symbolGraphString = makeSymbolGraphString(
664+
moduleName: "MyModule",
665+
symbols: symbols,
666+
platform: """
667+
"operatingSystem" : {
668+
"minimumVersion" : {
669+
"major" : 10,
670+
"minor" : 0
671+
},
672+
"name" : "ios"
673+
}
674+
"""
675+
)
676+
677+
// Don't use default availability version.
678+
679+
var (bundle, context) = try setupContext(
680+
defaultAvailability: """
681+
<dict>
682+
<key>name</key>
683+
<string>iOS</string>
684+
</dict>
685+
"""
686+
)
687+
688+
// Verify we add the version number into the symbols that have availability annotation.
689+
guard let availability = (context.documentationCache["c:@F@SymbolWithAvailability"]?.semantic as? Symbol)?.availability?.availability else {
690+
XCTFail("Did not find availability for symbol 'c:@F@SymbolWithAvailability'")
691+
return
692+
}
693+
XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "iOS" }))
694+
XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iOS" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 10, minor: 0, patch: 0))
695+
// Verify we don't add the version number into the symbols that don't have availability annotation.
696+
guard let availability = (context.documentationCache["c:@F@SymbolWithoutAvailability"]?.semantic as? Symbol)?.availability?.availability else {
697+
XCTFail("Did not find availability for symbol 'c:@F@SymbolWithoutAvailability'")
698+
return
699+
}
700+
XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "iOS" }))
701+
XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iOS" })?.introducedVersion, nil)
702+
// Verify we remove the version from the module availability information.
703+
var identifier = ResolvedTopicReference(bundleIdentifier: "test", path: "/documentation/MyModule", fragment: nil, sourceLanguage: .swift)
704+
var node = try context.entity(with: identifier)
705+
var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: identifier)
706+
var renderNode = translator.visit(node.semantic) as! RenderNode
707+
XCTAssertEqual(renderNode.metadata.platforms?.count, 1)
708+
XCTAssertEqual(renderNode.metadata.platforms?.first?.name, "iOS")
709+
XCTAssertEqual(renderNode.metadata.platforms?.first?.introduced, nil)
710+
711+
// Add an extra default availability to test behaviour when mixin in source with default behaviour.
712+
(bundle, context) = try setupContext(defaultAvailability: """
713+
<dict>
714+
<key>name</key>
715+
<string>iOS</string>
716+
<key>version</key>
717+
<string>8.0</string>
718+
</dict>
719+
<dict>
720+
<key>name</key>
721+
<string>watchOS</string>
722+
</dict>
723+
"""
724+
)
725+
726+
// Verify we add the version number into the symbols that have availability annotation.
727+
guard let availability = (context.documentationCache["c:@F@SymbolWithAvailability"]?.semantic as? Symbol)?.availability?.availability else {
728+
XCTFail("Did not find availability for symbol 'c:@F@SymbolWithAvailability'")
729+
return
730+
}
731+
XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "iOS" }))
732+
XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "watchOS" }))
733+
XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iOS" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 10, minor: 0, patch: 0))
734+
XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "watchOS" })?.introducedVersion, nil)
735+
736+
guard let availability = (context.documentationCache["c:@F@SymbolWithoutAvailability"]?.semantic as? Symbol)?.availability?.availability else {
737+
XCTFail("Did not find availability for symbol 'c:@F@SymbolWithAvailability'")
738+
return
739+
}
740+
XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "iOS" }))
741+
XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "watchOS" }))
742+
XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iOS" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 8, minor: 0, patch: 0))
743+
XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "watchOS" })?.introducedVersion, nil)
744+
745+
// Verify the module availability shows as expected.
746+
identifier = ResolvedTopicReference(bundleIdentifier: "test", path: "/documentation/MyModule", fragment: nil, sourceLanguage: .swift)
747+
node = try context.entity(with: identifier)
748+
translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: identifier)
749+
renderNode = translator.visit(node.semantic) as! RenderNode
750+
XCTAssertEqual(renderNode.metadata.platforms?.count, 4)
751+
var moduleAvailability = try XCTUnwrap(renderNode.metadata.platforms?.first(where: {$0.name == "iOS"}))
752+
XCTAssertEqual(moduleAvailability.introduced, "8.0")
753+
moduleAvailability = try XCTUnwrap(renderNode.metadata.platforms?.first(where: {$0.name == "watchOS"}))
754+
XCTAssertEqual(moduleAvailability.introduced, nil)
755+
}
582756
}

0 commit comments

Comments
 (0)