Skip to content
This repository has been archived by the owner on Jun 1, 2023. It is now read-only.

Add availabilityAttributes property to Symbol #106

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions Sources/SwiftDoc/Available.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
6 changes: 6 additions & 0 deletions Sources/SwiftDoc/Symbol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
214 changes: 214 additions & 0 deletions Tests/SwiftDocTests/AvailableTests.swift
Original file line number Diff line number Diff line change
@@ -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]
}
}