diff --git a/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift index aa778a00c..3c42b6fb3 100644 --- a/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift @@ -61,16 +61,17 @@ extension WithAttributesSyntax { }.first var lastPlatformName: TokenSyntax? = nil - var asteriskEncountered = false + var wildcardEncountered = false + let hasWildcard = entries.contains(where: \.isWildcard) return entries.compactMap { entry in switch entry { case let .availabilityVersionRestriction(restriction) where whenKeyword == .introduced: - return Availability(attribute: attribute, platformName: restriction.platform, version: restriction.version, message: message) + return Availability(attribute: attribute, platformName: restriction.platform, version: restriction.version, mayNeedTrailingWildcard: hasWildcard, message: message) case let .token(token): if case .identifier = token.tokenKind { lastPlatformName = token - } else if case let .binaryOperator(op) = token.tokenKind, op == "*" { - asteriskEncountered = true + } else if entry.isWildcard { + wildcardEncountered = true // It is syntactically valid to specify a platform name without a // version in an availability declaration, and it's used to resolve // a custom availability definition specified via the @@ -81,7 +82,7 @@ extension WithAttributesSyntax { return Availability(attribute: attribute, platformName: lastPlatformName, version: nil, message: message) } } else if case let .keyword(keyword) = token.tokenKind, keyword == whenKeyword { - if asteriskEncountered { + if wildcardEncountered { // Match the "always this availability" construct, i.e. // `@available(*, deprecated)` and `@available(*, unavailable)`. return Availability(attribute: attribute, platformName: lastPlatformName, version: nil, message: message) @@ -144,3 +145,13 @@ extension AttributeSyntax { .joined() } } + +extension AvailabilityArgumentSyntax.Argument { + var isWildcard: Bool { + if case let .token(token) = self, + case let .binaryOperator(op) = token.tokenKind, op == "*" { + return true + } + return false + } +} diff --git a/Sources/TestingMacros/Support/AvailabilityGuards.swift b/Sources/TestingMacros/Support/AvailabilityGuards.swift index deb3a0f8b..93451170c 100644 --- a/Sources/TestingMacros/Support/AvailabilityGuards.swift +++ b/Sources/TestingMacros/Support/AvailabilityGuards.swift @@ -24,6 +24,10 @@ struct Availability { /// The platform version, such as 1.2.3, if any. var version: VersionTupleSyntax? + /// Whether or not this availability attribute may need a trailing wildcard + /// (`*`) when it is expanded into `@available()` or `#available()`. + var mayNeedTrailingWildcard = true + /// The `message` argument to the attribute, if any. var message: SimpleStringLiteralExprSyntax? @@ -70,13 +74,14 @@ private func _createAvailabilityTraitExpr( "(\(literal: components.major), \(literal: components.minor), \(literal: components.patch))" } ?? "nil" let message = availability.message.map(\.trimmed).map(ExprSyntax.init) ?? "nil" + let trailingWildcard = availability.mayNeedTrailingWildcard ? ", *" : "" let sourceLocationExpr = createSourceLocationExpr(of: availability.attribute, context: context) switch (whenKeyword, availability.isSwift) { case (.introduced, false): return """ .__available(\(literal: availability.platformName!.textWithoutBackticks), introduced: \(version), message: \(message), sourceLocation: \(sourceLocationExpr)) { - if #available(\(availability.platformVersion!), *) { + if #available(\(availability.platformVersion!)\(raw: trailingWildcard)) { return true } return false @@ -207,8 +212,8 @@ func createSyntaxNode( do { let availableExprs: [ExprSyntax] = decl.availability(when: .introduced).lazy .filter { !$0.isSwift } - .compactMap(\.platformVersion) - .map { "#available(\($0), *)" } + .compactMap { ($0.platformVersion, $0.mayNeedTrailingWildcard ? ", *" : "") } + .map { "#available(\($0.0)\(raw: $0.1))" } if !availableExprs.isEmpty { let conditionList = ConditionElementListSyntax { for availableExpr in availableExprs { diff --git a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift index b65a6a62e..aec6d2c10 100644 --- a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift +++ b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift @@ -415,7 +415,12 @@ struct TestDeclarationMacroTests { [ #"#if os(moofOS)"#, #".__available("moofOS", obsoleted: nil, message: "Moof!", "#, - ] + ], + #"@available(customAvailabilityDomain) @Test func f() {}"#: + [ + #".__available("customAvailabilityDomain", introduced: nil, "#, + #"guard #available (customAvailabilityDomain) else"#, + ], ] ) func availabilityAttributeCapture(input: String, expectedOutputs: [String]) throws {