Skip to content

Commit

Permalink
Compute beta platform information for non-symbol documentation (#959) (
Browse files Browse the repository at this point in the history
…#972)

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.
  • Loading branch information
anferbui authored Jul 4, 2024
1 parent 422e9a5 commit d49f1ff
Show file tree
Hide file tree
Showing 12 changed files with 385 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ public class DocumentationContentRenderer {
}

// Verify that the current platform is in beta and the version number matches the introduced platform version.
guard current.beta && introduced.isEqualToVersionTriplet(current.version) else {
guard current.beta && SemanticVersion(introduced).isEqualToVersionTriplet(current.version) else {
return false
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import Foundation
import SymbolKit

extension SymbolGraph.SemanticVersion {
extension SemanticVersion {
enum Precision: Int {
case all = 0, patch, minor

Expand Down Expand Up @@ -45,6 +45,14 @@ extension SymbolGraph.SemanticVersion {
.joined(separator: ".")
}

init(_ semanticVersion: SymbolGraph.SemanticVersion) {
self.major = semanticVersion.major
self.minor = semanticVersion.minor
self.patch = semanticVersion.patch
self.prerelease = semanticVersion.prerelease
self.buildMetadata = semanticVersion.buildMetadata
}

/// Compares a version triplet to a semantic version.
/// - Parameter version: A version triplet to compare to this semantic version.
/// - Returns: Returns whether the given triple represents the same version as the current version.
Expand Down Expand Up @@ -125,29 +133,32 @@ public struct AvailabilityRenderItem: Codable, Hashable, Equatable {
init(_ availability: SymbolGraph.Symbol.Availability.AvailabilityItem, current: PlatformVersion?) {
let platformName = availability.domain.map({ PlatformName(operatingSystemName: $0.rawValue) })
name = platformName?.displayName
introduced = availability.introducedVersion?.stringRepresentation(precisionUpToNonsignificant: .minor)
deprecated = availability.deprecatedVersion?.stringRepresentation(precisionUpToNonsignificant: .minor)
obsoleted = availability.obsoletedVersion?.stringRepresentation(precisionUpToNonsignificant: .minor)

let introducedVersion = availability.introducedVersion.flatMap { SemanticVersion($0) }
introduced = introducedVersion?.stringRepresentation(precisionUpToNonsignificant: .minor)
deprecated = availability.deprecatedVersion.flatMap { SemanticVersion($0).stringRepresentation(precisionUpToNonsignificant: .minor) }
obsoleted = availability.obsoletedVersion.flatMap { SemanticVersion($0).stringRepresentation(precisionUpToNonsignificant: .minor) }
message = availability.message
renamed = availability.renamed
unconditionallyUnavailable = availability.isUnconditionallyUnavailable
unconditionallyDeprecated = availability.isUnconditionallyDeprecated

if let introducedVersion = availability.introducedVersion, let current, current.beta, introducedVersion.isEqualToVersionTriplet(current.version) {
isBeta = true
} else {
isBeta = false
}
isBeta = AvailabilityRenderItem.isBeta(introduced: introducedVersion, current: current)
}

init?(_ availability: Metadata.Availability, current: PlatformVersion?) {
// FIXME: Deprecated/Beta markings need platform versions to display properly in Swift-DocC-Render (rdar://56897597)
// Fill in the appropriate values here when that's fixed (https://github.com/apple/swift-docc/issues/441)

let platformName = PlatformName(metadataPlatform: availability.platform)
name = platformName?.displayName
introduced = availability.introduced
deprecated = availability.deprecated
introduced = availability.introduced.stringRepresentation(precisionUpToNonsignificant: .minor)
deprecated = availability.deprecated.flatMap { $0.stringRepresentation(precisionUpToNonsignificant: .minor) }
isBeta = AvailabilityRenderItem.isBeta(introduced: availability.introduced, current: current)
}

private static func isBeta(introduced: SemanticVersion?, current: PlatformVersion?) -> Bool {
guard let introduced, let current, current.beta, introduced.isEqualToVersionTriplet(current.version) else {
return false
}

return true
}

/// Creates a new item with the given platform name and version string.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ extension AutomaticDirectiveConvertible {
severityIfNotFound: reflectedArgument.required ? .warning : nil,
argumentName: reflectedArgument.name,
allowedValues: reflectedArgument.allowedValues,
expectedFormat: reflectedArgument.expectedFormat,
convert: { argumentValue in
return reflectedArgument.parseArgument(bundle, argumentValue)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ protocol DirectiveArgumentValueConvertible {
init?(rawDirectiveArgumentValue: String)

static func allowedValues() -> [String]?
static func expectedFormat() -> String?
}

extension DirectiveArgumentValueConvertible {
static func expectedFormat() -> String? {
return nil
}
}

extension RawRepresentable where Self: DirectiveArgumentValueConvertible, RawValue == String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ protocol _DirectiveArgumentProtocol {
var required: Bool { get }
var name: _DirectiveArgumentName { get }
var allowedValues: [String]? { get }
var expectedFormat: String? { get }
var hiddenFromDocumentation: Bool { get }

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

let parseArgument: (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Any?)
Expand Down Expand Up @@ -99,13 +101,15 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
name: _DirectiveArgumentName = .inferredFromPropertyName,
parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?),
allowedValues: [String]? = nil,
expectedFormat: String? = nil,
hiddenFromDocumentation: Bool = false
) {
self.init(
value: wrappedValue,
name: name,
transform: parseArgument,
allowedValues: allowedValues,
expectedFormat: expectedFormat,
required: nil,
hiddenFromDocumentation: hiddenFromDocumentation
)
Expand All @@ -116,13 +120,15 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
name: _DirectiveArgumentName = .inferredFromPropertyName,
parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?),
allowedValues: [String]? = nil,
expectedFormat: String? = nil,
hiddenFromDocumentation: Bool = false
) {
self.init(
value: nil,
name: name,
transform: parseArgument,
allowedValues: allowedValues,
expectedFormat: expectedFormat,
required: nil,
hiddenFromDocumentation: hiddenFromDocumentation
)
Expand All @@ -133,6 +139,7 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
name: _DirectiveArgumentName,
transform: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?),
allowedValues: [String]?,
expectedFormat: String?,
required: Bool?,
hiddenFromDocumentation: Bool
) {
Expand All @@ -143,6 +150,7 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
self.typeDisplayName = typeDisplayNameDescription(defaultValue: value, required: required)
self.parseArgument = transform
self.allowedValues = allowedValues
self.expectedFormat = expectedFormat
self.required = required
self.hiddenFromDocumentation = hiddenFromDocumentation
}
Expand All @@ -166,6 +174,7 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
name: _DirectiveArgumentName = .inferredFromPropertyName,
parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?),
allowedValues: [String]? = nil,
expectedFormat: String? = nil,
required: Bool,
hiddenFromDocumentation: Bool = false
) {
Expand All @@ -174,6 +183,7 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
name: name,
transform: parseArgument,
allowedValues: allowedValues,
expectedFormat: expectedFormat,
required: required,
hiddenFromDocumentation: hiddenFromDocumentation
)
Expand All @@ -185,6 +195,7 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
name: _DirectiveArgumentName = .inferredFromPropertyName,
parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?),
allowedValues: [String]? = nil,
expectedFormat: String? = nil,
required: Bool,
hiddenFromDocumentation: Bool = false
) {
Expand All @@ -193,6 +204,7 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
name: name,
transform: parseArgument,
allowedValues: allowedValues,
expectedFormat: expectedFormat,
required: required,
hiddenFromDocumentation: hiddenFromDocumentation
)
Expand Down Expand Up @@ -229,6 +241,7 @@ extension DirectiveArgumentWrapped where Value: DirectiveArgumentValueConvertibl
Value.init(rawDirectiveArgumentValue: argument)
}
self.allowedValues = Value.allowedValues()
self.expectedFormat = Value.expectedFormat()
self.required = required
self.hiddenFromDocumentation = hiddenFromDocumentation
}
Expand Down Expand Up @@ -335,13 +348,15 @@ extension DirectiveArgumentWrapped where Value: _OptionalDirectiveArgument {
name: _DirectiveArgumentName,
parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?),
allowedValues: [String]? = nil,
expectedFormat: String? = nil,
hiddenFromDocumentation: Bool = false
) {
self.name = name
self.defaultValue = value
self.typeDisplayName = typeDisplayNameDescription(optionalDefaultValue: value, required: false)
self.parseArgument = parseArgument
self.allowedValues = allowedValues
self.expectedFormat = expectedFormat
self.required = false
self.hiddenFromDocumentation = hiddenFromDocumentation
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ struct DirectiveMirror {
name: argumentName,
unnamed: unnamed,
allowedValues: argument.allowedValues,
expectedFormat: argument.expectedFormat,
propertyLabel: label,
argument: argument,
parseArgument: argument.parseArgument
Expand Down Expand Up @@ -166,6 +167,7 @@ extension DirectiveMirror {
let unnamed: Bool

let allowedValues: [String]?
let expectedFormat: String?

let propertyLabel: String
let argument: _DirectiveArgumentProtocol
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,19 @@ protocol DirectiveArgument<ArgumentValue> {
/// If non-`nil`, the list of allowed values the argument can take on,
/// suggested to the author as possible solutions
static func allowedValues() -> [String]?

/// If non-`nil`, a string describing the expected format for the argument value,
/// shown to the author as part of the diagnostic summary when an invalid value is provided.
static func expectedFormat() -> String?
}

extension DirectiveArgument {
static func allowedValues() -> [String]? {
return nil
}
static func expectedFormat() -> String? {
return nil
}
static func convert(_ argument: String) -> ArgumentValue? {
return ArgumentValue(rawDirectiveArgumentValue: argument)
}
Expand Down Expand Up @@ -52,6 +59,7 @@ extension Semantic.Analyses {
severityIfNotFound: severityIfNotFound,
argumentName: Converter.argumentName,
allowedValues: Converter.allowedValues(),
expectedFormat: Converter.expectedFormat(),
convert: Converter.convert(_:),
valueTypeDiagnosticName: String(describing: Converter.ArgumentValue.self)
).analyze(directive, arguments: arguments, problems: &problems) as? Converter.ArgumentValue
Expand All @@ -62,6 +70,7 @@ extension Semantic.Analyses {
let severityIfNotFound: DiagnosticSeverity?
let argumentName: String
let allowedValues: [String]?
let expectedFormat: String?
let convert: (String) -> (Any?)
let valueTypeDiagnosticName: String

Expand All @@ -73,33 +82,45 @@ extension Semantic.Analyses {
let arguments = directive.arguments(problems: &problems)
let source = directive.range?.lowerBound.source
let diagnosticArgumentName = argumentName.isEmpty ? "unlabeled" : argumentName

let diagnosticArgumentDescription = if argumentName.isEmpty {
"an unnamed parameter"
} else {
"the \(argumentName.singleQuoted) parameter"
}
let diagnosticExplanation = if let expectedFormat {
"""
\(Parent.directiveName) expects an argument for \(diagnosticArgumentDescription) \
that's convertible to \(expectedFormat)
"""
} else {
"""
\(Parent.directiveName) expects an argument for \(diagnosticArgumentDescription) \
that's convertible to \(valueTypeDiagnosticName.singleQuoted)
"""
}
guard let argument = arguments[argumentName] else {
if let severity = severityIfNotFound {
let argumentDiagnosticDescription: String
if argumentName.isEmpty {
argumentDiagnosticDescription = "an unnamed parameter"
} else {
argumentDiagnosticDescription = "the \(argumentName.singleQuoted) parameter"
}

let diagnostic = Diagnostic(
source: source,
severity: severity,
range: directive.range,
identifier: "org.swift.docc.HasArgument.\(diagnosticArgumentName)",
summary: "Missing argument for \(diagnosticArgumentName) parameter",
explanation: """
\(Parent.directiveName) expects an argument for \(argumentDiagnosticDescription) \
that's convertible to \(valueTypeDiagnosticName.singleQuoted)
"""
explanation: diagnosticExplanation
)
problems.append(Problem(diagnostic: diagnostic, possibleSolutions: []))
}
return nil
}
guard let value = convert(argument.value) else {
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)")
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)",
explanation: diagnosticExplanation
)
let solutions = allowedValues.map { allowedValues -> [Solution] in
return allowedValues.compactMap { allowedValue -> Solution? in
guard let range = argument.valueRange else {
Expand Down
Loading

0 comments on commit d49f1ff

Please sign in to comment.