Skip to content

Commit ba836db

Browse files
authored
Add support for platform-specific unconditional availability. (#807)
This PR adds support for the following variant of `@available`: ```swift @available(macOS, unavailable) @test func f() {} ``` Previously, we were simply ignoring these availability attributes. Resolves rdar://137776333. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 8cb653d commit ba836db

File tree

6 files changed

+82
-14
lines changed

6 files changed

+82
-14
lines changed

Diff for: Sources/Testing/Traits/ConditionTrait+Macro.swift

+9-2
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,16 @@ extension Trait where Self == ConditionTrait {
9292
_ condition: @escaping @Sendable () -> Bool
9393
) -> Self {
9494
// TODO: Semantic capture of platform name/version (rather than just a comment)
95-
Self(
95+
let message: Comment = if let message {
96+
message
97+
} else if let version {
98+
"Obsolete as of \(_description(ofPlatformName: platformName, version: version))"
99+
} else {
100+
"Unavailable on \(_description(ofPlatformName: platformName, version: nil))"
101+
}
102+
return Self(
96103
kind: .conditional(condition),
97-
comments: [message ?? "Obsolete as of \(_description(ofPlatformName: platformName, version: version))"],
104+
comments: [message],
98105
sourceLocation: sourceLocation
99106
)
100107
}

Diff for: Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift

+8-4
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,14 @@ extension WithAttributesSyntax {
8080
if let lastPlatformName, whenKeyword == .introduced {
8181
return Availability(attribute: attribute, platformName: lastPlatformName, version: nil, message: message)
8282
}
83-
} else if case let .keyword(keyword) = token.tokenKind, keyword == whenKeyword, asteriskEncountered {
84-
// Match the "always this availability" construct, i.e.
85-
// `@available(*, deprecated)` and `@available(*, unavailable)`.
86-
return Availability(attribute: attribute, platformName: lastPlatformName, version: nil, message: message)
83+
} else if case let .keyword(keyword) = token.tokenKind, keyword == whenKeyword {
84+
if asteriskEncountered {
85+
// Match the "always this availability" construct, i.e.
86+
// `@available(*, deprecated)` and `@available(*, unavailable)`.
87+
return Availability(attribute: attribute, platformName: lastPlatformName, version: nil, message: message)
88+
} else if keyword == .unavailable {
89+
return Availability(attribute: attribute, platformName: lastPlatformName, version: nil, message: message)
90+
}
8791
}
8892
case let .availabilityLabeledArgument(argument):
8993
if argument.label.tokenKind == .keyword(whenKeyword), case let .version(version) = argument.value {

Diff for: Sources/TestingMacros/Support/AvailabilityGuards.swift

+40-6
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,25 @@ private func _createAvailabilityTraitExpr(
114114
}
115115
"""
116116

117-
case (.unavailable, _):
118-
return ".__unavailable(message: \(message), sourceLocation: \(sourceLocationExpr))"
117+
case (.unavailable, true):
118+
// @available(swift, unavailable) is unsupported. The compiler emits a
119+
// warning but doesn't prevent calling the function. Emit a no-op.
120+
return ".enabled(if: true)"
121+
122+
case (.unavailable, false):
123+
if let platformName = availability.platformName {
124+
return """
125+
.__available(\(literal: platformName.textWithoutBackticks), obsoleted: nil, message: \(message), sourceLocation: \(sourceLocationExpr)) {
126+
#if os(\(platformName.trimmed))
127+
return false
128+
#else
129+
return true
130+
#endif
131+
}
132+
"""
133+
} else {
134+
return ".__unavailable(message: \(message), sourceLocation: \(sourceLocationExpr))"
135+
}
119136

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

204221
// As above, but for unavailability (`#unavailable(...)`.)
205222
do {
206-
let unavailableExprs: [ExprSyntax] = decl.availability(when: .obsoleted).lazy
223+
let obsoletedExprs: [ExprSyntax] = decl.availability(when: .obsoleted).lazy
207224
.filter { !$0.isSwift }
208225
.compactMap(\.platformVersion)
209226
.map { "#unavailable(\($0))" }
210-
if !unavailableExprs.isEmpty {
227+
if !obsoletedExprs.isEmpty {
211228
let conditionList = ConditionElementListSyntax {
212-
for unavailableExpr in unavailableExprs {
213-
unavailableExpr
229+
for obsoletedExpr in obsoletedExprs {
230+
obsoletedExpr
214231
}
215232
}
216233
result = """
@@ -220,6 +237,23 @@ func createSyntaxNode(
220237
\(result)
221238
"""
222239
}
240+
241+
let unavailableExprs: [ExprSyntax] = decl.availability(when: .unavailable).lazy
242+
.filter { !$0.isSwift }
243+
.filter { $0.version == nil }
244+
.compactMap(\.platformName)
245+
.map { "os(\($0.trimmed))" }
246+
if !unavailableExprs.isEmpty {
247+
for unavailableExpr in unavailableExprs {
248+
result = """
249+
#if \(unavailableExpr)
250+
\(exitStatement)
251+
#else
252+
\(result)
253+
#endif
254+
"""
255+
}
256+
}
223257
}
224258

225259
// If this function has a minimum or maximum Swift version requirement, we

Diff for: Sources/TestingMacros/TestDeclarationMacro.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,9 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
252252

253253
// Generate a thunk function that invokes the actual function.
254254
var thunkBody: CodeBlockItemListSyntax
255-
if functionDecl.availability(when: .unavailable).first != nil {
255+
if functionDecl.availability(when: .unavailable).first(where: { $0.platformVersion == nil }) != nil {
256+
// The function is unconditionally disabled, so don't bother emitting a
257+
// thunk body that calls it.
256258
thunkBody = ""
257259
} else if let typeName {
258260
if functionDecl.isStaticOrClass {

Diff for: Tests/TestingMacrosTests/TestDeclarationMacroTests.swift

+5
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,11 @@ struct TestDeclarationMacroTests {
307307
#".__available("Swift", obsoleted: (2, 0, nil), "#,
308308
#"#if swift(>=1.0) && swift(<2.0)"#,
309309
],
310+
#"@available(moofOS, unavailable, message: "Moof!") @Test func f() {}"#:
311+
[
312+
#"#if os(moofOS)"#,
313+
#".__available("moofOS", obsoleted: nil, message: "Moof!", "#,
314+
]
310315
]
311316
)
312317
func availabilityAttributeCapture(input: String, expectedOutputs: [String]) throws {

Diff for: Tests/TestingTests/RunnerTests.swift

+17-1
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,14 @@ final class RunnerTests: XCTestCase {
637637
@available(macOS 999.0, iOS 999.0, watchOS 999.0, tvOS 999.0, visionOS 999.0, *)
638638
func futureAvailable() {}
639639

640+
@Test(.hidden)
641+
@available(macOS, unavailable)
642+
@available(iOS, unavailable)
643+
@available(watchOS, unavailable)
644+
@available(tvOS, unavailable)
645+
@available(visionOS, unavailable)
646+
func perPlatformUnavailable() {}
647+
640648
@Test(.hidden)
641649
@available(macOS, introduced: 999.0)
642650
@available(iOS, introduced: 999.0)
@@ -674,7 +682,7 @@ final class RunnerTests: XCTestCase {
674682
let testSkipped = expectation(description: "Test skipped")
675683
#if SWT_TARGET_OS_APPLE
676684
testStarted.expectedFulfillmentCount = 4
677-
testSkipped.expectedFulfillmentCount = 7
685+
testSkipped.expectedFulfillmentCount = 8
678686
#else
679687
testStarted.expectedFulfillmentCount = 2
680688
testSkipped.expectedFulfillmentCount = 2
@@ -719,6 +727,14 @@ final class RunnerTests: XCTestCase {
719727
func unavailable() {}
720728

721729
#if SWT_TARGET_OS_APPLE
730+
@Test(.hidden)
731+
@available(macOS, unavailable, message: "Expected Message")
732+
@available(iOS, unavailable, message: "Expected Message")
733+
@available(watchOS, unavailable, message: "Expected Message")
734+
@available(tvOS, unavailable, message: "Expected Message")
735+
@available(visionOS, unavailable, message: "Expected Message")
736+
func perPlatformUnavailable() {}
737+
722738
@Test(.hidden)
723739
@available(macOS, introduced: 999.0, message: "Expected Message")
724740
@available(iOS, introduced: 999.0, message: "Expected Message")

0 commit comments

Comments
 (0)