Skip to content

Commit

Permalink
add @Available directive for setting platform availability
Browse files Browse the repository at this point in the history
rdar://57847232
  • Loading branch information
QuietMisdreavus committed Dec 19, 2022
1 parent 150eb7d commit 8d4243a
Show file tree
Hide file tree
Showing 13 changed files with 1,182 additions and 2 deletions.
34 changes: 34 additions & 0 deletions Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,23 @@ public struct RenderNodeTranslator: SemanticVisitor {
))
}
}

if let availability = article.metadata?.availability, !availability.isEmpty {
let renderAvailability = availability.compactMap({
let currentPlatform: PlatformVersion?
if let name = PlatformName(metadataPlatform: $0.platform),
let contextPlatform = context.externalMetadata.currentPlatforms?[name.displayName] {
currentPlatform = contextPlatform
} else {
currentPlatform = nil
}
return .init($0, current: currentPlatform)
}).sorted(by: AvailabilityRenderOrder.compare)

if !renderAvailability.isEmpty {
node.metadata.platformsVariants = .init(defaultValue: renderAvailability)
}
}

collectedTopicReferences.append(contentsOf: contentCompiler.collectedTopicReferences)
node.references = createTopicRenderReferences()
Expand Down Expand Up @@ -1193,6 +1210,23 @@ public struct RenderNodeTranslator: SemanticVisitor {
.filter({ !($0.unconditionallyUnavailable == true) })
.sorted(by: AvailabilityRenderOrder.compare)
)

if let availability = documentationNode.metadata?.availability, !availability.isEmpty {
let renderAvailability = availability.compactMap({
let currentPlatform: PlatformVersion?
if let name = PlatformName(metadataPlatform: $0.platform),
let contextPlatform = context.externalMetadata.currentPlatforms?[name.displayName] {
currentPlatform = contextPlatform
} else {
currentPlatform = nil
}
return .init($0, current: currentPlatform)
}).sorted(by: AvailabilityRenderOrder.compare)

if !renderAvailability.isEmpty {
node.metadata.platformsVariants.defaultValue = renderAvailability
}
}

node.metadata.requiredVariants = VariantCollection<Bool>(from: symbol.isRequiredVariants) ?? .init(defaultValue: false)
node.metadata.role = contentRenderer.role(for: documentationNode.kind).rawValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,23 @@ public struct AvailabilityRenderItem: Codable, Hashable, Equatable {
isBeta = false
}
}

init?(_ availability: MetadataAvailability, current: PlatformVersion?) {
if availability.introduced == nil {
// 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 (issue link forthcoming)
return nil
}

let platformName: PlatformName?
if availability.platform == .any {
platformName = nil
} else {
platformName = PlatformName(operatingSystemName: availability.platform.rawValue)
}
name = platformName?.displayName
introduced = availability.introduced
}

/// Creates a new item with the given platform name and version string.
/// - Parameters:
Expand Down
138 changes: 138 additions & 0 deletions Sources/SwiftDocC/Semantics/Metadata/Availability.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2022 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation
import Markdown

/// A directive that sets the platform availability information for a documentation page.
///
/// `@Available` is analagous to the `@available` attribute in Swift: It allows you to specify a
/// platform version that the page relates to. To specify a platform and version, list the platform
/// name and use the `introduced` argument:
///
/// ```markdown
/// @Available(macOS, introduced: "12.0")
/// ```
///
/// The available platforms are `macOS`, `iOS`, `watchOS`, and `tvOS`.
///
/// This directive is available on both articles and documentation extension files. In extension
/// files, the information overrides any information from the symbol itself.
///
/// This directive is only valid within a ``Metadata`` directive:
///
/// ```markdown
/// @Metadata {
/// @Available(macOS, introduced: "12.0")
/// }
/// ```
public final class MetadataAvailability: Semantic, AutomaticDirectiveConvertible {
static public let directiveName: String = "Available"

public enum Platform: String, RawRepresentable, CaseIterable, DirectiveArgumentValueConvertible {
case any = "*"
case macOS, iOS, watchOS, tvOS

public init?(rawValue: String) {
for platform in Self.allCases {
if platform.rawValue.lowercased() == rawValue.lowercased() {
self = platform
return
}
}
return nil
}
}

/// The platform that this argument's information applies to.
@DirectiveArgumentWrapped(name: .unnamed)
public var platform: Platform = .any

/// The platform version that this page applies to.
@DirectiveArgumentWrapped
public var introduced: String? = nil

/// Whether to mark this page as "Deprecated".
@DirectiveArgumentWrapped
public var isDeprecated: Bool = false

/// Whether to mark this page as "Beta".
@DirectiveArgumentWrapped
public var isBeta: Bool = false

static var keyPaths: [String : AnyKeyPath] = [
"platform" : \MetadataAvailability._platform,
"introduced" : \MetadataAvailability._introduced,
"isDeprecated" : \MetadataAvailability._isDeprecated,
"isBeta" : \MetadataAvailability._isBeta,
]

func validate(
source: URL?,
for bundle: DocumentationBundle,
in context: DocumentationContext,
problems: inout [Problem]
) -> Bool {
var isValid = true

if platform == .any && introduced != nil {
problems.append(.init(diagnostic: .init(
source: source,
severity: .warning,
range: originalMarkup.range,
identifier: "org.swift.docc.\(MetadataAvailability.self).introducedVersionForAllPlatforms",
summary: "\(MetadataAvailability.directiveName.singleQuoted) directive requires a platform with the `introduced` argument"
)))

isValid = false
}

if platform == .any && introduced == nil && isDeprecated == false && isBeta == false {
problems.append(.init(diagnostic: .init(
source: source,
severity: .warning,
range: originalMarkup.range,
identifier: "org.swift.docc.\(MetadataAvailability.self).emptyAttribute",
summary: "\(MetadataAvailability.directiveName.singleQuoted) directive requires a platform and `introduced` argument, or an `isDeprecated` or `isBeta` argument"
)))

isValid = false
}

if isDeprecated {
problems.append(.init(diagnostic: .init(
source: source,
severity: .information,
range: originalMarkup.range,
identifier: "org.swift.docc.\(MetadataAvailability.self).unusedDeprecated",
summary: "\(MetadataAvailability.directiveName.singleQuoted) `isDeprecated` argument is currently unused"
)))
}

if isBeta {
problems.append(.init(diagnostic: .init(
source: source,
severity: .information,
range: originalMarkup.range,
identifier: "org.swift.docc.\(MetadataAvailability.self).unusedBeta",
summary: "\(MetadataAvailability.directiveName.singleQuoted) `isBeta` argument is currently unused"
)))
}

return isValid
}

public let originalMarkup: Markdown.BlockDirective

@available(*, deprecated, message: "Do not call directly. Required for 'AutomaticDirectiveConvertible'.")
init(originalMarkup: Markdown.BlockDirective) {
self.originalMarkup = originalMarkup
}
}
108 changes: 107 additions & 1 deletion Sources/SwiftDocC/Semantics/Metadata/Metadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import Markdown
/// - ``DisplayName``
/// - ``PageImage``
/// - ``CallToAction``
/// - ``MetadataAvailability``
public final class Metadata: Semantic, AutomaticDirectiveConvertible {
public let originalMarkup: BlockDirective

Expand All @@ -48,6 +49,9 @@ public final class Metadata: Semantic, AutomaticDirectiveConvertible {

@ChildDirective
var callToAction: CallToAction? = nil

@ChildDirective(requirements: .zeroOrMore)
var availability: [MetadataAvailability]

static var keyPaths: [String : AnyKeyPath] = [
"documentationOptions" : \Metadata._documentationOptions,
Expand All @@ -56,6 +60,7 @@ public final class Metadata: Semantic, AutomaticDirectiveConvertible {
"pageImages" : \Metadata._pageImages,
"customMetadata" : \Metadata._customMetadata,
"callToAction" : \Metadata._callToAction,
"availability" : \Metadata._availability,
]

/// Creates a metadata object with a given markup, documentation extension, and technology root.
Expand All @@ -78,7 +83,7 @@ public final class Metadata: Semantic, AutomaticDirectiveConvertible {

func validate(source: URL?, for bundle: DocumentationBundle, in context: DocumentationContext, problems: inout [Problem]) -> Bool {
// Check that something is configured in the metadata block
if documentationOptions == nil && technologyRoot == nil && displayName == nil && pageImages.isEmpty && customMetadata.isEmpty {
if documentationOptions == nil && technologyRoot == nil && displayName == nil && pageImages.isEmpty && customMetadata.isEmpty && callToAction == nil && availability.isEmpty {
let diagnostic = Diagnostic(
source: source,
severity: .information,
Expand Down Expand Up @@ -132,6 +137,107 @@ public final class Metadata: Semantic, AutomaticDirectiveConvertible {
problems.append(Problem(diagnostic: diagnostic, possibleSolutions: [solution]))
}
}

var categorizedAvailability = [MetadataAvailability.Platform : [MetadataAvailability]]()
for availability in availability {
categorizedAvailability[availability.platform, default: []].append(availability)
}

for availabilityAttrs in categorizedAvailability.values {
guard availabilityAttrs.count > 1 else {
continue
}

let duplicateIntroduced = availabilityAttrs.filter({ $0.introduced != nil })
let duplicateBeta = availabilityAttrs.filter({ $0.isBeta })
let duplicateDeprecated = availabilityAttrs.filter({ $0.isDeprecated })
if duplicateIntroduced.count > 1 {
for avail in duplicateIntroduced {
let diagnostic = Diagnostic(
source: avail.originalMarkup.nameLocation?.source,
severity: .warning,
range: avail.originalMarkup.range,
identifier: "org.swift.docc.\(MetadataAvailability.self).DuplicateIntroduced",
summary: "Duplicate \(MetadataAvailability.directiveName.singleQuoted) directive with `introduced` argument",
explanation: """
A documentation page can only contain a single `introduced` version for each platform.
"""
)

guard let range = avail.originalMarkup.range else {
problems.append(Problem(diagnostic: diagnostic))
continue
}

let solution = Solution(
summary: "Remove extraneous \(MetadataAvailability.directiveName.singleQuoted) directive",
replacements: [
Replacement(range: range, replacement: "")
]
)

problems.append(Problem(diagnostic: diagnostic, possibleSolutions: [solution]))
}
}

if duplicateBeta.count > 1 {
for avail in duplicateBeta {
let diagnostic = Diagnostic(
source: avail.originalMarkup.nameLocation?.source,
severity: .warning,
range: avail.originalMarkup.range,
identifier: "org.swift.docc.\(MetadataAvailability.self).DuplicateBeta",
summary: "Duplicate \(MetadataAvailability.directiveName.singleQuoted) directive with `isBeta` argument",
explanation: """
A documentation page can only be declared `isBeta` once per platform.
"""
)

guard let range = avail.originalMarkup.range else {
problems.append(Problem(diagnostic: diagnostic))
continue
}

let solution = Solution(
summary: "Remove extraneous \(MetadataAvailability.directiveName.singleQuoted) directive",
replacements: [
Replacement(range: range, replacement: "")
]
)

problems.append(Problem(diagnostic: diagnostic, possibleSolutions: [solution]))
}
}

if duplicateDeprecated.count > 1 {
for avail in duplicateDeprecated {
let diagnostic = Diagnostic(
source: avail.originalMarkup.nameLocation?.source,
severity: .warning,
range: avail.originalMarkup.range,
identifier: "org.swift.docc.\(MetadataAvailability.self).DuplicateDeprecated",
summary: "Duplicate \(MetadataAvailability.directiveName.singleQuoted) directive with `isDeprecated` argument",
explanation: """
A documentation page can only be declared `isDeprecated` once per platform.
"""
)

guard let range = avail.originalMarkup.range else {
problems.append(Problem(diagnostic: diagnostic))
continue
}

let solution = Solution(
summary: "Remove extraneous \(MetadataAvailability.directiveName.singleQuoted) directive",
replacements: [
Replacement(range: range, replacement: "")
]
)

problems.append(Problem(diagnostic: diagnostic, possibleSolutions: [solution]))
}
}
}

return true
}
Expand Down
11 changes: 11 additions & 0 deletions Sources/SwiftDocC/Semantics/Symbol/PlatformName.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,15 @@ public struct PlatformName: Codable, Hashable, Equatable {
}
self = knowDomain
}

/// Creates a new platform name from the given metadata availability attribute platform.
///
/// Returns `nil` if the given platform was ``MetadataAvailability/Platform-swift.enum/any``.
init?(metadataPlatform platform: MetadataAvailability.Platform) {
if platform == .any {
return nil
} else {
self = .init(operatingSystemName: platform.rawValue)
}
}
}
Loading

0 comments on commit 8d4243a

Please sign in to comment.