diff --git a/Sources/SwiftDoc/Available.swift b/Sources/SwiftDoc/Available.swift new file mode 100644 index 00000000..ea29a119 --- /dev/null +++ b/Sources/SwiftDoc/Available.swift @@ -0,0 +1,102 @@ +import SwiftSemantics + +public struct PlatformAvailability { + public let platform: String + public let version: String? // Semver/Version? + + /// Returns true when this represents the '*' case. + public func isOtherPlatform() -> Bool { return version == nil && platform == "*"} +} + +public enum AvailabilityKind: Equatable { + case introduced(version: String) + case obsoleted(version: String) + case renamed(message: String) + case message(message: String) + case deprecated(version: String?) + case unavailable +} + +extension AvailabilityKind { + init?(name: String?, value: String) { + if let name = name { + switch name { + case "introduced": + self = .introduced(version: value) + case "obsoleted": + self = .obsoleted(version: value) + case "renamed": + self = .renamed(message: value) + case "message": + self = .message(message: value) + case "deprecated": + self = .deprecated(version: value) + default: + return nil + } + } else { + // check if unavailable or deprecated (kinds that don't require values) + switch value { + case "deprecated": + self = .deprecated(version: nil) + case "unavailable": + self = .unavailable + default: + return nil + } + + } + } +} + + +public final class Availability { + public let platforms: [PlatformAvailability] + public let attributes: [AvailabilityKind] + + init(arguments: [Attribute.Argument]) { + var platforms: [PlatformAvailability] = [] + var attributes: [AvailabilityKind] = [] + + arguments.forEach { argument in + if let availabilityKind = AvailabilityKind(name: argument.name, value: argument.value) { + attributes.append(availabilityKind) + } else { + if let platform = PlatformAvailability(from: argument) { + platforms.append(platform) + } + } + } + + self.platforms = platforms + self.attributes = attributes + } +} + + +extension PlatformAvailability { + init?(from argument: Attribute.Argument) { + + // Shorthand from SwiftSemantics.Attribute.Argument will have both name and version in `value` property + // example: @available(macOS 10.15, iOS 13, *) + if argument.name == nil { + let components = argument.value.split(separator: " ", maxSplits: 1) + if components.count == 2, + let platform = components.first, + let version = components.last + { + self.platform = String(platform) + self.version = String(version) + } + else { + // example: @available(iOS, deprecated: 13, renamed: "NewAndImprovedViewController") + // this will be the `iOS` portion. Will also be the * in otherPlatform cases + self.platform = argument.value + self.version = nil + } + } else { + // There is no name, so it includes a colon (:) so lets try an AvailabilityKind + return nil + } + } +} diff --git a/Sources/SwiftDoc/Symbol.swift b/Sources/SwiftDoc/Symbol.swift index 02514f8e..401aaf92 100644 --- a/Sources/SwiftDoc/Symbol.swift +++ b/Sources/SwiftDoc/Symbol.swift @@ -65,6 +65,12 @@ public final class Symbol { public var isDocumented: Bool { return documentation?.isEmpty == false } + + public var availabilityAttributes: [Availability] { + let availableAttributes = api.attributes.filter({ $0.name == "available" }) + + return availableAttributes.compactMap { Availability(arguments: $0.arguments) } + } } // MARK: - Equatable diff --git a/Tests/SwiftDocTests/AvailableTests.swift b/Tests/SwiftDocTests/AvailableTests.swift new file mode 100644 index 00000000..9f0f63af --- /dev/null +++ b/Tests/SwiftDocTests/AvailableTests.swift @@ -0,0 +1,214 @@ +import XCTest + +import SwiftDoc +import SwiftSemantics +import struct SwiftSemantics.Protocol +import SwiftSyntax + +final class AvailabilityTests: XCTestCase { + + func testShortHandAvailabilityMultiplePlatforms() throws { + let source = #""" + + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + protocol test { } + + """# + let symbol = try! firstSymbol(fromString: source) + XCTAssertEqual(symbol.availabilityAttributes.count, 1) + + let availability = symbol.availabilityAttributes.first! + XCTAssertEqual(availability.attributes.count, 0) + XCTAssertEqual(availability.platforms.count, 5) + + XCTAssertEqual(availability.platforms[0].platform, "macOS") + XCTAssertEqual(availability.platforms[1].platform, "iOS") + XCTAssertEqual(availability.platforms[2].platform, "watchOS") + XCTAssertEqual(availability.platforms[3].platform, "tvOS") + XCTAssertFalse(availability.platforms[3].isOtherPlatform()) + XCTAssertEqual(availability.platforms[4].platform, "*") + XCTAssertTrue(availability.platforms[4].isOtherPlatform()) + + XCTAssertEqual(availability.platforms[0].version, "10.15") + XCTAssertEqual(availability.platforms[1].version, "13") + XCTAssertEqual(availability.platforms[2].version, "6") + XCTAssertEqual(availability.platforms[3].version, "13") + + XCTAssertNil(availability.platforms[4].version) + } + + func testUnavailableAvailability() throws { + let source = #""" + + @available(tvOS, unavailable) + protocol test { } + + """# + let symbol = try! firstSymbol(fromString: source) + XCTAssertEqual(symbol.availabilityAttributes.count, 1) + + let availability = symbol.availabilityAttributes.first! + XCTAssertEqual(availability.attributes.count, 1) + XCTAssertEqual(availability.platforms.count, 1) + + XCTAssertEqual(availability.platforms[0].platform, "tvOS") + XCTAssertNil(availability.platforms[0].version) + + XCTAssertEqual(availability.attributes[0], AvailabilityKind.unavailable) + } + + func testDepcrecatedNoVersionAvailability() throws { + let source = #""" + + @available(iOS, deprecated) + protocol test { } + + """# + let symbol = try! firstSymbol(fromString: source) + XCTAssertEqual(symbol.availabilityAttributes.count, 1) + + let availability = symbol.availabilityAttributes.first! + XCTAssertEqual(availability.attributes.count, 1) + XCTAssertEqual(availability.platforms.count, 1) + + XCTAssertEqual(availability.platforms[0].platform, "iOS") + XCTAssertNil(availability.platforms[0].version) + + XCTAssertEqual(availability.attributes[0], AvailabilityKind.deprecated(version: nil)) + } + + func testDepcrecatedWithVersionAvailability() throws { + let source = #""" + + @available(iOS, deprecated: 2.5) + protocol test { } + + """# + let symbol = try! firstSymbol(fromString: source) + XCTAssertEqual(symbol.availabilityAttributes.count, 1) + + let availability = symbol.availabilityAttributes.first! + XCTAssertEqual(availability.attributes.count, 1) + XCTAssertEqual(availability.platforms.count, 1) + + XCTAssertEqual(availability.platforms[0].platform, "iOS") + XCTAssertNil(availability.platforms[0].version) + + XCTAssertEqual(availability.attributes[0], AvailabilityKind.deprecated(version: "2.5")) + } + + func testMessageAvailability() throws { + let source = #""" + + @available(*, message: "this is no longer used") + protocol test { } + + """# + let symbol = try! firstSymbol(fromString: source) + XCTAssertEqual(symbol.availabilityAttributes.count, 1) + + let availability = symbol.availabilityAttributes.first! + XCTAssertEqual(availability.attributes.count, 1) + XCTAssertEqual(availability.platforms.count, 1) + + XCTAssertEqual(availability.platforms[0].platform, "*") + XCTAssertNil(availability.platforms[0].version) + XCTAssertTrue(availability.platforms[0].isOtherPlatform()) + + XCTAssertEqual(availability.attributes[0], AvailabilityKind.message(message: #""this is no longer used""#)) + } + + func testRenamedAvailability() throws { + let source = #""" + + @available(*, renamed: "SomeNewProtcol") + protocol test { } + + """# + let symbol = try! firstSymbol(fromString: source) + XCTAssertEqual(symbol.availabilityAttributes.count, 1) + + let availability = symbol.availabilityAttributes.first! + XCTAssertEqual(availability.attributes.count, 1) + XCTAssertEqual(availability.platforms.count, 1) + + XCTAssertEqual(availability.platforms[0].platform, "*") + XCTAssertNil(availability.platforms[0].version) + XCTAssertTrue(availability.platforms[0].isOtherPlatform()) + + XCTAssertEqual(availability.attributes[0], AvailabilityKind.renamed(message: #""SomeNewProtcol""#)) + } + + func testObseletedAvailability() throws { + let source = #""" + + @available(iOS, obsoleted: 2.0) + protocol test { } + + """# + let symbol = try! firstSymbol(fromString: source) + XCTAssertEqual(symbol.availabilityAttributes.count, 1) + + let availability = symbol.availabilityAttributes.first! + XCTAssertEqual(availability.attributes.count, 1) + XCTAssertEqual(availability.platforms.count, 1) + + XCTAssertEqual(availability.platforms[0].platform, "iOS") + XCTAssertNil(availability.platforms[0].version) + XCTAssertFalse(availability.platforms[0].isOtherPlatform()) + + XCTAssertEqual(availability.attributes[0], AvailabilityKind.obsoleted(version: "2.0")) + } + + func testIntroducedAvailability() throws { + let source = #""" + + @available(iOS, introduced: 2.0) + protocol test { } + + """# + let symbol = try! firstSymbol(fromString: source) + XCTAssertEqual(symbol.availabilityAttributes.count, 1) + + let availability = symbol.availabilityAttributes.first! + XCTAssertEqual(availability.attributes.count, 1) + XCTAssertEqual(availability.platforms.count, 1) + + XCTAssertEqual(availability.platforms[0].platform, "iOS") + XCTAssertNil(availability.platforms[0].version) + XCTAssertFalse(availability.platforms[0].isOtherPlatform()) + + XCTAssertEqual(availability.attributes[0], AvailabilityKind.introduced(version: "2.0")) + } + + func testMultipleAvailability() throws { + let source = #""" + + @available(*, introduced: 2.0, deprecated: 2.1, renamed: "NewProtocol", message: "some message") + protocol test { } + + """# + let symbol = try! firstSymbol(fromString: source) + XCTAssertEqual(symbol.availabilityAttributes.count, 1) + + let availability = symbol.availabilityAttributes.first! + XCTAssertEqual(availability.attributes.count, 4) + XCTAssertEqual(availability.platforms.count, 1) + + XCTAssertEqual(availability.platforms[0].platform, "*") + XCTAssertNil(availability.platforms[0].version) + XCTAssertTrue(availability.platforms[0].isOtherPlatform()) + + XCTAssertEqual(availability.attributes[0], AvailabilityKind.introduced(version: "2.0")) + XCTAssertEqual(availability.attributes[1], AvailabilityKind.deprecated(version: "2.1")) + XCTAssertEqual(availability.attributes[2], AvailabilityKind.renamed(message: #""NewProtocol""#)) + XCTAssertEqual(availability.attributes[3], AvailabilityKind.message(message: #""some message""#)) + } + + func firstSymbol(fromString string: String) throws -> Symbol { + let url = try temporaryFile(contents: string) + let sourceFile = try SourceFile(file: url, relativeTo: url.deletingLastPathComponent()) + return sourceFile.symbols[0] + } +} +