Skip to content

Commit 34a872d

Browse files
authored
Compute beta platform information for non-symbol documentation (#959)
Fixes a bug where beta platform information was never being derived for non-symbol documentation, such as sample code articles. Sample code articles defined with `@Available` directives were never displaying a beta badge regardless of whether the platform was configured to be a beta platform. * Use `VersionTriplet` for `@Available` directive * Compute platform beta status when using `@Available` * Adds documentation for new `@Available` behavior * Add unit tests for new `@Available` behaviour * Add unit tests to `PlatformAvailabilityTests` for beta availability * Add diagnostic explanation for non-convertible directive arguments * Revert change to make `isBeta` a `let` property * Use if-expression to declare variables * Make assertions in `PlatformAvailabilityTests` clearer * Make `AvailabilityRenderItem.isBeta(introduced:current:)` private * Move to using `SemanticVersion` rather than `VersionTriplet` * Merge "deprecated" parameter changes with SemanticVersion changes * Update `@Available` documentation Resolves rdar://129355087.
1 parent f019ab8 commit 34a872d

12 files changed

+385
-85
lines changed

Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ public class DocumentationContentRenderer {
230230
}
231231

232232
// Verify that the current platform is in beta and the version number matches the introduced platform version.
233-
guard current.beta && introduced.isEqualToVersionTriplet(current.version) else {
233+
guard current.beta && SemanticVersion(introduced).isEqualToVersionTriplet(current.version) else {
234234
return false
235235
}
236236
}

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

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import Foundation
1212
import SymbolKit
1313

14-
extension SymbolGraph.SemanticVersion {
14+
extension SemanticVersion {
1515
enum Precision: Int {
1616
case all = 0, patch, minor
1717

@@ -45,6 +45,14 @@ extension SymbolGraph.SemanticVersion {
4545
.joined(separator: ".")
4646
}
4747

48+
init(_ semanticVersion: SymbolGraph.SemanticVersion) {
49+
self.major = semanticVersion.major
50+
self.minor = semanticVersion.minor
51+
self.patch = semanticVersion.patch
52+
self.prerelease = semanticVersion.prerelease
53+
self.buildMetadata = semanticVersion.buildMetadata
54+
}
55+
4856
/// Compares a version triplet to a semantic version.
4957
/// - Parameter version: A version triplet to compare to this semantic version.
5058
/// - Returns: Returns whether the given triple represents the same version as the current version.
@@ -125,29 +133,32 @@ public struct AvailabilityRenderItem: Codable, Hashable, Equatable {
125133
init(_ availability: SymbolGraph.Symbol.Availability.AvailabilityItem, current: PlatformVersion?) {
126134
let platformName = availability.domain.map({ PlatformName(operatingSystemName: $0.rawValue) })
127135
name = platformName?.displayName
128-
introduced = availability.introducedVersion?.stringRepresentation(precisionUpToNonsignificant: .minor)
129-
deprecated = availability.deprecatedVersion?.stringRepresentation(precisionUpToNonsignificant: .minor)
130-
obsoleted = availability.obsoletedVersion?.stringRepresentation(precisionUpToNonsignificant: .minor)
136+
137+
let introducedVersion = availability.introducedVersion.flatMap { SemanticVersion($0) }
138+
introduced = introducedVersion?.stringRepresentation(precisionUpToNonsignificant: .minor)
139+
deprecated = availability.deprecatedVersion.flatMap { SemanticVersion($0).stringRepresentation(precisionUpToNonsignificant: .minor) }
140+
obsoleted = availability.obsoletedVersion.flatMap { SemanticVersion($0).stringRepresentation(precisionUpToNonsignificant: .minor) }
131141
message = availability.message
132142
renamed = availability.renamed
133143
unconditionallyUnavailable = availability.isUnconditionallyUnavailable
134144
unconditionallyDeprecated = availability.isUnconditionallyDeprecated
135-
136-
if let introducedVersion = availability.introducedVersion, let current, current.beta, introducedVersion.isEqualToVersionTriplet(current.version) {
137-
isBeta = true
138-
} else {
139-
isBeta = false
140-
}
145+
isBeta = AvailabilityRenderItem.isBeta(introduced: introducedVersion, current: current)
141146
}
142147

143148
init?(_ availability: Metadata.Availability, current: PlatformVersion?) {
144-
// FIXME: Deprecated/Beta markings need platform versions to display properly in Swift-DocC-Render (rdar://56897597)
145-
// Fill in the appropriate values here when that's fixed (https://github.com/apple/swift-docc/issues/441)
146-
147149
let platformName = PlatformName(metadataPlatform: availability.platform)
148150
name = platformName?.displayName
149-
introduced = availability.introduced
150-
deprecated = availability.deprecated
151+
introduced = availability.introduced.stringRepresentation(precisionUpToNonsignificant: .minor)
152+
deprecated = availability.deprecated.flatMap { $0.stringRepresentation(precisionUpToNonsignificant: .minor) }
153+
isBeta = AvailabilityRenderItem.isBeta(introduced: availability.introduced, current: current)
154+
}
155+
156+
private static func isBeta(introduced: SemanticVersion?, current: PlatformVersion?) -> Bool {
157+
guard let introduced, let current, current.beta, introduced.isEqualToVersionTriplet(current.version) else {
158+
return false
159+
}
160+
161+
return true
151162
}
152163

153164
/// Creates a new item with the given platform name and version string.

Sources/SwiftDocC/Semantics/DirectiveInfrastructure/AutomaticDirectiveConvertible.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ extension AutomaticDirectiveConvertible {
157157
severityIfNotFound: reflectedArgument.required ? .warning : nil,
158158
argumentName: reflectedArgument.name,
159159
allowedValues: reflectedArgument.allowedValues,
160+
expectedFormat: reflectedArgument.expectedFormat,
160161
convert: { argumentValue in
161162
return reflectedArgument.parseArgument(bundle, argumentValue)
162163
},

Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveArgumentValueConvertible.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ protocol DirectiveArgumentValueConvertible {
1616
init?(rawDirectiveArgumentValue: String)
1717

1818
static func allowedValues() -> [String]?
19+
static func expectedFormat() -> String?
20+
}
21+
22+
extension DirectiveArgumentValueConvertible {
23+
static func expectedFormat() -> String? {
24+
return nil
25+
}
1926
}
2027

2128
extension RawRepresentable where Self: DirectiveArgumentValueConvertible, RawValue == String {

Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveArgumentWrapper.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ protocol _DirectiveArgumentProtocol {
1616
var required: Bool { get }
1717
var name: _DirectiveArgumentName { get }
1818
var allowedValues: [String]? { get }
19+
var expectedFormat: String? { get }
1920
var hiddenFromDocumentation: Bool { get }
2021

2122
var parseArgument: (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Any?) { get }
@@ -64,6 +65,7 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
6465
let name: _DirectiveArgumentName
6566
let typeDisplayName: String
6667
let allowedValues: [String]?
68+
let expectedFormat: String?
6769
let hiddenFromDocumentation: Bool
6870

6971
let parseArgument: (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Any?)
@@ -99,13 +101,15 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
99101
name: _DirectiveArgumentName = .inferredFromPropertyName,
100102
parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?),
101103
allowedValues: [String]? = nil,
104+
expectedFormat: String? = nil,
102105
hiddenFromDocumentation: Bool = false
103106
) {
104107
self.init(
105108
value: wrappedValue,
106109
name: name,
107110
transform: parseArgument,
108111
allowedValues: allowedValues,
112+
expectedFormat: expectedFormat,
109113
required: nil,
110114
hiddenFromDocumentation: hiddenFromDocumentation
111115
)
@@ -116,13 +120,15 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
116120
name: _DirectiveArgumentName = .inferredFromPropertyName,
117121
parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?),
118122
allowedValues: [String]? = nil,
123+
expectedFormat: String? = nil,
119124
hiddenFromDocumentation: Bool = false
120125
) {
121126
self.init(
122127
value: nil,
123128
name: name,
124129
transform: parseArgument,
125130
allowedValues: allowedValues,
131+
expectedFormat: expectedFormat,
126132
required: nil,
127133
hiddenFromDocumentation: hiddenFromDocumentation
128134
)
@@ -133,6 +139,7 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
133139
name: _DirectiveArgumentName,
134140
transform: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?),
135141
allowedValues: [String]?,
142+
expectedFormat: String?,
136143
required: Bool?,
137144
hiddenFromDocumentation: Bool
138145
) {
@@ -143,6 +150,7 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
143150
self.typeDisplayName = typeDisplayNameDescription(defaultValue: value, required: required)
144151
self.parseArgument = transform
145152
self.allowedValues = allowedValues
153+
self.expectedFormat = expectedFormat
146154
self.required = required
147155
self.hiddenFromDocumentation = hiddenFromDocumentation
148156
}
@@ -166,6 +174,7 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
166174
name: _DirectiveArgumentName = .inferredFromPropertyName,
167175
parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?),
168176
allowedValues: [String]? = nil,
177+
expectedFormat: String? = nil,
169178
required: Bool,
170179
hiddenFromDocumentation: Bool = false
171180
) {
@@ -174,6 +183,7 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
174183
name: name,
175184
transform: parseArgument,
176185
allowedValues: allowedValues,
186+
expectedFormat: expectedFormat,
177187
required: required,
178188
hiddenFromDocumentation: hiddenFromDocumentation
179189
)
@@ -185,6 +195,7 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
185195
name: _DirectiveArgumentName = .inferredFromPropertyName,
186196
parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?),
187197
allowedValues: [String]? = nil,
198+
expectedFormat: String? = nil,
188199
required: Bool,
189200
hiddenFromDocumentation: Bool = false
190201
) {
@@ -193,6 +204,7 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
193204
name: name,
194205
transform: parseArgument,
195206
allowedValues: allowedValues,
207+
expectedFormat: expectedFormat,
196208
required: required,
197209
hiddenFromDocumentation: hiddenFromDocumentation
198210
)
@@ -229,6 +241,7 @@ extension DirectiveArgumentWrapped where Value: DirectiveArgumentValueConvertibl
229241
Value.init(rawDirectiveArgumentValue: argument)
230242
}
231243
self.allowedValues = Value.allowedValues()
244+
self.expectedFormat = Value.expectedFormat()
232245
self.required = required
233246
self.hiddenFromDocumentation = hiddenFromDocumentation
234247
}
@@ -335,13 +348,15 @@ extension DirectiveArgumentWrapped where Value: _OptionalDirectiveArgument {
335348
name: _DirectiveArgumentName,
336349
parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?),
337350
allowedValues: [String]? = nil,
351+
expectedFormat: String? = nil,
338352
hiddenFromDocumentation: Bool = false
339353
) {
340354
self.name = name
341355
self.defaultValue = value
342356
self.typeDisplayName = typeDisplayNameDescription(optionalDefaultValue: value, required: false)
343357
self.parseArgument = parseArgument
344358
self.allowedValues = allowedValues
359+
self.expectedFormat = expectedFormat
345360
self.required = false
346361
self.hiddenFromDocumentation = hiddenFromDocumentation
347362
}

Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveMirror.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ struct DirectiveMirror {
5959
name: argumentName,
6060
unnamed: unnamed,
6161
allowedValues: argument.allowedValues,
62+
expectedFormat: argument.expectedFormat,
6263
propertyLabel: label,
6364
argument: argument,
6465
parseArgument: argument.parseArgument
@@ -166,6 +167,7 @@ extension DirectiveMirror {
166167
let unnamed: Bool
167168

168169
let allowedValues: [String]?
170+
let expectedFormat: String?
169171

170172
let propertyLabel: String
171173
let argument: _DirectiveArgumentProtocol

Sources/SwiftDocC/Semantics/General Purpose Analyses/HasArgumentOfType.swift

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,19 @@ protocol DirectiveArgument<ArgumentValue> {
1919
/// If non-`nil`, the list of allowed values the argument can take on,
2020
/// suggested to the author as possible solutions
2121
static func allowedValues() -> [String]?
22+
23+
/// If non-`nil`, a string describing the expected format for the argument value,
24+
/// shown to the author as part of the diagnostic summary when an invalid value is provided.
25+
static func expectedFormat() -> String?
2226
}
2327

2428
extension DirectiveArgument {
2529
static func allowedValues() -> [String]? {
2630
return nil
2731
}
32+
static func expectedFormat() -> String? {
33+
return nil
34+
}
2835
static func convert(_ argument: String) -> ArgumentValue? {
2936
return ArgumentValue(rawDirectiveArgumentValue: argument)
3037
}
@@ -52,6 +59,7 @@ extension Semantic.Analyses {
5259
severityIfNotFound: severityIfNotFound,
5360
argumentName: Converter.argumentName,
5461
allowedValues: Converter.allowedValues(),
62+
expectedFormat: Converter.expectedFormat(),
5563
convert: Converter.convert(_:),
5664
valueTypeDiagnosticName: String(describing: Converter.ArgumentValue.self)
5765
).analyze(directive, arguments: arguments, problems: &problems) as? Converter.ArgumentValue
@@ -62,6 +70,7 @@ extension Semantic.Analyses {
6270
let severityIfNotFound: DiagnosticSeverity?
6371
let argumentName: String
6472
let allowedValues: [String]?
73+
let expectedFormat: String?
6574
let convert: (String) -> (Any?)
6675
let valueTypeDiagnosticName: String
6776

@@ -73,33 +82,45 @@ extension Semantic.Analyses {
7382
let arguments = directive.arguments(problems: &problems)
7483
let source = directive.range?.lowerBound.source
7584
let diagnosticArgumentName = argumentName.isEmpty ? "unlabeled" : argumentName
76-
85+
let diagnosticArgumentDescription = if argumentName.isEmpty {
86+
"an unnamed parameter"
87+
} else {
88+
"the \(argumentName.singleQuoted) parameter"
89+
}
90+
let diagnosticExplanation = if let expectedFormat {
91+
"""
92+
\(Parent.directiveName) expects an argument for \(diagnosticArgumentDescription) \
93+
that's convertible to \(expectedFormat)
94+
"""
95+
} else {
96+
"""
97+
\(Parent.directiveName) expects an argument for \(diagnosticArgumentDescription) \
98+
that's convertible to \(valueTypeDiagnosticName.singleQuoted)
99+
"""
100+
}
77101
guard let argument = arguments[argumentName] else {
78102
if let severity = severityIfNotFound {
79-
let argumentDiagnosticDescription: String
80-
if argumentName.isEmpty {
81-
argumentDiagnosticDescription = "an unnamed parameter"
82-
} else {
83-
argumentDiagnosticDescription = "the \(argumentName.singleQuoted) parameter"
84-
}
85-
86103
let diagnostic = Diagnostic(
87104
source: source,
88105
severity: severity,
89106
range: directive.range,
90107
identifier: "org.swift.docc.HasArgument.\(diagnosticArgumentName)",
91108
summary: "Missing argument for \(diagnosticArgumentName) parameter",
92-
explanation: """
93-
\(Parent.directiveName) expects an argument for \(argumentDiagnosticDescription) \
94-
that's convertible to \(valueTypeDiagnosticName.singleQuoted)
95-
"""
109+
explanation: diagnosticExplanation
96110
)
97111
problems.append(Problem(diagnostic: diagnostic, possibleSolutions: []))
98112
}
99113
return nil
100114
}
101115
guard let value = convert(argument.value) else {
102-
let diagnostic = Diagnostic(source: source, severity: .warning, range: argument.valueRange, identifier: "org.swift.docc.HasArgument.\(diagnosticArgumentName).ConversionFailed", summary: "Cannot convert \(argument.value.singleQuoted) to type \(valueTypeDiagnosticName.singleQuoted)")
116+
let diagnostic = Diagnostic(
117+
source: source,
118+
severity: .warning,
119+
range: argument.valueRange,
120+
identifier: "org.swift.docc.HasArgument.\(diagnosticArgumentName).ConversionFailed",
121+
summary: "Cannot convert \(argument.value.singleQuoted) to type \(valueTypeDiagnosticName.singleQuoted)",
122+
explanation: diagnosticExplanation
123+
)
103124
let solutions = allowedValues.map { allowedValues -> [Solution] in
104125
return allowedValues.compactMap { allowedValue -> Solution? in
105126
guard let range = argument.valueRange else {

0 commit comments

Comments
 (0)