Skip to content

Commit 926dcc4

Browse files
Allow availability items with only deprecated versions. (#1079)
* Allow availability items with deprecated version and no introduced versions. There can be symbols that define an explicit default avaialblility verion in the symbol graph but don't define an introduced version. Before this change DocC would drop the availability item entirely. This change allows this case without droping the item. This is needed since the `@available` attribute allows defining only the deprecated version. rdar://138034711 * Filter out oboleted availability items. Obsoleted availbaility is not something that we want to display in the final documentation of a symbol. Before the change made on the first commit this logic was not needed since the availability items that surfaced had the requirement of an introduced version, but since the introduction of versionless availability items this is not the case anymore, but we don't want this information, that is represented in the SGF in the following way: ``` "availability":[ { "domain":"platform name", "obsoleted":{"major":13,"minor":0} } ] ``` to be rendered in the final documentation.
1 parent 90d64a8 commit 926dcc4

File tree

9 files changed

+203
-30
lines changed

9 files changed

+203
-30
lines changed

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/Model/AvailabilityParser.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ struct AvailabilityParser {
4040

4141
// Check if the symbol is unconditionally deprecated
4242
let isUnconditionallyDeprecated = availability.availability
43-
.allSatisfy { $0.isUnconditionallyDeprecated || $0.isUnconditionallyUnavailable || $0.deprecatedVersion != nil }
43+
.allSatisfy { $0.isUnconditionallyDeprecated || $0.isUnconditionallyUnavailable || $0.deprecatedVersion != nil || $0.obsoletedVersion != nil }
4444
// If a symbol is unavailable on all known platforms, it should not be marked unconditionally deprecated.
45-
&& availability.availability.contains(where: { $0.isUnconditionallyDeprecated || $0.deprecatedVersion != nil })
45+
&& availability.availability.contains(where: { $0.isUnconditionallyDeprecated || $0.deprecatedVersion != nil || $0.obsoletedVersion != nil })
4646
return isUnconditionallyDeprecated
4747
}
4848

Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,10 +1253,9 @@ public struct RenderNodeTranslator: SemanticVisitor {
12531253

12541254
return availability.availability
12551255
.compactMap { availability -> AvailabilityRenderItem? in
1256-
// Filter items with insufficient availability data.
1257-
// Allow availability without version information, but only if
1258-
// both, introduced and deprecated, are nil.
1259-
if availability.introducedVersion == nil && availability.deprecatedVersion != nil {
1256+
// Allow availability items without introduced and/or deprecated version,
1257+
// but filter out items that are obsoleted.
1258+
if availability.obsoletedVersion != nil {
12601259
return nil
12611260
}
12621261
guard let name = availability.domain.map({ PlatformName(operatingSystemName: $0.rawValue) }),

Sources/SwiftDocCTestUtilities/FilesAndFolders.swift

Lines changed: 19 additions & 10 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
}
112115

113-
enum CodingKeys: String, CodingKey {
114-
case displayName = "CFBundleDisplayName"
115-
case identifier = "CFBundleIdentifier"
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: 14 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,16 @@ extension XCTestCase {
115119
)
116120
}
117121

122+
package func makeAvailabilityItem(
123+
domainName: String,
124+
introduced: SymbolGraph.SemanticVersion? = nil,
125+
deprecated: SymbolGraph.SemanticVersion? = nil,
126+
obsoleted: SymbolGraph.SemanticVersion? = nil,
127+
unconditionallyUnavailable: Bool = false
128+
) -> SymbolGraph.Symbol.Availability.AvailabilityItem {
129+
return SymbolGraph.Symbol.Availability.AvailabilityItem(domain: .init(rawValue: domainName), introducedVersion: introduced, deprecatedVersion: deprecated, obsoletedVersion: obsoleted, message: nil, renamed: nil, isUnconditionallyDeprecated: false, isUnconditionallyUnavailable: unconditionallyUnavailable, willEventuallyBeDeprecated: false)
130+
}
131+
118132
package func makeSymbolNames(name: String) -> SymbolGraph.Symbol.Names {
119133
SymbolGraph.Symbol.Names(
120134
title: name,

Tests/SwiftDocCTests/Model/AvailabilityParserTests.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,24 @@ class AvailabilityParserTests: XCTestCase {
202202
XCTAssertTrue(compiler.isDeprecated())
203203
XCTAssertEqual(compiler.deprecationMessage(), "deprecated")
204204
}
205+
206+
func testADeprecatedAndObsolete() throws {
207+
let json = """
208+
[
209+
{
210+
"domain": "tvOS",
211+
"obsoleted": { "major": 10, "minor": 17 }
212+
},
213+
{
214+
"domain": "macOS",
215+
"deprecated": { "major": 10, "minor": 17 }
216+
}
217+
]
218+
"""
219+
let availability = try JSONDecoder().decode(Availability.self, from: json.data(using: .utf8)!)
220+
221+
/// Test all platforms
222+
let compiler = AvailabilityParser(availability)
223+
XCTAssertTrue(compiler.isDeprecated())
224+
}
205225
}

Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1504,16 +1504,28 @@ class SemaToRenderNodeTests: XCTestCase {
15041504

15051505
// Verify only 3 availability items are rendered, since the iOS availability in the graph fixture is invalid
15061506
// and therefore Catalyst and iPadOS are also invalid.
1507-
XCTAssertEqual(platforms.count, 3)
1507+
XCTAssertEqual(platforms.count, 6)
15081508

1509-
XCTAssertEqual(platforms[0].name, "macOS")
1510-
XCTAssertEqual(platforms[0].introduced, "10.15")
1509+
XCTAssertEqual(platforms[0].name, "Mac Catalyst")
1510+
XCTAssertEqual(platforms[0].introduced, nil)
1511+
XCTAssertEqual(platforms[0].deprecated, "13.0")
15111512

1512-
XCTAssertEqual(platforms[1].name, "tvOS")
1513-
XCTAssertEqual(platforms[1].introduced, "13.0")
1513+
XCTAssertEqual(platforms[1].name, "iOS")
1514+
XCTAssertEqual(platforms[1].introduced, nil)
1515+
XCTAssertEqual(platforms[1].deprecated, "13.0")
15141516

1515-
XCTAssertEqual(platforms[2].name, "watchOS")
1516-
XCTAssertEqual(platforms[2].introduced, "6.0")
1517+
XCTAssertEqual(platforms[2].name, "iPadOS")
1518+
XCTAssertEqual(platforms[2].introduced, nil)
1519+
XCTAssertEqual(platforms[2].deprecated, "13.0")
1520+
1521+
XCTAssertEqual(platforms[3].name, "macOS")
1522+
XCTAssertEqual(platforms[3].introduced, "10.15")
1523+
1524+
XCTAssertEqual(platforms[4].name, "tvOS")
1525+
XCTAssertEqual(platforms[4].introduced, "13.0")
1526+
1527+
XCTAssertEqual(platforms[5].name, "watchOS")
1528+
XCTAssertEqual(platforms[5].introduced, "6.0")
15171529
}
15181530

15191531
func testAvailabilityFromCurrentPlatformOverridesExistingValue() throws {
@@ -1975,7 +1987,7 @@ Document
19751987
let renderNode = try DocumentationNodeConverter(bundle: bundle, context: context).convert(node)
19761988

19771989
// Verify platform beta was plumbed all the way to the render JSON
1978-
XCTAssertEqual(renderNode.metadata.platforms?.first?.isBeta, true)
1990+
XCTAssertEqual(renderNode.metadata.platforms?.first(where: { $0.name == "macOS" })?.isBeta, true)
19791991
}
19801992

19811993
// Beta platform earlier than the introduced version
@@ -1989,7 +2001,7 @@ Document
19892001
let renderNode = try DocumentationNodeConverter(bundle: bundle, context: context).convert(node)
19902002

19912003
// Verify platform beta was plumbed all the way to the render JSON
1992-
XCTAssertEqual(renderNode.metadata.platforms?.first?.isBeta, true)
2004+
XCTAssertEqual(renderNode.metadata.platforms?.first(where: { $0.name == "macOS" })?.isBeta, true)
19932005
}
19942006

19952007
// Set only some platforms to beta & the exact version MyClass is being introduced at
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
import XCTest
13+
import SymbolKit
14+
@testable import SwiftDocC
15+
import SwiftDocCTestUtilities
16+
17+
class SymbolAvailabilityTests: XCTestCase {
18+
19+
private func symbolAvailability(
20+
defaultAvailability: [DefaultAvailability.ModuleAvailability] = [],
21+
symbolGraphOperatingSystemPlatformName: String,
22+
symbols: [SymbolGraph.Symbol],
23+
symbolName: String
24+
) throws -> [SymbolGraph.Symbol.Availability.AvailabilityItem] {
25+
let catalog = Folder(
26+
name: "unit-test.docc",
27+
content: [
28+
InfoPlist(defaultAvailability: [
29+
"ModuleName": defaultAvailability
30+
]),
31+
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
32+
moduleName: "ModuleName",
33+
platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: SymbolGraph.OperatingSystem(name: symbolGraphOperatingSystemPlatformName), environment: nil),
34+
symbols: symbols,
35+
relationships: []
36+
)),
37+
]
38+
)
39+
let (_, context) = try loadBundle(catalog: catalog)
40+
let reference = try XCTUnwrap(context.soleRootModuleReference).appendingPath(symbolName)
41+
let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol)
42+
return try XCTUnwrap(symbol.availability?.availability)
43+
}
44+
45+
private func renderNodeAvailability(
46+
defaultAvailability: [DefaultAvailability.ModuleAvailability] = [],
47+
symbolGraphOperatingSystemPlatformName: String,
48+
symbols: [SymbolGraph.Symbol],
49+
symbolName: String
50+
) throws -> [AvailabilityRenderItem] {
51+
let catalog = Folder(
52+
name: "unit-test.docc",
53+
content: [
54+
InfoPlist(defaultAvailability: [
55+
"ModuleName": defaultAvailability
56+
]),
57+
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
58+
moduleName: "ModuleName",
59+
platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: SymbolGraph.OperatingSystem(name: symbolGraphOperatingSystemPlatformName), environment: nil),
60+
symbols: symbols,
61+
relationships: []
62+
)),
63+
]
64+
)
65+
let (bundle, context) = try loadBundle(catalog: catalog)
66+
let reference = try XCTUnwrap(context.soleRootModuleReference).appendingPath(symbolName)
67+
let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: reference.path, sourceLanguage: .swift))
68+
var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference)
69+
return try XCTUnwrap((translator.visit(node.semantic as! Symbol) as! RenderNode).metadata.platformsVariants.defaultValue)
70+
}
71+
72+
func testSymbolGraphSymbolWithoutDeprecatedVersionAndIntroducedVersion() throws {
73+
74+
let availability = try renderNodeAvailability(
75+
defaultAvailability: [],
76+
symbolGraphOperatingSystemPlatformName: "ios",
77+
symbols: [
78+
makeSymbol(
79+
id: "platform-1-symbol",
80+
kind: .class,
81+
pathComponents: ["SymbolName"],
82+
availability: [makeAvailabilityItem(domainName: "iOS", deprecated: SymbolGraph.SemanticVersion(string: "1.2.3"))]
83+
)
84+
],
85+
symbolName: "SymbolName"
86+
)
87+
88+
XCTAssertEqual(availability.map { "\($0.name ?? "<nil>") \($0.introduced ?? "<nil>") - \($0.deprecated ?? "<nil>")" }, [
89+
// The availability items wihout an introduced version should still emit the deprecated version if available.
90+
"iOS <nil> - 1.2.3",
91+
"iPadOS <nil> - 1.2.3",
92+
"Mac Catalyst <nil> - 1.2.3",
93+
])
94+
}
95+
96+
func testSymbolGraphSymbolWithObsoleteVersion() throws {
97+
98+
let availability = try renderNodeAvailability(
99+
defaultAvailability: [],
100+
symbolGraphOperatingSystemPlatformName: "ios",
101+
symbols: [
102+
makeSymbol(
103+
id: "platform-1-symbol",
104+
kind: .class,
105+
pathComponents: ["SymbolName"],
106+
availability: [makeAvailabilityItem(domainName: "iOS", obsoleted: SymbolGraph.SemanticVersion(string: "1.2.3"))]
107+
)
108+
], symbolName: "SymbolName"
109+
)
110+
XCTAssertEqual(availability.map { "\($0.name ?? "<nil>") \($0.introduced ?? "<nil>") - \($0.deprecated ?? "<nil>")" }.sorted(), [
111+
// The availability items that are obsolete are not rendered in the final documentation.
112+
])
113+
}
114+
115+
}

Tests/SwiftDocCTests/Test Resources/TestBundle-RenderIndex.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@
172172
],
173173
"path" : "\/documentation\/mykit\/myclass",
174174
"title" : "MyClass",
175-
"type" : "class"
175+
"type" : "class",
176+
"deprecated" : true
176177
},
177178
{
178179
"children" : [
@@ -293,7 +294,8 @@
293294
],
294295
"path" : "\/documentation\/mykit\/myclass",
295296
"title" : "MyClass",
296-
"type" : "class"
297+
"type" : "class",
298+
"deprecated" : true
297299
},
298300
{
299301
"children" : [
@@ -414,7 +416,8 @@
414416
],
415417
"path" : "\/documentation\/mykit\/myclass",
416418
"title" : "MyClass",
417-
"type" : "class"
419+
"type" : "class",
420+
"deprecated" : true
418421
}
419422
],
420423
"path" : "\/documentation\/mykit\/myprotocol",
@@ -540,7 +543,8 @@
540543
],
541544
"path" : "\/documentation\/mykit\/myclass",
542545
"title" : "MyClass",
543-
"type" : "class"
546+
"type" : "class",
547+
"deprecated" : true
544548
},
545549
{
546550
"title" : "MyKit in Practice",

0 commit comments

Comments
 (0)