Skip to content

Add support for platform-specific unconditional availability. #807

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
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
11 changes: 9 additions & 2 deletions Sources/Testing/Traits/ConditionTrait+Macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,16 @@ extension Trait where Self == ConditionTrait {
_ condition: @escaping @Sendable () -> Bool
) -> Self {
// TODO: Semantic capture of platform name/version (rather than just a comment)
Self(
let message: Comment = if let message {
message
} else if let version {
"Obsolete as of \(_description(ofPlatformName: platformName, version: version))"
} else {
"Unavailable on \(_description(ofPlatformName: platformName, version: nil))"
}
return Self(
kind: .conditional(condition),
comments: [message ?? "Obsolete as of \(_description(ofPlatformName: platformName, version: version))"],
comments: [message],
sourceLocation: sourceLocation
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,14 @@ extension WithAttributesSyntax {
if let lastPlatformName, whenKeyword == .introduced {
return Availability(attribute: attribute, platformName: lastPlatformName, version: nil, message: message)
}
} else if case let .keyword(keyword) = token.tokenKind, keyword == whenKeyword, asteriskEncountered {
// Match the "always this availability" construct, i.e.
// `@available(*, deprecated)` and `@available(*, unavailable)`.
return Availability(attribute: attribute, platformName: lastPlatformName, version: nil, message: message)
} else if case let .keyword(keyword) = token.tokenKind, keyword == whenKeyword {
if asteriskEncountered {
// Match the "always this availability" construct, i.e.
// `@available(*, deprecated)` and `@available(*, unavailable)`.
return Availability(attribute: attribute, platformName: lastPlatformName, version: nil, message: message)
} else if keyword == .unavailable {
return Availability(attribute: attribute, platformName: lastPlatformName, version: nil, message: message)
}
}
case let .availabilityLabeledArgument(argument):
if argument.label.tokenKind == .keyword(whenKeyword), case let .version(version) = argument.value {
Expand Down
46 changes: 40 additions & 6 deletions Sources/TestingMacros/Support/AvailabilityGuards.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,25 @@ private func _createAvailabilityTraitExpr(
}
"""

case (.unavailable, _):
return ".__unavailable(message: \(message), sourceLocation: \(sourceLocationExpr))"
case (.unavailable, true):
// @available(swift, unavailable) is unsupported. The compiler emits a
// warning but doesn't prevent calling the function. Emit a no-op.
return ".enabled(if: true)"

case (.unavailable, false):
if let platformName = availability.platformName {
return """
.__available(\(literal: platformName.textWithoutBackticks), obsoleted: nil, message: \(message), sourceLocation: \(sourceLocationExpr)) {
#if os(\(platformName.trimmed))
return false
#else
return true
#endif
}
"""
} else {
return ".__unavailable(message: \(message), sourceLocation: \(sourceLocationExpr))"
}

default:
fatalError("Unsupported keyword \(whenKeyword) passed to \(#function). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new")
Expand Down Expand Up @@ -203,14 +220,14 @@ func createSyntaxNode(

// As above, but for unavailability (`#unavailable(...)`.)
do {
let unavailableExprs: [ExprSyntax] = decl.availability(when: .obsoleted).lazy
let obsoletedExprs: [ExprSyntax] = decl.availability(when: .obsoleted).lazy
.filter { !$0.isSwift }
.compactMap(\.platformVersion)
.map { "#unavailable(\($0))" }
if !unavailableExprs.isEmpty {
if !obsoletedExprs.isEmpty {
let conditionList = ConditionElementListSyntax {
for unavailableExpr in unavailableExprs {
unavailableExpr
for obsoletedExpr in obsoletedExprs {
obsoletedExpr
}
}
result = """
Expand All @@ -220,6 +237,23 @@ func createSyntaxNode(
\(result)
"""
}

let unavailableExprs: [ExprSyntax] = decl.availability(when: .unavailable).lazy
.filter { !$0.isSwift }
.filter { $0.version == nil }
.compactMap(\.platformName)
.map { "os(\($0.trimmed))" }
if !unavailableExprs.isEmpty {
for unavailableExpr in unavailableExprs {
result = """
#if \(unavailableExpr)
\(exitStatement)
#else
\(result)
#endif
"""
}
}
}

// If this function has a minimum or maximum Swift version requirement, we
Expand Down
4 changes: 3 additions & 1 deletion Sources/TestingMacros/TestDeclarationMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,9 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {

// Generate a thunk function that invokes the actual function.
var thunkBody: CodeBlockItemListSyntax
if functionDecl.availability(when: .unavailable).first != nil {
if functionDecl.availability(when: .unavailable).first(where: { $0.platformVersion == nil }) != nil {
// The function is unconditionally disabled, so don't bother emitting a
// thunk body that calls it.
thunkBody = ""
} else if let typeName {
if functionDecl.isStaticOrClass {
Expand Down
5 changes: 5 additions & 0 deletions Tests/TestingMacrosTests/TestDeclarationMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,11 @@ struct TestDeclarationMacroTests {
#".__available("Swift", obsoleted: (2, 0, nil), "#,
#"#if swift(>=1.0) && swift(<2.0)"#,
],
#"@available(moofOS, unavailable, message: "Moof!") @Test func f() {}"#:
[
#"#if os(moofOS)"#,
#".__available("moofOS", obsoleted: nil, message: "Moof!", "#,
]
]
)
func availabilityAttributeCapture(input: String, expectedOutputs: [String]) throws {
Expand Down
18 changes: 17 additions & 1 deletion Tests/TestingTests/RunnerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,14 @@ final class RunnerTests: XCTestCase {
@available(macOS 999.0, iOS 999.0, watchOS 999.0, tvOS 999.0, visionOS 999.0, *)
func futureAvailable() {}

@Test(.hidden)
@available(macOS, unavailable)
@available(iOS, unavailable)
@available(watchOS, unavailable)
@available(tvOS, unavailable)
@available(visionOS, unavailable)
func perPlatformUnavailable() {}

@Test(.hidden)
@available(macOS, introduced: 999.0)
@available(iOS, introduced: 999.0)
Expand Down Expand Up @@ -674,7 +682,7 @@ final class RunnerTests: XCTestCase {
let testSkipped = expectation(description: "Test skipped")
#if SWT_TARGET_OS_APPLE
testStarted.expectedFulfillmentCount = 4
testSkipped.expectedFulfillmentCount = 7
testSkipped.expectedFulfillmentCount = 8
#else
testStarted.expectedFulfillmentCount = 2
testSkipped.expectedFulfillmentCount = 2
Expand Down Expand Up @@ -719,6 +727,14 @@ final class RunnerTests: XCTestCase {
func unavailable() {}

#if SWT_TARGET_OS_APPLE
@Test(.hidden)
@available(macOS, unavailable, message: "Expected Message")
@available(iOS, unavailable, message: "Expected Message")
@available(watchOS, unavailable, message: "Expected Message")
@available(tvOS, unavailable, message: "Expected Message")
@available(visionOS, unavailable, message: "Expected Message")
func perPlatformUnavailable() {}

@Test(.hidden)
@available(macOS, introduced: 999.0, message: "Expected Message")
@available(iOS, introduced: 999.0, message: "Expected Message")
Expand Down