From 8341a0b68d577743050abb32b8bf21a3bbf21299 Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Mon, 11 May 2020 22:15:22 -0500 Subject: [PATCH 1/2] Add availability property to Symbol --- Sources/SwiftDoc/AvailabilityAttribute.swift | 65 +++++++++++++++++++ Sources/SwiftDoc/Symbol.swift | 6 ++ .../AvailabilityAttributeTests.swift | 38 +++++++++++ 3 files changed, 109 insertions(+) create mode 100644 Sources/SwiftDoc/AvailabilityAttribute.swift create mode 100644 Tests/SwiftDocTests/AvailabilityAttributeTests.swift diff --git a/Sources/SwiftDoc/AvailabilityAttribute.swift b/Sources/SwiftDoc/AvailabilityAttribute.swift new file mode 100644 index 00000000..7f02c8f2 --- /dev/null +++ b/Sources/SwiftDoc/AvailabilityAttribute.swift @@ -0,0 +1,65 @@ +import SwiftSemantics + +public enum AvailabilityAttributeType: Equatable { + /// all platforms (marked with *) + case allPlatforms + case platform(platform: String, version: String?) + case introduced(version: String?) + case deprecated(version: String?) + case obsoleted(version: String?) + case renamed(message: String) + case message(message: String) + case unavailable(version: String?) +} + +extension AvailabilityAttributeType { + init?(from argument: Attribute.Argument) { + switch argument.value { + case "*": + self = .allPlatforms + return + case "introduced": + self = .introduced(version: nil) + return + case "deprecated": + self = .deprecated(version: nil) + return + case "obsoleted": + self = .obsoleted(version: nil) + return + case "unavailable": + self = .unavailable(version: nil) + return + case "iOS", "macOS", "tvOS", "watchOS", "swift": + self = .platform(platform: argument.value, version: nil) + default: + guard let name = argument.name else { + return nil + } + + switch name { + case "iOS", "macOS", "tvOS", "watchOS", "swift": + self = .platform(platform: name, version: argument.value) + case "introduced": + self = .introduced(version: argument.value) + case "deprecated": + self = .deprecated(version: argument.value) + case "obsoleted": + self = .obsoleted(version: argument.value) + case "renamed": + self = .renamed(message: argument.value) + case "message": + self = .message(message: argument.value) + default: return nil + } + } + } +} + +public final class AvailabilityAttribute { + public let attributes: [AvailabilityAttributeType] + + init(arguments: [Attribute.Argument]) { + attributes = arguments.compactMap { AvailabilityAttributeType.init(from: $0) } + } +} \ No newline at end of file diff --git a/Sources/SwiftDoc/Symbol.swift b/Sources/SwiftDoc/Symbol.swift index c41f5933..befd2ed6 100644 --- a/Sources/SwiftDoc/Symbol.swift +++ b/Sources/SwiftDoc/Symbol.swift @@ -62,6 +62,12 @@ public final class Symbol { public var isDocumented: Bool { return documentation?.isEmpty == false } + + public var availabilityAttributes: [AvailabilityAttribute] { + let availableAttributes = api.attributes.filter({ $0.name == "available" }) + + return availableAttributes.compactMap { AvailabilityAttribute(arguments: $0.arguments) } + } } // MARK: - Equatable diff --git a/Tests/SwiftDocTests/AvailabilityAttributeTests.swift b/Tests/SwiftDocTests/AvailabilityAttributeTests.swift new file mode 100644 index 00000000..f82e3f56 --- /dev/null +++ b/Tests/SwiftDocTests/AvailabilityAttributeTests.swift @@ -0,0 +1,38 @@ +import XCTest + +import SwiftDoc +import SwiftSemantics +import struct SwiftSemantics.Protocol +import SwiftSyntax + +final class AvailabilityAttributeTypesTests: XCTestCase { + func testBasicAvailabilityType() throws { + let source = #""" + + @available(iOS, deprecated: 13, renamed: "NewAndImprovedViewController") + class OldViewController: UIViewController { } + + """# + + let url = try temporaryFile(contents: source) + let sourceFile = try SourceFile(file: url, relativeTo: url.deletingLastPathComponent()) + + let symbol = sourceFile.symbols[0] + XCTAssert(symbol.api is Class) + + XCTAssertEqual(symbol.availabilityAttributes.count, 1) + + let attributes = symbol.availabilityAttributes.first!.attributes + XCTAssertEqual(attributes.count, 3) + + let iOS = attributes[0] + XCTAssertEqual(iOS, AvailabilityAttributeType.platform(platform: "iOS", version: nil)) + + let deprecation = attributes[1] + XCTAssertEqual(deprecation, AvailabilityAttributeType.deprecated(version: "13")) + + let renamed = attributes[2] + XCTAssertEqual(renamed, AvailabilityAttributeType.renamed(message: "\"NewAndImprovedViewController\"")) + + } +} \ No newline at end of file From cb1a8bc73e8e2dd8937dccc778fdc46b91177d14 Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Fri, 29 May 2020 09:56:37 -0500 Subject: [PATCH 2/2] Refactor Availability. Add more tests --- Sources/SwiftDoc/AvailabilityAttribute.swift | 65 ------ Sources/SwiftDoc/Available.swift | 102 +++++++++ Sources/SwiftDoc/Symbol.swift | 6 +- .../AvailabilityAttributeTests.swift | 38 ---- Tests/SwiftDocTests/AvailableTests.swift | 214 ++++++++++++++++++ 5 files changed, 319 insertions(+), 106 deletions(-) delete mode 100644 Sources/SwiftDoc/AvailabilityAttribute.swift create mode 100644 Sources/SwiftDoc/Available.swift delete mode 100644 Tests/SwiftDocTests/AvailabilityAttributeTests.swift create mode 100644 Tests/SwiftDocTests/AvailableTests.swift diff --git a/Sources/SwiftDoc/AvailabilityAttribute.swift b/Sources/SwiftDoc/AvailabilityAttribute.swift deleted file mode 100644 index 7f02c8f2..00000000 --- a/Sources/SwiftDoc/AvailabilityAttribute.swift +++ /dev/null @@ -1,65 +0,0 @@ -import SwiftSemantics - -public enum AvailabilityAttributeType: Equatable { - /// all platforms (marked with *) - case allPlatforms - case platform(platform: String, version: String?) - case introduced(version: String?) - case deprecated(version: String?) - case obsoleted(version: String?) - case renamed(message: String) - case message(message: String) - case unavailable(version: String?) -} - -extension AvailabilityAttributeType { - init?(from argument: Attribute.Argument) { - switch argument.value { - case "*": - self = .allPlatforms - return - case "introduced": - self = .introduced(version: nil) - return - case "deprecated": - self = .deprecated(version: nil) - return - case "obsoleted": - self = .obsoleted(version: nil) - return - case "unavailable": - self = .unavailable(version: nil) - return - case "iOS", "macOS", "tvOS", "watchOS", "swift": - self = .platform(platform: argument.value, version: nil) - default: - guard let name = argument.name else { - return nil - } - - switch name { - case "iOS", "macOS", "tvOS", "watchOS", "swift": - self = .platform(platform: name, version: argument.value) - case "introduced": - self = .introduced(version: argument.value) - case "deprecated": - self = .deprecated(version: argument.value) - case "obsoleted": - self = .obsoleted(version: argument.value) - case "renamed": - self = .renamed(message: argument.value) - case "message": - self = .message(message: argument.value) - default: return nil - } - } - } -} - -public final class AvailabilityAttribute { - public let attributes: [AvailabilityAttributeType] - - init(arguments: [Attribute.Argument]) { - attributes = arguments.compactMap { AvailabilityAttributeType.init(from: $0) } - } -} \ No newline at end of file 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 befd2ed6..22f5a084 100644 --- a/Sources/SwiftDoc/Symbol.swift +++ b/Sources/SwiftDoc/Symbol.swift @@ -63,10 +63,10 @@ public final class Symbol { return documentation?.isEmpty == false } - public var availabilityAttributes: [AvailabilityAttribute] { + public var availabilityAttributes: [Availability] { let availableAttributes = api.attributes.filter({ $0.name == "available" }) - - return availableAttributes.compactMap { AvailabilityAttribute(arguments: $0.arguments) } + + return availableAttributes.compactMap { Availability(arguments: $0.arguments) } } } diff --git a/Tests/SwiftDocTests/AvailabilityAttributeTests.swift b/Tests/SwiftDocTests/AvailabilityAttributeTests.swift deleted file mode 100644 index f82e3f56..00000000 --- a/Tests/SwiftDocTests/AvailabilityAttributeTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -import XCTest - -import SwiftDoc -import SwiftSemantics -import struct SwiftSemantics.Protocol -import SwiftSyntax - -final class AvailabilityAttributeTypesTests: XCTestCase { - func testBasicAvailabilityType() throws { - let source = #""" - - @available(iOS, deprecated: 13, renamed: "NewAndImprovedViewController") - class OldViewController: UIViewController { } - - """# - - let url = try temporaryFile(contents: source) - let sourceFile = try SourceFile(file: url, relativeTo: url.deletingLastPathComponent()) - - let symbol = sourceFile.symbols[0] - XCTAssert(symbol.api is Class) - - XCTAssertEqual(symbol.availabilityAttributes.count, 1) - - let attributes = symbol.availabilityAttributes.first!.attributes - XCTAssertEqual(attributes.count, 3) - - let iOS = attributes[0] - XCTAssertEqual(iOS, AvailabilityAttributeType.platform(platform: "iOS", version: nil)) - - let deprecation = attributes[1] - XCTAssertEqual(deprecation, AvailabilityAttributeType.deprecated(version: "13")) - - let renamed = attributes[2] - XCTAssertEqual(renamed, AvailabilityAttributeType.renamed(message: "\"NewAndImprovedViewController\"")) - - } -} \ No newline at end of file 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] + } +} +