Skip to content

Commit 3e05bd7

Browse files
authored
Fix cross language availability inheritance (#1027)
* Fix cross-language availability inheritance Fixes a bug where DocC incorrectly inherits platform availability information of an API from other languages in which the API is defined. For example, if an API is deprecated in Swift but doesn't have availability information in Objective-C at all, DocC no longer considers the API as being deprecated in Objective-C. rdar://131331606 * Only show deprecated summaries on deprecated API When a symbol is deprecated in Swift but not in Objective-C say, this changes makes DocC include the deprecation summary only on the Swift version of the page. In addition, for backwards compatibility, if a symbol is not deprecated in any language, DocC will still include the deprecation summary.
1 parent 797bb78 commit 3e05bd7

File tree

6 files changed

+240
-14
lines changed

6 files changed

+240
-14
lines changed

Sources/SwiftDocC/Model/DocumentationNode.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,10 @@ public struct DocumentationNode {
223223
platformName: platformName
224224
) { mixins in
225225
mixins[SymbolGraph.Symbol.Availability.mixinKey] as? SymbolGraph.Symbol.Availability
226+
227+
// If the symbol graph doesn't provide availability data for this variant, hardcode it as `[]` so
228+
// that it doesn't get inferred from another variant.
229+
?? .init(availability: [])
226230
}
227231

228232
let endpointVariants = DocumentationDataVariants(

Sources/SwiftDocC/Model/Rendering/RenderNode/RenderNode+Codable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ extension RenderNode: Codable {
9595
try container.encodeIfPresent(sampleDownload, forKey: .sampleCodeDownload)
9696
try container.encodeIfPresent(downloadNotAvailableSummary, forKey: .downloadNotAvailableSummary)
9797

98-
try container.encodeVariantCollection(deprecationSummaryVariants, forKey: .deprecationSummary, encoder: encoder)
98+
try container.encodeVariantCollectionIfNotEmpty(deprecationSummaryVariants, forKey: .deprecationSummary, encoder: encoder)
9999

100100
try container.encodeIfPresent(diffAvailability, forKey: .diffAvailability)
101101
try container.encodeIfPresent(variants, forKey: .variants)

Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,8 +1238,16 @@ public struct RenderNodeTranslator: SemanticVisitor {
12381238

12391239
node.metadata.extendedModuleVariants = VariantCollection<String?>(from: symbol.extendedModuleVariants)
12401240

1241+
let defaultAvailability = defaultAvailability(for: bundle, moduleName: moduleName.symbolName, currentPlatforms: context.externalMetadata.currentPlatforms)?
1242+
.filter { $0.unconditionallyUnavailable != true }
1243+
.sorted(by: AvailabilityRenderOrder.compare)
1244+
12411245
node.metadata.platformsVariants = VariantCollection<[AvailabilityRenderItem]?>(from: symbol.availabilityVariants) { _, availability in
1242-
availability.availability
1246+
guard !availability.availability.isEmpty else {
1247+
return defaultAvailability
1248+
}
1249+
1250+
return availability.availability
12431251
.compactMap { availability -> AvailabilityRenderItem? in
12441252
// Filter items with insufficient availability data
12451253
guard availability.introducedVersion != nil else {
@@ -1255,11 +1263,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
12551263
}
12561264
.filter({ !($0.unconditionallyUnavailable == true) })
12571265
.sorted(by: AvailabilityRenderOrder.compare)
1258-
} ?? .init(defaultValue:
1259-
defaultAvailability(for: bundle, moduleName: moduleName.symbolName, currentPlatforms: context.externalMetadata.currentPlatforms)?
1260-
.filter({ !($0.unconditionallyUnavailable == true) })
1261-
.sorted(by: AvailabilityRenderOrder.compare)
1262-
)
1266+
} ?? .init(defaultValue: defaultAvailability)
12631267

12641268
if let availability = documentationNode.metadata?.availability, !availability.isEmpty {
12651269
let renderAvailability = availability.compactMap({
@@ -1648,12 +1652,39 @@ public struct RenderNodeTranslator: SemanticVisitor {
16481652
return seeAlsoSections
16491653
} ?? .init(defaultValue: [])
16501654

1651-
node.deprecationSummaryVariants = VariantCollection<[RenderBlockContent]?>(
1652-
from: symbol.deprecatedSummaryVariants
1653-
) { _, deprecatedSummary in
1654-
// If there is a deprecation summary in a documentation extension file add it to the render node
1655-
visitMarkupContainer(MarkupContainer(deprecatedSummary.content)) as? [RenderBlockContent]
1656-
} ?? .init(defaultValue: nil)
1655+
/// The set of traits in which the symbol is deprecated in at least one platform.
1656+
let traitsInWhichSymbolsIsDeprecated = documentationNode.availableVariantTraits.filter { trait in
1657+
guard let platforms = symbol.availabilityVariants[trait]?.availability else {
1658+
return false
1659+
}
1660+
1661+
return platforms.contains(where: { platform in
1662+
platform.deprecatedVersion != nil || platform.isUnconditionallyDeprecated
1663+
})
1664+
}
1665+
1666+
node.deprecationSummaryVariants = VariantCollection(
1667+
from: documentationNode.availableVariantTraits,
1668+
fallbackDefaultValue: nil,
1669+
transform: { trait in
1670+
if traitsInWhichSymbolsIsDeprecated.contains(trait) || traitsInWhichSymbolsIsDeprecated.isEmpty {
1671+
// It's possible for a symbol to only be deprecated in _some_ of its language representations
1672+
// In this case, only display the deprecation information for those language representations.
1673+
//
1674+
// Also, previous versions of DocC treated a page as deprecated if it had a custom `@DeprecationSummmary` description,
1675+
// even if the symbol wasn't deprecated. We preserve that behavior for backwards compatibility.
1676+
// TODO: Warn about using `@DeprecationSummmary` for non-deprecated pages,
1677+
// suggesting to use `@Available` to add deprecation information.
1678+
guard let deprecatedSummary = symbol.deprecatedSummaryVariants[trait] else {
1679+
return nil
1680+
}
1681+
1682+
return visitMarkupContainer(MarkupContainer(deprecatedSummary.content)) as? [RenderBlockContent]
1683+
}
1684+
1685+
return []
1686+
}
1687+
) ?? .init(defaultValue: nil)
16571688

16581689
collectedTopicReferences.append(contentsOf: contentCompiler.collectedTopicReferences)
16591690
node.references = createTopicRenderReferences()

Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorSymbolVariantsTests.swift

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,26 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase {
10241024
symbol.deprecatedSummaryVariants[.objectiveC] = DeprecatedSection(
10251025
text: "Objective-C Deprecation Variant"
10261026
)
1027+
1028+
// Explicitly mark this symbol as deprecated in both Swift and Objective-C,
1029+
// otherwise the deprecation summary is ignored.
1030+
symbol.availabilityVariants[.swift] = .init(
1031+
availability: [
1032+
.init(
1033+
domain: .init(rawValue: SymbolGraph.Symbol.Availability.Domain.macOS),
1034+
introducedVersion: .init(major: 15, minor: 0, patch: 0),
1035+
deprecatedVersion: .init(major: 15, minor: 1, patch: 0),
1036+
obsoletedVersion: nil,
1037+
message: nil,
1038+
renamed: nil,
1039+
isUnconditionallyDeprecated: false,
1040+
isUnconditionallyUnavailable: false,
1041+
willEventuallyBeDeprecated: false
1042+
)
1043+
]
1044+
)
1045+
1046+
symbol.availabilityVariants[.objectiveC] = symbol.availabilityVariants[.swift]
10271047
},
10281048
assertOriginalRenderNode: { renderNode in
10291049
XCTAssertEqual(
@@ -1040,6 +1060,77 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase {
10401060
)
10411061
}
10421062

1063+
/// Tests that APIs don't inherit platform availability from their variant in other languages.
1064+
///
1065+
/// The `DeprecatedInOneLanguageOnly` catalog defines a symbol `MyClass` which has availability
1066+
/// annotations in Swift but not in Objective-C. This test verifies that the Swift render node for `MyClass` does
1067+
/// indeed include availability information, but that the Objective-C one doesn't.
1068+
func testDoesNotInheritAvailabilityFromOtherLanguage() throws {
1069+
try assertMultiVariantSymbol(
1070+
bundleName: "DeprecatedInOneLanguageOnly",
1071+
assertOriginalRenderNode: { renderNode in
1072+
XCTAssert(renderNode.metadata.platforms?.isEmpty == false)
1073+
}, assertAfterApplyingVariant: { renderNode in
1074+
XCTAssertNil(
1075+
renderNode.metadata.platforms,
1076+
"Unexpectedly inherited documentation from the Swift symbol graph."
1077+
)
1078+
}
1079+
)
1080+
}
1081+
1082+
/// Tests that deprecation summaries only show up on variants of pages that are actually deprecated.
1083+
func testIncludesDeprecationSummaryOnlyInDeprecatedVariantOfSymbol() throws {
1084+
let deprecatedOnOnePlatform = SymbolGraph.Symbol.Availability.AvailabilityItem(
1085+
domain: .init(rawValue: SymbolGraph.Symbol.Availability.Domain.macOS),
1086+
introducedVersion: .init(major: 15, minor: 0, patch: 0),
1087+
deprecatedVersion: .init(major: 15, minor: 1, patch: 0),
1088+
obsoletedVersion: nil,
1089+
message: nil,
1090+
renamed: nil,
1091+
isUnconditionallyDeprecated: false,
1092+
isUnconditionallyUnavailable: false,
1093+
willEventuallyBeDeprecated: false
1094+
)
1095+
1096+
let unconditionallyDeprecated = SymbolGraph.Symbol.Availability.AvailabilityItem(
1097+
domain: .init(rawValue: SymbolGraph.Symbol.Availability.Domain.macOS),
1098+
introducedVersion: .init(major: 15, minor: 0, patch: 0),
1099+
deprecatedVersion: nil,
1100+
obsoletedVersion: nil,
1101+
message: nil,
1102+
renamed: nil,
1103+
isUnconditionallyDeprecated: true,
1104+
isUnconditionallyUnavailable: false,
1105+
willEventuallyBeDeprecated: false
1106+
)
1107+
1108+
for deprecatedAvailability in [deprecatedOnOnePlatform, unconditionallyDeprecated] {
1109+
try assertMultiVariantSymbol(
1110+
configureSymbol: { symbol in
1111+
symbol.deprecatedSummaryVariants[.swift] = DeprecatedSection(
1112+
text: "Deprecation summary"
1113+
)
1114+
1115+
symbol.availabilityVariants[.swift] = .init(
1116+
availability: [deprecatedAvailability]
1117+
)
1118+
1119+
// Explicitly remove availability information for the Objective-C variant of this symbol.
1120+
symbol.availabilityVariants[.objectiveC] = nil
1121+
},
1122+
assertOriginalRenderNode: { renderNode in
1123+
XCTAssertNotNil(renderNode.deprecationSummary)
1124+
}, assertAfterApplyingVariant: { renderNode in
1125+
XCTAssert(
1126+
renderNode.deprecationSummary?.isEmpty == true,
1127+
"Unexpectedly found a deprecation summary in the Objective-C variant of the page."
1128+
)
1129+
}
1130+
)
1131+
}
1132+
}
1133+
10431134
func testTopicRenderReferenceVariants() throws {
10441135
func myFunctionReference(in renderNode: RenderNode) throws -> TopicRenderReference {
10451136
return try XCTUnwrap(
@@ -1086,14 +1177,15 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase {
10861177
}
10871178

10881179
private func assertMultiVariantSymbol(
1180+
bundleName: String = "TestBundle",
10891181
configureContext: (DocumentationContext, ResolvedTopicReference) throws -> () = { _, _ in },
10901182
configureSymbol: (Symbol) throws -> () = { _ in },
10911183
configureRenderNodeTranslator: (inout RenderNodeTranslator) -> () = { _ in },
10921184
assertOriginalRenderNode: (RenderNode) throws -> (),
10931185
assertAfterApplyingVariant: (RenderNode) throws -> () = { _ in },
10941186
assertDataAfterApplyingVariant: (Data) throws -> () = { _ in }
10951187
) throws {
1096-
let (_, bundle, context) = try testBundleAndContext(copying: "TestBundle")
1188+
let (_, bundle, context) = try testBundleAndContext(copying: bundleName)
10971189

10981190
let identifier = ResolvedTopicReference(
10991191
bundleIdentifier: bundle.identifier,
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"metadata": {
3+
"formatVersion" : {
4+
"major" : 1
5+
},
6+
"generator" : "swift"
7+
},
8+
"module" : {
9+
"name" : "MyKit",
10+
"platform" : {
11+
"architecture" : "x86_64",
12+
"vendor" : "apple",
13+
"operatingSystem" : {
14+
"name" : "ios",
15+
"minimumVersion" : {
16+
"major" : 13,
17+
"minor" : 0,
18+
"patch" : 0
19+
}
20+
}
21+
}
22+
},
23+
"symbols" : [
24+
{
25+
"accessLevel" : "public",
26+
"kind" : {
27+
"identifier" : "swift.class",
28+
"displayName" : "Class"
29+
},
30+
"names" : {
31+
"title" : "MyClass"
32+
},
33+
"pathComponents" : [
34+
"MyClass"
35+
],
36+
"identifier" : {
37+
"precise" : "s:5MyKit0A5ClassC",
38+
"interfaceLanguage": "occ"
39+
}
40+
}
41+
],
42+
"relationships" : []
43+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"metadata": {
3+
"formatVersion" : {
4+
"major" : 1
5+
},
6+
"generator" : "swift"
7+
},
8+
"module" : {
9+
"name" : "MyKit",
10+
"platform" : {
11+
"architecture" : "x86_64",
12+
"vendor" : "apple",
13+
"operatingSystem" : {
14+
"name" : "ios",
15+
"minimumVersion" : {
16+
"major" : 13,
17+
"minor" : 0,
18+
"patch" : 0
19+
}
20+
}
21+
}
22+
},
23+
"symbols" : [
24+
{
25+
"accessLevel" : "public",
26+
"kind" : {
27+
"identifier" : "swift.class",
28+
"displayName" : "Class"
29+
},
30+
"names" : {
31+
"title" : "MyClass"
32+
},
33+
"availability" : [
34+
{
35+
"domain": "macOS",
36+
"introduced": {
37+
"major": 10,
38+
"minor": 15
39+
},
40+
"deprecated": {
41+
"major": 10,
42+
"minor": 16
43+
}
44+
}
45+
],
46+
"pathComponents" : [
47+
"MyClass"
48+
],
49+
"identifier" : {
50+
"precise" : "s:5MyKit0A5ClassC",
51+
"interfaceLanguage": "swift"
52+
}
53+
}
54+
],
55+
"relationships" : []
56+
}

0 commit comments

Comments
 (0)