From 6673f025bf694f7c51888228b8a7a123d14a1b1b Mon Sep 17 00:00:00 2001 From: Victoria Mitchell Date: Wed, 14 Dec 2022 17:06:50 -0700 Subject: [PATCH 1/9] add `@Available` directive for setting platform availability rdar://57847232 --- .../Rendering/RenderNodeTranslator.swift | 34 ++ .../AvailabilityRenderMetadataItem.swift | 17 + .../Semantics/Metadata/Availability.swift | 138 +++++ .../Semantics/Metadata/Metadata.swift | 108 +++- .../Semantics/Symbol/PlatformName.swift | 11 + .../Rendering/PlatformAvailabilityTests.swift | 46 ++ .../DirectiveIndexTests.swift | 1 + .../DirectiveMirrorTests.swift | 2 +- .../Semantics/MetadataAvailabilityTests.swift | 236 ++++++++ .../AvailableArticle.md | 10 + .../AvailabilityBundle.docc/Info.plist | 12 + .../AvailabilityBundle.docc/MyClass.md | 7 + .../mykit-iOS.symbols.json | 562 ++++++++++++++++++ 13 files changed, 1182 insertions(+), 2 deletions(-) create mode 100644 Sources/SwiftDocC/Semantics/Metadata/Availability.swift create mode 100644 Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift create mode 100644 Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/AvailableArticle.md create mode 100644 Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/Info.plist create mode 100644 Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/MyClass.md create mode 100644 Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/mykit-iOS.symbols.json diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift index 95c9fccbd4..cb34d77f7b 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift @@ -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() @@ -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(from: symbol.isRequiredVariants) ?? .init(defaultValue: false) node.metadata.role = contentRenderer.role(for: documentationNode.kind).rawValue diff --git a/Sources/SwiftDocC/Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift b/Sources/SwiftDocC/Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift index e233ec0bda..8486a2db9b 100644 --- a/Sources/SwiftDocC/Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift +++ b/Sources/SwiftDocC/Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift @@ -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: diff --git a/Sources/SwiftDocC/Semantics/Metadata/Availability.swift b/Sources/SwiftDocC/Semantics/Metadata/Availability.swift new file mode 100644 index 0000000000..0a6b2ffa36 --- /dev/null +++ b/Sources/SwiftDocC/Semantics/Metadata/Availability.swift @@ -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 + } +} diff --git a/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift b/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift index cb99ce15e2..4559590bc5 100644 --- a/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift +++ b/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift @@ -24,6 +24,7 @@ import Markdown /// - ``DisplayName`` /// - ``PageImage`` /// - ``CallToAction`` +/// - ``MetadataAvailability`` public final class Metadata: Semantic, AutomaticDirectiveConvertible { public let originalMarkup: BlockDirective @@ -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, @@ -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. @@ -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, @@ -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 } diff --git a/Sources/SwiftDocC/Semantics/Symbol/PlatformName.swift b/Sources/SwiftDocC/Semantics/Symbol/PlatformName.swift index 761d8e3d1a..88dcfdd0e2 100644 --- a/Sources/SwiftDocC/Semantics/Symbol/PlatformName.swift +++ b/Sources/SwiftDocC/Semantics/Symbol/PlatformName.swift @@ -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) + } + } } diff --git a/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift b/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift index 3036a6899c..85c9c491f3 100644 --- a/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift @@ -31,4 +31,50 @@ class PlatformAvailabilityTests: XCTestCase { // The "iOS" platform in the fixture is unconditionally unavailable XCTAssertEqual(true, platforms.first { $0.name == "iOS" }?.unconditionallyUnavailable) } + + /// Ensure that adding `@Available` directives in an article causes the final RenderNode to contain the appropriate availability data. + func testPlatformAvailabilityFromArticle() throws { + let (bundle, context) = try testBundleAndContext(named: "AvailabilityBundle") + let reference = ResolvedTopicReference( + bundleIdentifier: bundle.identifier, + path: "/documentation/AvailableArticle", + sourceLanguage: .swift + ) + let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) + var translator = RenderNodeTranslator( + context: context, + bundle: bundle, + identifier: reference, + source: nil + ) + let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode) + let availability = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue) + XCTAssertEqual(availability.count, 1) + let iosAvailability = try XCTUnwrap(availability.first) + XCTAssertEqual(iosAvailability.name, "iOS") + XCTAssertEqual(iosAvailability.introduced, "16.0") + } + + /// Ensure that adding `@Available` directives in an extension file overrides the symbol's availability. + func testPlatformAvailabilityFromExtension() throws { + let (bundle, context) = try testBundleAndContext(named: "AvailabilityBundle") + let reference = ResolvedTopicReference( + bundleIdentifier: bundle.identifier, + path: "/documentation/MyKit/MyClass", + sourceLanguage: .swift + ) + let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) + var translator = RenderNodeTranslator( + context: context, + bundle: bundle, + identifier: reference, + source: nil + ) + let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) + let availability = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue) + XCTAssertEqual(availability.count, 1) + let iosAvailability = try XCTUnwrap(availability.first) + XCTAssertEqual(iosAvailability.name, "iOS") + XCTAssertEqual(iosAvailability.introduced, "16.0") + } } diff --git a/Tests/SwiftDocCTests/Semantics/DirectiveInfrastructure/DirectiveIndexTests.swift b/Tests/SwiftDocCTests/Semantics/DirectiveInfrastructure/DirectiveIndexTests.swift index 1b938e3a18..95629008b9 100644 --- a/Tests/SwiftDocCTests/Semantics/DirectiveInfrastructure/DirectiveIndexTests.swift +++ b/Tests/SwiftDocCTests/Semantics/DirectiveInfrastructure/DirectiveIndexTests.swift @@ -21,6 +21,7 @@ class DirectiveIndexTests: XCTestCase { "AutomaticArticleSubheading", "AutomaticSeeAlso", "AutomaticTitleHeading", + "Available", "CallToAction", "Chapter", "Choice", diff --git a/Tests/SwiftDocCTests/Semantics/DirectiveInfrastructure/DirectiveMirrorTests.swift b/Tests/SwiftDocCTests/Semantics/DirectiveInfrastructure/DirectiveMirrorTests.swift index c5462c7955..cbf2c3a2cf 100644 --- a/Tests/SwiftDocCTests/Semantics/DirectiveInfrastructure/DirectiveMirrorTests.swift +++ b/Tests/SwiftDocCTests/Semantics/DirectiveInfrastructure/DirectiveMirrorTests.swift @@ -39,7 +39,7 @@ class DirectiveMirrorTests: XCTestCase { XCTAssertFalse(reflectedDirective.allowsMarkup) XCTAssert(reflectedDirective.arguments.isEmpty) - XCTAssertEqual(reflectedDirective.childDirectives.count, 6) + XCTAssertEqual(reflectedDirective.childDirectives.count, 7) XCTAssertEqual( reflectedDirective.childDirectives["DocumentationExtension"]?.propertyLabel, diff --git a/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift b/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift new file mode 100644 index 0000000000..1cf32606cb --- /dev/null +++ b/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift @@ -0,0 +1,236 @@ +/* + 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 XCTest +import Markdown + +@testable import SwiftDocC + +class MetadataAvailabilityTests: XCTestCase { + func testInvalidWithNoArguments() throws { + let source = "@Available" + let document = Document(parsing: source, options: .parseBlockDirectives) + let directive = document.child(at: 0) as? BlockDirective + XCTAssertNotNil(directive) + + let (bundle, context) = try testBundleAndContext(named: "AvailabilityBundle") + + directive.map { directive in + var problems = [Problem]() + XCTAssertEqual(MetadataAvailability.directiveName, directive.name) + let availability = MetadataAvailability(from: directive, source: nil, for: bundle, in: context, problems: &problems) + XCTAssertNil(availability) + XCTAssertEqual(1, problems.count) + let diagnosticIdentifiers = Set(problems.map { $0.diagnostic.identifier }) + XCTAssertTrue(diagnosticIdentifiers.contains("org.swift.docc.\(MetadataAvailability.self).emptyAttribute")) + } + } + + func testInvalidIntroducedForAllPlatforms() throws { + func assertInvalidDirective(source: String) throws { + let document = Document(parsing: source, options: .parseBlockDirectives) + let directive = document.child(at: 0) as? BlockDirective + XCTAssertNotNil(directive) + + let (bundle, context) = try testBundleAndContext(named: "AvailabilityBundle") + + directive.map { directive in + var problems = [Problem]() + XCTAssertEqual(MetadataAvailability.directiveName, directive.name) + let availability = MetadataAvailability(from: directive, source: nil, for: bundle, in: context, problems: &problems) + XCTAssertNil(availability) + XCTAssertEqual(1, problems.count) + let diagnosticIdentifiers = Set(problems.map { $0.diagnostic.identifier }) + XCTAssertTrue(diagnosticIdentifiers.contains("org.swift.docc.\(MetadataAvailability.self).introducedVersionForAllPlatforms")) + } + } + + try assertInvalidDirective(source: "@Available(introduced: \"1.0\")") + try assertInvalidDirective(source: "@Available(*, introduced: \"1.0\")") + } + + func testInvalidDuplicateIntroduced() throws { + func assertInvalidDirective(source: String) throws { + let document = Document(parsing: source, options: .parseBlockDirectives) + let directive = document.child(at: 0) as? BlockDirective + XCTAssertNotNil(directive) + + let (bundle, context) = try testBundleAndContext(named: "AvailabilityBundle") + + directive.map { directive in + var problems = [Problem]() + XCTAssertEqual(Metadata.directiveName, directive.name) + let _ = Metadata(from: directive, source: nil, for: bundle, in: context, problems: &problems) + XCTAssertEqual(2, problems.count) + let diagnosticIdentifiers = Set(problems.map { $0.diagnostic.identifier }) + XCTAssertEqual(diagnosticIdentifiers, ["org.swift.docc.\(MetadataAvailability.self).DuplicateIntroduced"]) + } + } + + for platform in MetadataAvailability.Platform.allCases { + guard platform != .any else { continue } + + let source = """ + @Metadata { + @Available(\(platform.rawValue), introduced: \"1.0\") + @Available(\(platform.rawValue), introduced: \"2.0\") + } + """ + try assertInvalidDirective(source: source) + } + } + + func testInvalidDuplicateBeta() throws { + throw XCTSkip("FIXME: isBeta is unused (issue link forthcoming)") + +// func assertInvalidDirective(source: String) throws { +// let document = Document(parsing: source, options: .parseBlockDirectives) +// let directive = document.child(at: 0) as? BlockDirective +// XCTAssertNotNil(directive) +// +// let (bundle, context) = try testBundleAndContext(named: "AvailabilityBundle") +// +// directive.map { directive in +// var problems = [Problem]() +// XCTAssertEqual(Metadata.directiveName, directive.name) +// let _ = Metadata(from: directive, source: nil, for: bundle, in: context, problems: &problems) +// XCTAssertEqual(2, problems.count) +// let diagnosticIdentifiers = Set(problems.map { $0.diagnostic.identifier }) +// XCTAssertEqual(diagnosticIdentifiers, ["org.swift.docc.\(MetadataAvailability.self).DuplicateBeta"]) +// } +// } +// +// for platform in MetadataAvailability.Platform.allCases { +// let source = """ +// @Metadata { +// @Available(\(platform.rawValue), isBeta: true) +// @Available(\(platform.rawValue), isBeta: true) +// } +// """ +// try assertInvalidDirective(source: source) +// } +// +// // also test for giving no platform +// let source = """ +// @Metadata { +// @Available(isBeta: true) +// @Available(isBeta: true) +// } +// """ +// try assertInvalidDirective(source: source) + } + + func testInvalidDuplicateDeprecated() throws { + throw XCTSkip("FIXME: isDeprecated is unused (issue link forthcoming)") + +// func assertInvalidDirective(source: String) throws { +// let document = Document(parsing: source, options: .parseBlockDirectives) +// let directive = document.child(at: 0) as? BlockDirective +// XCTAssertNotNil(directive) +// +// let (bundle, context) = try testBundleAndContext(named: "AvailabilityBundle") +// +// directive.map { directive in +// var problems = [Problem]() +// XCTAssertEqual(Metadata.directiveName, directive.name) +// let _ = Metadata(from: directive, source: nil, for: bundle, in: context, problems: &problems) +// XCTAssertEqual(2, problems.count) +// let diagnosticIdentifiers = Set(problems.map { $0.diagnostic.identifier }) +// XCTAssertEqual(diagnosticIdentifiers, ["org.swift.docc.\(MetadataAvailability.self).DuplicateDeprecated"]) +// } +// } +// +// for platform in MetadataAvailability.Platform.allCases { +// let source = """ +// @Metadata { +// @Available(\(platform.rawValue), isDeprecated: true) +// @Available(\(platform.rawValue), isDeprecated: true) +// } +// """ +// try assertInvalidDirective(source: source) +// } +// +// // also test for giving no platform +// let source = """ +// @Metadata { +// @Available(isDeprecated: true) +// @Available(isDeprecated: true) +// } +// """ +// try assertInvalidDirective(source: source) + } + + func testValidDirective() throws { + // assemble all the combinations of arguments you could give + let validArguments: [String] = [ + // FIXME: isBeta and isDeprecated are unused (issue link forthcoming) +// "isBeta: true", +// "isDeprecated: true", +// "isBeta: true, isDeprecated: true", + ] + // separate those that give a version so we can test the `*` platform separately + var validArgumentsWithVersion = ["introduced: \"1.0\""] + for arg in validArguments { + validArgumentsWithVersion.append("introduced: \"1.0\", \(arg)") + } + + for platform in MetadataAvailability.Platform.allCases { + if platform != .any { + for args in validArgumentsWithVersion { + try assertValidAvailability(source: "@Available(\(platform.rawValue), \(args))") + } + } else { + for args in validArguments { + try assertValidAvailability(source: "@Available(\(platform.rawValue), \(args))") + } + } + } + + // also test for giving no platform + for args in validArguments { + try assertValidAvailability(source: "@Available(\(args))") + } + + // basic validity test for giving several directives + // FIXME: re-add isBeta after that is implemented (issue link forthcoming) + let source = """ + @Metadata { + @Available(macOS, introduced: "11.0") + @Available(iOS, introduced: "15.0") + } + """ + try assertValidMetadata(source: source) + } + + func assertValidDirective(_ type: Directive.Type, source: String) throws { + let document = Document(parsing: source, options: .parseBlockDirectives) + let directive = document.child(at: 0) as? BlockDirective + XCTAssertNotNil(directive) + + let (bundle, context) = try testBundleAndContext(named: "AvailabilityBundle") + + directive.map { directive in + var problems = [Problem]() + XCTAssertEqual(Directive.directiveName, directive.name) + let converted = Directive(from: directive, source: nil, for: bundle, in: context, problems: &problems) + XCTAssertNotNil(converted) + XCTAssert(problems.isEmpty) + } + } + + func assertValidAvailability(source: String) throws { + try assertValidDirective(MetadataAvailability.self, source: source) + } + + func assertValidMetadata(source: String) throws { + try assertValidDirective(Metadata.self, source: source) + } +} diff --git a/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/AvailableArticle.md b/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/AvailableArticle.md new file mode 100644 index 0000000000..d3d0ade16b --- /dev/null +++ b/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/AvailableArticle.md @@ -0,0 +1,10 @@ +# Available Article + +@Metadata { + @TechnologyRoot + @Available(iOS, introduced: 16.0) +} + +Here's a cool framework that I'm offering to the world. + + diff --git a/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/Info.plist b/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/Info.plist new file mode 100644 index 0000000000..e55715adc6 --- /dev/null +++ b/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/Info.plist @@ -0,0 +1,12 @@ + + + + + CFBundleDisplayName + AvailabilityBundle + CFBundleIdentifier + org.swift.docc.availability + CFBundleVersion + 0.1.0 + + diff --git a/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/MyClass.md b/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/MyClass.md new file mode 100644 index 0000000000..c6c21bd5de --- /dev/null +++ b/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/MyClass.md @@ -0,0 +1,7 @@ +# ``MyKit/MyClass`` + +@Metadata { + @Available(iOS, introduced: "16.0") +} + + diff --git a/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/mykit-iOS.symbols.json b/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/mykit-iOS.symbols.json new file mode 100644 index 0000000000..5d330a130d --- /dev/null +++ b/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/mykit-iOS.symbols.json @@ -0,0 +1,562 @@ +{ + "metadata": { + "formatVersion" : { + "major" : 1 + }, + "generator" : "app/1.0" + }, + "module" : { + "name" : "MyKit", + "platform" : { + "architecture" : "x86_64", + "vendor" : "apple", + "operatingSystem" : { + "name" : "ios", + "minimumVersion" : { + "major" : 13, + "minor" : 0, + "patch" : 0 + } + } + } + }, + "symbols" : [ + { + "accessLevel" : "public", + "kind" : { + "identifier" : "swift.class", + "displayName" : "Class" + }, + "names" : { + "title" : "MyClass", + "subHeading" : [ + { + "kind" : "keyword", + "spelling" : "class" + }, + { + "kind" : "text", + "spelling" : " " + }, + { + "kind" : "identifier", + "spelling" : "MyClass" + } + ], + "navigator" : [ + { + "kind" : "identifier", + "spelling" : "MyClassNavigator" + } + ] + }, + "availability" : [ + { + "domain": "macOS", + "introduced": { + "major": 10, + "minor": 15 + } + }, + { + "domain": "watchOS", + "introduced": { + "major": 6, + "minor": 0 + } + }, + { + "domain": "tvOS", + "introduced": { + "major": 13, + "minor": 0 + } + }, + { + "domain": "iOS", + "deprecated": { + "major": 13, + "minor": 0 + } + } + ], + "pathComponents" : [ + "MyClass" + ], + "identifier" : { + "precise" : "s:5MyKit0A5ClassC", + "interfaceLanguage": "swift" + }, + "declarationFragments" : [ + { + "kind" : "keyword", + "spelling" : "class" + }, + { + "kind" : "text", + "spelling" : " " + }, + { + "kind" : "identifier", + "spelling" : "MyClass" + } + ] + }, + { + "accessLevel" : "public", + "kind" : { + "identifier" : "swift.method", + "displayName" : "Instance Method" + }, + "names" : { + "title" : "myFunction()" + }, + "pathComponents" : [ + "MyClass", + "myFunction()" + ], + "identifier" : { + "precise" : "s:5MyKit0A5ClassC10myFunctionyyF", + "interfaceLanguage": "swift" + }, + "docComment" : { + "lines" : [ + { + "text" : "A cool API to call." + }, + { + "text" : "" + }, + { + "text" : "- Parameters:" + }, + { + "text" : " - name: A parameter" + }, + { + "text" : "- Returns: Return value" + } + ] + }, + "declarationFragments" : [ + { + "kind" : "keyword", + "spelling" : "func" + }, + { + "kind" : "text", + "spelling" : " " + }, + { + "kind" : "identifier", + "spelling" : "myFunction" + }, + { + "kind" : "text", + "spelling" : "(" + }, + { + "kind" : "externalParam", + "spelling" : "for" + }, + { + "kind" : "text", + "spelling" : " " + }, + { + "kind" : "internalParam", + "spelling" : "name" + }, + { + "kind" : "unhandledTokenKind", + "spelling" : "..." + }, + { + "kind" : "text", + "spelling" : ")" + } + ], + "swiftExtension" : { + "extendedModule": "MyKit", + "constraints" : [ + { + "kind" : "sameType", + "lhs" : "Label", + "rhs" : "Text" + }, + { + "kind" : "superclass", + "lhs" : "Observer", + "rhs" : "NSObject" + }, + { + "kind" : "conformance", + "lhs" : "S", + "rhs" : "StringProtocol" + } + ] + } + }, + { + "accessLevel" : "public", + "declarationFragments" : [ + { + "kind" : "keyword", + "spelling" : "func" + }, + { + "kind" : "text", + "spelling" : " " + }, + { + "kind" : "identifier", + "spelling" : "globalFunction" + }, + { + "kind" : "text", + "spelling" : "(" + }, + { + "kind" : "identifier", + "spelling" : "_" + }, + { + "kind" : "text", + "spelling" : ": " + }, + { + "kind" : "typeIdentifier", + "preciseIdentifier" : "s:10Foundation4DataV", + "spelling" : "Data" + }, + { + "kind" : "text", + "spelling" : ", " + }, + { + "kind" : "identifier", + "spelling" : "considering" + }, + { + "kind" : "text", + "spelling" : ": " + }, + { + "kind" : "typeIdentifier", + "preciseIdentifier" : "s:Si", + "spelling" : "Int" + }, + { + "kind" : "text", + "spelling" : ")" + } + ], + "docComment" : { + "lines" : [ + + ] + }, + "functionSignature" : { + "parameters" : [ + { + "declarationFragments" : [ + { + "kind" : "identifier", + "spelling" : "data" + }, + { + "kind" : "text", + "spelling" : ": " + }, + { + "kind" : "typeIdentifier", + "preciseIdentifier" : "s:10Foundation4DataV", + "spelling" : "Data" + } + ], + "name" : "data" + }, + { + "declarationFragments" : [ + { + "kind" : "identifier", + "spelling" : "considering" + }, + { + "kind" : "text", + "spelling" : ": " + }, + { + "kind" : "typeIdentifier", + "preciseIdentifier" : "s:Si", + "spelling" : "Int" + } + ], + "name" : "considering" + } + ], + "returns" : [ + { + "kind" : "text", + "spelling" : "()" + } + ] + }, + "identifier" : { + "interfaceLanguage" : "swift", + "precise" : "s:5MyKit14globalFunction_11consideringy10Foundation4DataV_SitF" + }, + "kind" : { + "displayName" : "Function", + "identifier" : "swift.func" + }, + "names" : { + "navigator" : [ + { + "kind" : "keyword", + "spelling" : "func" + }, + { + "kind" : "text", + "spelling" : " " + }, + { + "kind" : "identifier", + "spelling" : "globalFunction" + }, + { + "kind" : "text", + "spelling" : "(" + }, + { + "kind" : "typeIdentifier", + "preciseIdentifier" : "s:10Foundation4DataV", + "spelling" : "Data" + }, + { + "kind" : "text", + "spelling" : ", " + }, + { + "kind" : "identifier", + "spelling" : "considering" + }, + { + "kind" : "text", + "spelling" : ": " + }, + { + "kind" : "typeIdentifier", + "preciseIdentifier" : "s:Si", + "spelling" : "Int" + }, + { + "kind" : "text", + "spelling" : ")" + }, + { + "kind" : "text", + "spelling" : "\n" + } + ], + "subHeading" : [ + { + "kind" : "keyword", + "spelling" : "func" + }, + { + "kind" : "text", + "spelling" : " " + }, + { + "kind" : "identifier", + "spelling" : "globalFunction" + }, + { + "kind" : "text", + "spelling" : "(" + }, + { + "kind" : "typeIdentifier", + "preciseIdentifier" : "s:10Foundation4DataV", + "spelling" : "Data" + }, + { + "kind" : "text", + "spelling" : ", " + }, + { + "kind" : "identifier", + "spelling" : "considering" + }, + { + "kind" : "text", + "spelling" : ": " + }, + { + "kind" : "typeIdentifier", + "preciseIdentifier" : "s:Si", + "spelling" : "Int" + }, + { + "kind" : "text", + "spelling" : ")" + }, + { + "kind" : "text", + "spelling" : "\n" + } + ], + "title" : "globalFunction(_:considering:)" + }, + "pathComponents" : [ + "globalFunction(_:considering:)" + ] + }, + { + "accessLevel" : "internal", + "kind" : { + "identifier" : "swift.init", + "displayName" : "Initializer" + }, + "names" : { + "title" : "init()" + }, + "pathComponents" : [ + "MyClass", + "init()" + ], + "identifier" : { + "precise" : "s:5MyKit0A5ClassCACycfc", + "interfaceLanguage": "swift" + } + }, + { + "accessLevel" : "internal", + "kind" : { + "identifier" : "swift.init", + "displayName" : "Initializer" + }, + "names" : { + "title" : "init()" + }, + "pathComponents" : [ + "MyClass", + "init()" + ], + "identifier" : { + "precise" : "s:5MyKit0A5ClassCACycfcDUPLICATE", + "interfaceLanguage" : "swift" + } + }, + { + "accessLevel" : "public", + "kind" : { + "identifier" : "swift.protocol", + "displayName" : "Protocol" + }, + "names" : { + "title" : "MyProtocol", + "navigator": [ + { + "kind" : "identifier", + "spelling" : "MyProtocol" + } + ], + "subHeading": [ + { + "kind" : "keyword", + "spelling" : "protocol" + }, + { + "kind" : "text", + "spelling" : " " + }, + { + "kind" : "identifier", + "spelling" : "MyProtocol" + }, + { + "kind" : "text", + "spelling" : " : " + }, + { + "kind" : "typeIdentifier", + "spelling" : "Hashable", + "preciseIdentifier" : "p:hPP" + } + ] + }, + "pathComponents" : [ + "MyProtocol" + ], + "identifier" : { + "precise" : "s:5MyKit0A5ProtocolP", + "interfaceLanguage": "swift" + }, + "declarationFragments" : [ + { + "kind" : "keyword", + "spelling" : "protocol" + }, + { + "kind" : "text", + "spelling" : " " + }, + { + "kind" : "identifier", + "spelling" : "MyProtocol" + }, + { + "kind" : "text", + "spelling" : " : " + }, + { + "kind" : "typeIdentifier", + "spelling" : "Hashable", + "preciseIdentifier" : "p:hPP" + } + ] + } + ], + "relationships" : [ + { + "source" : "s:5MyKit0A5ProtocolP", + "target" : "s:5Foundation0A5NSCodableP", + "kind" : "conformsTo", + "targetFallback": "Foundation.NSCodable" + }, + { + "source" : "s:5MyKit0A5ProtocolP", + "target" : "s:5Foundation0A5EarhartP", + "kind" : "conformsTo" + }, + { + "source" : "s:5MyKit0A5ClassC", + "target" : "s:5MyKit0A5ProtocolP", + "kind" : "conformsTo", + "swiftConstraints" : [ + { + "kind" : "conformance", + "lhs" : "Element", + "rhs" : "Equatable" + } + ] + }, + { + "source" : "s:5MyKit0A5ClassC10myFunctionyyF", + "target" : "s:5MyKit0A5ClassC", + "kind" : "memberOf" + }, + { + "source" : "s:5MyKit0A5ClassCACycfc", + "target" : "s:5MyKit0A5ClassC", + "kind" : "memberOf" + }, + { + "source" : "s:5MyKit0A5ClassCACycfcDUPLICATE", + "target" : "s:5MyKit0A5ClassC", + "kind" : "memberOf" + } + ] +} From 2c55fe06086a9c6f7d6be7cbb297ba615e8c9a8b Mon Sep 17 00:00:00 2001 From: Victoria Mitchell Date: Wed, 14 Dec 2022 17:21:55 -0700 Subject: [PATCH 2/9] add issue link in fixme comments --- .../Rendering/Symbol/AvailabilityRenderMetadataItem.swift | 2 +- .../Semantics/MetadataAvailabilityTests.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftDocC/Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift b/Sources/SwiftDocC/Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift index 8486a2db9b..ab03a06fb7 100644 --- a/Sources/SwiftDocC/Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift +++ b/Sources/SwiftDocC/Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift @@ -143,7 +143,7 @@ public struct AvailabilityRenderItem: Codable, Hashable, Equatable { 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) + // Fill in the appropriate values here when that's fixed (https://github.com/apple/swift-docc/issues/441) return nil } diff --git a/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift b/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift index 1cf32606cb..bffa53d4a9 100644 --- a/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift @@ -89,7 +89,7 @@ class MetadataAvailabilityTests: XCTestCase { } func testInvalidDuplicateBeta() throws { - throw XCTSkip("FIXME: isBeta is unused (issue link forthcoming)") + throw XCTSkip("FIXME: isBeta is unused (https://github.com/apple/swift-docc/issues/441)") // func assertInvalidDirective(source: String) throws { // let document = Document(parsing: source, options: .parseBlockDirectives) @@ -129,7 +129,7 @@ class MetadataAvailabilityTests: XCTestCase { } func testInvalidDuplicateDeprecated() throws { - throw XCTSkip("FIXME: isDeprecated is unused (issue link forthcoming)") + throw XCTSkip("FIXME: isDeprecated is unused (https://github.com/apple/swift-docc/issues/441)") // func assertInvalidDirective(source: String) throws { // let document = Document(parsing: source, options: .parseBlockDirectives) @@ -171,7 +171,7 @@ class MetadataAvailabilityTests: XCTestCase { func testValidDirective() throws { // assemble all the combinations of arguments you could give let validArguments: [String] = [ - // FIXME: isBeta and isDeprecated are unused (issue link forthcoming) + // FIXME: isBeta and isDeprecated are unused (https://github.com/apple/swift-docc/issues/441) // "isBeta: true", // "isDeprecated: true", // "isBeta: true, isDeprecated: true", @@ -200,7 +200,7 @@ class MetadataAvailabilityTests: XCTestCase { } // basic validity test for giving several directives - // FIXME: re-add isBeta after that is implemented (issue link forthcoming) + // FIXME: re-add isBeta after that is implemented (https://github.com/apple/swift-docc/issues/441) let source = """ @Metadata { @Available(macOS, introduced: "11.0") From 23a842682fb3d09a5c83caa4fdcec8b3603f8024 Mon Sep 17 00:00:00 2001 From: Victoria Mitchell Date: Thu, 22 Dec 2022 11:30:50 -0700 Subject: [PATCH 3/9] remove isBeta and isDeprecated arguments --- .../AvailabilityRenderMetadataItem.swift | 7 +- .../Semantics/Metadata/Availability.swift | 73 +---------- .../Semantics/Metadata/Metadata.swift | 60 --------- .../Semantics/Symbol/PlatformName.swift | 9 +- .../Semantics/MetadataAvailabilityTests.swift | 120 +----------------- 5 files changed, 15 insertions(+), 254 deletions(-) diff --git a/Sources/SwiftDocC/Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift b/Sources/SwiftDocC/Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift index ab03a06fb7..f971a59a6d 100644 --- a/Sources/SwiftDocC/Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift +++ b/Sources/SwiftDocC/Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift @@ -147,12 +147,7 @@ public struct AvailabilityRenderItem: Codable, Hashable, Equatable { return nil } - let platformName: PlatformName? - if availability.platform == .any { - platformName = nil - } else { - platformName = PlatformName(operatingSystemName: availability.platform.rawValue) - } + let platformName = PlatformName(metadataPlatform: availability.platform) name = platformName?.displayName introduced = availability.introduced } diff --git a/Sources/SwiftDocC/Semantics/Metadata/Availability.swift b/Sources/SwiftDocC/Semantics/Metadata/Availability.swift index 0a6b2ffa36..7960d94ac0 100644 --- a/Sources/SwiftDocC/Semantics/Metadata/Availability.swift +++ b/Sources/SwiftDocC/Semantics/Metadata/Availability.swift @@ -37,7 +37,8 @@ public final class MetadataAvailability: Semantic, AutomaticDirectiveConvertible static public let directiveName: String = "Available" public enum Platform: String, RawRepresentable, CaseIterable, DirectiveArgumentValueConvertible { - case any = "*" + // FIXME: re-add `case any = "*"` when `isBeta` and `isDeprecated` are implemented + // cf. https://github.com/apple/swift-docc/issues/441 case macOS, iOS, watchOS, tvOS public init?(rawValue: String) { @@ -53,82 +54,20 @@ public final class MetadataAvailability: Semantic, AutomaticDirectiveConvertible /// The platform that this argument's information applies to. @DirectiveArgumentWrapped(name: .unnamed) - public var platform: Platform = .any + public var platform: Platform /// The platform version that this page applies to. @DirectiveArgumentWrapped - public var introduced: String? = nil + public var introduced: String - /// 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 + // FIXME: `isBeta` and `isDeprecated` properties/arguments + // cf. https://github.com/apple/swift-docc/issues/441 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'.") diff --git a/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift b/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift index 4559590bc5..71b5370e20 100644 --- a/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift +++ b/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift @@ -149,8 +149,6 @@ public final class Metadata: Semantic, AutomaticDirectiveConvertible { } 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( @@ -179,64 +177,6 @@ public final class Metadata: Semantic, AutomaticDirectiveConvertible { 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 diff --git a/Sources/SwiftDocC/Semantics/Symbol/PlatformName.swift b/Sources/SwiftDocC/Semantics/Symbol/PlatformName.swift index 88dcfdd0e2..96ede600dd 100644 --- a/Sources/SwiftDocC/Semantics/Symbol/PlatformName.swift +++ b/Sources/SwiftDocC/Semantics/Symbol/PlatformName.swift @@ -106,10 +106,9 @@ public struct PlatformName: Codable, Hashable, Equatable { /// /// 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) - } + // Note: This is still an optional initializer to prevent source breakage when + // `MetadataAvailability.Platform` re-introduces the `.any` case + // cf. https://github.com/apple/swift-docc/issues/441 + self = .init(operatingSystemName: platform.rawValue) } } diff --git a/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift b/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift index bffa53d4a9..85d057baf1 100644 --- a/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift @@ -28,35 +28,9 @@ class MetadataAvailabilityTests: XCTestCase { XCTAssertEqual(MetadataAvailability.directiveName, directive.name) let availability = MetadataAvailability(from: directive, source: nil, for: bundle, in: context, problems: &problems) XCTAssertNil(availability) - XCTAssertEqual(1, problems.count) - let diagnosticIdentifiers = Set(problems.map { $0.diagnostic.identifier }) - XCTAssertTrue(diagnosticIdentifiers.contains("org.swift.docc.\(MetadataAvailability.self).emptyAttribute")) } } - func testInvalidIntroducedForAllPlatforms() throws { - func assertInvalidDirective(source: String) throws { - let document = Document(parsing: source, options: .parseBlockDirectives) - let directive = document.child(at: 0) as? BlockDirective - XCTAssertNotNil(directive) - - let (bundle, context) = try testBundleAndContext(named: "AvailabilityBundle") - - directive.map { directive in - var problems = [Problem]() - XCTAssertEqual(MetadataAvailability.directiveName, directive.name) - let availability = MetadataAvailability(from: directive, source: nil, for: bundle, in: context, problems: &problems) - XCTAssertNil(availability) - XCTAssertEqual(1, problems.count) - let diagnosticIdentifiers = Set(problems.map { $0.diagnostic.identifier }) - XCTAssertTrue(diagnosticIdentifiers.contains("org.swift.docc.\(MetadataAvailability.self).introducedVersionForAllPlatforms")) - } - } - - try assertInvalidDirective(source: "@Available(introduced: \"1.0\")") - try assertInvalidDirective(source: "@Available(*, introduced: \"1.0\")") - } - func testInvalidDuplicateIntroduced() throws { func assertInvalidDirective(source: String) throws { let document = Document(parsing: source, options: .parseBlockDirectives) @@ -76,8 +50,6 @@ class MetadataAvailabilityTests: XCTestCase { } for platform in MetadataAvailability.Platform.allCases { - guard platform != .any else { continue } - let source = """ @Metadata { @Available(\(platform.rawValue), introduced: \"1.0\") @@ -88,86 +60,6 @@ class MetadataAvailabilityTests: XCTestCase { } } - func testInvalidDuplicateBeta() throws { - throw XCTSkip("FIXME: isBeta is unused (https://github.com/apple/swift-docc/issues/441)") - -// func assertInvalidDirective(source: String) throws { -// let document = Document(parsing: source, options: .parseBlockDirectives) -// let directive = document.child(at: 0) as? BlockDirective -// XCTAssertNotNil(directive) -// -// let (bundle, context) = try testBundleAndContext(named: "AvailabilityBundle") -// -// directive.map { directive in -// var problems = [Problem]() -// XCTAssertEqual(Metadata.directiveName, directive.name) -// let _ = Metadata(from: directive, source: nil, for: bundle, in: context, problems: &problems) -// XCTAssertEqual(2, problems.count) -// let diagnosticIdentifiers = Set(problems.map { $0.diagnostic.identifier }) -// XCTAssertEqual(diagnosticIdentifiers, ["org.swift.docc.\(MetadataAvailability.self).DuplicateBeta"]) -// } -// } -// -// for platform in MetadataAvailability.Platform.allCases { -// let source = """ -// @Metadata { -// @Available(\(platform.rawValue), isBeta: true) -// @Available(\(platform.rawValue), isBeta: true) -// } -// """ -// try assertInvalidDirective(source: source) -// } -// -// // also test for giving no platform -// let source = """ -// @Metadata { -// @Available(isBeta: true) -// @Available(isBeta: true) -// } -// """ -// try assertInvalidDirective(source: source) - } - - func testInvalidDuplicateDeprecated() throws { - throw XCTSkip("FIXME: isDeprecated is unused (https://github.com/apple/swift-docc/issues/441)") - -// func assertInvalidDirective(source: String) throws { -// let document = Document(parsing: source, options: .parseBlockDirectives) -// let directive = document.child(at: 0) as? BlockDirective -// XCTAssertNotNil(directive) -// -// let (bundle, context) = try testBundleAndContext(named: "AvailabilityBundle") -// -// directive.map { directive in -// var problems = [Problem]() -// XCTAssertEqual(Metadata.directiveName, directive.name) -// let _ = Metadata(from: directive, source: nil, for: bundle, in: context, problems: &problems) -// XCTAssertEqual(2, problems.count) -// let diagnosticIdentifiers = Set(problems.map { $0.diagnostic.identifier }) -// XCTAssertEqual(diagnosticIdentifiers, ["org.swift.docc.\(MetadataAvailability.self).DuplicateDeprecated"]) -// } -// } -// -// for platform in MetadataAvailability.Platform.allCases { -// let source = """ -// @Metadata { -// @Available(\(platform.rawValue), isDeprecated: true) -// @Available(\(platform.rawValue), isDeprecated: true) -// } -// """ -// try assertInvalidDirective(source: source) -// } -// -// // also test for giving no platform -// let source = """ -// @Metadata { -// @Available(isDeprecated: true) -// @Available(isDeprecated: true) -// } -// """ -// try assertInvalidDirective(source: source) - } - func testValidDirective() throws { // assemble all the combinations of arguments you could give let validArguments: [String] = [ @@ -183,14 +75,10 @@ class MetadataAvailabilityTests: XCTestCase { } for platform in MetadataAvailability.Platform.allCases { - if platform != .any { - for args in validArgumentsWithVersion { - try assertValidAvailability(source: "@Available(\(platform.rawValue), \(args))") - } - } else { - for args in validArguments { - try assertValidAvailability(source: "@Available(\(platform.rawValue), \(args))") - } + // FIXME: Test validArguments with the `*` platform once that's introduced + // cf. https://github.com/apple/swift-docc/issues/441 + for args in validArgumentsWithVersion { + try assertValidAvailability(source: "@Available(\(platform.rawValue), \(args))") } } From 812ce614a3df96f7693e1012d65345656c1bd792 Mon Sep 17 00:00:00 2001 From: Victoria Mitchell Date: Thu, 22 Dec 2022 11:34:21 -0700 Subject: [PATCH 4/9] review: use single quotes in diagnostic messages --- Sources/SwiftDocC/Semantics/Metadata/Metadata.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift b/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift index 71b5370e20..49e0a77aed 100644 --- a/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift +++ b/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift @@ -156,9 +156,9 @@ public final class Metadata: Semantic, AutomaticDirectiveConvertible { severity: .warning, range: avail.originalMarkup.range, identifier: "org.swift.docc.\(MetadataAvailability.self).DuplicateIntroduced", - summary: "Duplicate \(MetadataAvailability.directiveName.singleQuoted) directive with `introduced` argument", + summary: "Duplicate \(MetadataAvailability.directiveName.singleQuoted) directive with 'introduced' argument", explanation: """ - A documentation page can only contain a single `introduced` version for each platform. + A documentation page can only contain a single 'introduced' version for each platform. """ ) From bdf3208191140dd5f719e66ab8fa29e3622613de Mon Sep 17 00:00:00 2001 From: Victoria Mitchell Date: Thu, 22 Dec 2022 11:35:19 -0700 Subject: [PATCH 5/9] review: show multiple directives in the docs --- Sources/SwiftDocC/Semantics/Metadata/Availability.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/SwiftDocC/Semantics/Metadata/Availability.swift b/Sources/SwiftDocC/Semantics/Metadata/Availability.swift index 7960d94ac0..0baed1c2a8 100644 --- a/Sources/SwiftDocC/Semantics/Metadata/Availability.swift +++ b/Sources/SwiftDocC/Semantics/Metadata/Availability.swift @@ -31,6 +31,7 @@ import Markdown /// ```markdown /// @Metadata { /// @Available(macOS, introduced: "12.0") +/// @Available(iOS, introduced: "15.0") /// } /// ``` public final class MetadataAvailability: Semantic, AutomaticDirectiveConvertible { From 917f2361a8f04e33271d240095ee2497bdcf4495 Mon Sep 17 00:00:00 2001 From: Victoria Mitchell Date: Thu, 22 Dec 2022 11:39:36 -0700 Subject: [PATCH 6/9] review: condense platform fetching into flatMap --- .../Model/Rendering/RenderNodeTranslator.swift | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift index cb34d77f7b..eb1f551f93 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift @@ -807,12 +807,8 @@ 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 + let currentPlatform = PlatformName(metadataPlatform: $0.platform).flatMap { name in + context.externalMetadata.currentPlatforms?[name.displayName] } return .init($0, current: currentPlatform) }).sorted(by: AvailabilityRenderOrder.compare) @@ -1213,12 +1209,8 @@ public struct RenderNodeTranslator: SemanticVisitor { 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 + let currentPlatform = PlatformName(metadataPlatform: $0.platform).flatMap { name in + context.externalMetadata.currentPlatforms?[name.displayName] } return .init($0, current: currentPlatform) }).sorted(by: AvailabilityRenderOrder.compare) From cab931c8812e2eefac84e7d489311e2a62b36cc4 Mon Sep 17 00:00:00 2001 From: Victoria Mitchell Date: Thu, 22 Dec 2022 11:42:19 -0700 Subject: [PATCH 7/9] review: condense categorizedAvailability initialization --- Sources/SwiftDocC/Semantics/Metadata/Metadata.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift b/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift index 49e0a77aed..3426c0d8cb 100644 --- a/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift +++ b/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift @@ -138,10 +138,7 @@ public final class Metadata: Semantic, AutomaticDirectiveConvertible { } } - var categorizedAvailability = [MetadataAvailability.Platform : [MetadataAvailability]]() - for availability in availability { - categorizedAvailability[availability.platform, default: []].append(availability) - } + let categorizedAvailability = Dictionary(grouping: availability, by: \.platform) for availabilityAttrs in categorizedAvailability.values { guard availabilityAttrs.count > 1 else { From 0d533aa6e256215b240b5b542bfa08cde19852f0 Mon Sep 17 00:00:00 2001 From: Victoria Mitchell Date: Thu, 5 Jan 2023 13:51:51 -0700 Subject: [PATCH 8/9] add test with multiple availability directives --- .../Rendering/PlatformAvailabilityTests.swift | 29 +++++++++++++++++++ .../AvailableArticle.md | 6 ++++ .../ComplexAvailable.md | 11 +++++++ 3 files changed, 46 insertions(+) create mode 100644 Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/ComplexAvailable.md diff --git a/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift b/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift index 85c9c491f3..8805a628ff 100644 --- a/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift @@ -77,4 +77,33 @@ class PlatformAvailabilityTests: XCTestCase { XCTAssertEqual(iosAvailability.name, "iOS") XCTAssertEqual(iosAvailability.introduced, "16.0") } + + func testMultiplePlatformAvailabilityFromArticle() throws { + let (bundle, context) = try testBundleAndContext(named: "AvailabilityBundle") + let reference = ResolvedTopicReference( + bundleIdentifier: bundle.identifier, + path: "/documentation/AvailabilityBundle/ComplexAvailable", + sourceLanguage: .swift + ) + let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) + var translator = RenderNodeTranslator( + context: context, + bundle: bundle, + identifier: reference, + source: nil + ) + let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode) + let availability = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue) + XCTAssertEqual(availability.count, 3) + + XCTAssert(availability.contains(where: { item in + item.name == "iOS" && item.introduced == "15.0" + })) + XCTAssert(availability.contains(where: { item in + item.name == "macOS" && item.introduced == "12.0" + })) + XCTAssert(availability.contains(where: { item in + item.name == "watchOS" && item.introduced == "7.0" + })) + } } diff --git a/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/AvailableArticle.md b/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/AvailableArticle.md index d3d0ade16b..ec112ad342 100644 --- a/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/AvailableArticle.md +++ b/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/AvailableArticle.md @@ -7,4 +7,10 @@ Here's a cool framework that I'm offering to the world. +## Topics + +### Cool Articles + +- + diff --git a/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/ComplexAvailable.md b/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/ComplexAvailable.md new file mode 100644 index 0000000000..10797aba95 --- /dev/null +++ b/Tests/SwiftDocCTests/Test Bundles/AvailabilityBundle.docc/ComplexAvailable.md @@ -0,0 +1,11 @@ +# Complex Available Article + +@Metadata { + @Available(macOS, introduced: "12.0") + @Available(iOS, introduced: "15.0") + @Available(watchOS, introduced: "7.0") +} + +This applies to three different platforms! + + From bef65998e077956a28f9b2effd1bcd32f6144bcf Mon Sep 17 00:00:00 2001 From: Victoria Mitchell Date: Thu, 5 Jan 2023 14:10:41 -0700 Subject: [PATCH 9/9] review: rename `MetadataAvailability` to `Metadata.Availability` --- .../AvailabilityRenderMetadataItem.swift | 2 +- .../Semantics/Metadata/Availability.swift | 106 +++++++++--------- .../Semantics/Metadata/Metadata.swift | 10 +- .../Semantics/Symbol/PlatformName.swift | 6 +- .../Semantics/MetadataAvailabilityTests.swift | 12 +- 5 files changed, 69 insertions(+), 67 deletions(-) diff --git a/Sources/SwiftDocC/Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift b/Sources/SwiftDocC/Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift index f971a59a6d..fb74065b93 100644 --- a/Sources/SwiftDocC/Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift +++ b/Sources/SwiftDocC/Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift @@ -140,7 +140,7 @@ public struct AvailabilityRenderItem: Codable, Hashable, Equatable { } } - init?(_ availability: MetadataAvailability, current: PlatformVersion?) { + init?(_ availability: Metadata.Availability, 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 (https://github.com/apple/swift-docc/issues/441) diff --git a/Sources/SwiftDocC/Semantics/Metadata/Availability.swift b/Sources/SwiftDocC/Semantics/Metadata/Availability.swift index 0baed1c2a8..206d41ab4f 100644 --- a/Sources/SwiftDocC/Semantics/Metadata/Availability.swift +++ b/Sources/SwiftDocC/Semantics/Metadata/Availability.swift @@ -11,68 +11,70 @@ 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") -/// @Available(iOS, introduced: "15.0") -/// } -/// ``` -public final class MetadataAvailability: Semantic, AutomaticDirectiveConvertible { - static public let directiveName: String = "Available" +extension Metadata { + /// 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") + /// @Available(iOS, introduced: "15.0") + /// } + /// ``` + public final class Availability: Semantic, AutomaticDirectiveConvertible { + static public let directiveName: String = "Available" - public enum Platform: String, RawRepresentable, CaseIterable, DirectiveArgumentValueConvertible { - // FIXME: re-add `case any = "*"` when `isBeta` and `isDeprecated` are implemented - // cf. https://github.com/apple/swift-docc/issues/441 - case macOS, iOS, watchOS, tvOS + public enum Platform: String, RawRepresentable, CaseIterable, DirectiveArgumentValueConvertible { + // FIXME: re-add `case any = "*"` when `isBeta` and `isDeprecated` are implemented + // cf. https://github.com/apple/swift-docc/issues/441 + case macOS, iOS, watchOS, tvOS - public init?(rawValue: String) { - for platform in Self.allCases { - if platform.rawValue.lowercased() == rawValue.lowercased() { - self = platform - return + public init?(rawValue: String) { + for platform in Self.allCases { + if platform.rawValue.lowercased() == rawValue.lowercased() { + self = platform + return + } } + return nil } - return nil } - } - /// The platform that this argument's information applies to. - @DirectiveArgumentWrapped(name: .unnamed) - public var platform: Platform + /// The platform that this argument's information applies to. + @DirectiveArgumentWrapped(name: .unnamed) + public var platform: Platform - /// The platform version that this page applies to. - @DirectiveArgumentWrapped - public var introduced: String + /// The platform version that this page applies to. + @DirectiveArgumentWrapped + public var introduced: String - // FIXME: `isBeta` and `isDeprecated` properties/arguments - // cf. https://github.com/apple/swift-docc/issues/441 + // FIXME: `isBeta` and `isDeprecated` properties/arguments + // cf. https://github.com/apple/swift-docc/issues/441 - static var keyPaths: [String : AnyKeyPath] = [ - "platform" : \MetadataAvailability._platform, - "introduced" : \MetadataAvailability._introduced, - ] + static var keyPaths: [String : AnyKeyPath] = [ + "platform" : \Availability._platform, + "introduced" : \Availability._introduced, + ] - public let originalMarkup: Markdown.BlockDirective + public let originalMarkup: Markdown.BlockDirective - @available(*, deprecated, message: "Do not call directly. Required for 'AutomaticDirectiveConvertible'.") - init(originalMarkup: Markdown.BlockDirective) { - self.originalMarkup = originalMarkup + @available(*, deprecated, message: "Do not call directly. Required for 'AutomaticDirectiveConvertible'.") + init(originalMarkup: Markdown.BlockDirective) { + self.originalMarkup = originalMarkup + } } } diff --git a/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift b/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift index 3426c0d8cb..134125f5f9 100644 --- a/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift +++ b/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift @@ -24,7 +24,7 @@ import Markdown /// - ``DisplayName`` /// - ``PageImage`` /// - ``CallToAction`` -/// - ``MetadataAvailability`` +/// - ``Availability`` public final class Metadata: Semantic, AutomaticDirectiveConvertible { public let originalMarkup: BlockDirective @@ -51,7 +51,7 @@ public final class Metadata: Semantic, AutomaticDirectiveConvertible { var callToAction: CallToAction? = nil @ChildDirective(requirements: .zeroOrMore) - var availability: [MetadataAvailability] + var availability: [Availability] static var keyPaths: [String : AnyKeyPath] = [ "documentationOptions" : \Metadata._documentationOptions, @@ -152,8 +152,8 @@ public final class Metadata: Semantic, AutomaticDirectiveConvertible { 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", + identifier: "org.swift.docc.\(Metadata.Availability.self).DuplicateIntroduced", + summary: "Duplicate \(Metadata.Availability.directiveName.singleQuoted) directive with 'introduced' argument", explanation: """ A documentation page can only contain a single 'introduced' version for each platform. """ @@ -165,7 +165,7 @@ public final class Metadata: Semantic, AutomaticDirectiveConvertible { } let solution = Solution( - summary: "Remove extraneous \(MetadataAvailability.directiveName.singleQuoted) directive", + summary: "Remove extraneous \(Metadata.Availability.directiveName.singleQuoted) directive", replacements: [ Replacement(range: range, replacement: "") ] diff --git a/Sources/SwiftDocC/Semantics/Symbol/PlatformName.swift b/Sources/SwiftDocC/Semantics/Symbol/PlatformName.swift index 96ede600dd..64d20abf12 100644 --- a/Sources/SwiftDocC/Semantics/Symbol/PlatformName.swift +++ b/Sources/SwiftDocC/Semantics/Symbol/PlatformName.swift @@ -104,10 +104,10 @@ public struct PlatformName: Codable, Hashable, Equatable { /// 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) { + /// Returns `nil` if the given platform was ``Metadata/Availability/Platform-swift.enum/any``. + init?(metadataPlatform platform: Metadata.Availability.Platform) { // Note: This is still an optional initializer to prevent source breakage when - // `MetadataAvailability.Platform` re-introduces the `.any` case + // `Availability.Platform` re-introduces the `.any` case // cf. https://github.com/apple/swift-docc/issues/441 self = .init(operatingSystemName: platform.rawValue) } diff --git a/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift b/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift index 85d057baf1..8283bc497d 100644 --- a/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift @@ -25,8 +25,8 @@ class MetadataAvailabilityTests: XCTestCase { directive.map { directive in var problems = [Problem]() - XCTAssertEqual(MetadataAvailability.directiveName, directive.name) - let availability = MetadataAvailability(from: directive, source: nil, for: bundle, in: context, problems: &problems) + XCTAssertEqual(Metadata.Availability.directiveName, directive.name) + let availability = Metadata.Availability(from: directive, source: nil, for: bundle, in: context, problems: &problems) XCTAssertNil(availability) } } @@ -45,11 +45,11 @@ class MetadataAvailabilityTests: XCTestCase { let _ = Metadata(from: directive, source: nil, for: bundle, in: context, problems: &problems) XCTAssertEqual(2, problems.count) let diagnosticIdentifiers = Set(problems.map { $0.diagnostic.identifier }) - XCTAssertEqual(diagnosticIdentifiers, ["org.swift.docc.\(MetadataAvailability.self).DuplicateIntroduced"]) + XCTAssertEqual(diagnosticIdentifiers, ["org.swift.docc.\(Metadata.Availability.self).DuplicateIntroduced"]) } } - for platform in MetadataAvailability.Platform.allCases { + for platform in Metadata.Availability.Platform.allCases { let source = """ @Metadata { @Available(\(platform.rawValue), introduced: \"1.0\") @@ -74,7 +74,7 @@ class MetadataAvailabilityTests: XCTestCase { validArgumentsWithVersion.append("introduced: \"1.0\", \(arg)") } - for platform in MetadataAvailability.Platform.allCases { + for platform in Metadata.Availability.Platform.allCases { // FIXME: Test validArguments with the `*` platform once that's introduced // cf. https://github.com/apple/swift-docc/issues/441 for args in validArgumentsWithVersion { @@ -115,7 +115,7 @@ class MetadataAvailabilityTests: XCTestCase { } func assertValidAvailability(source: String) throws { - try assertValidDirective(MetadataAvailability.self, source: source) + try assertValidDirective(Metadata.Availability.self, source: source) } func assertValidMetadata(source: String) throws {