Skip to content

Commit 010c505

Browse files
committed
Do not perform full expansion of #expect() when try or await is present.
For reasons that have been documented at length (see #162), we aren't able to correctly expand conditions on `#expect()` or `#require()` that have effects (`try` or `await`.) We aren't currently detecting all possible patterns that expand incorrectly. This PR causes us to back out of the full expansion if the `try` or `await` keyword is present _anywhere_ inside an expectation condition expression. Resolves #783.
1 parent 5f166be commit 010c505

File tree

2 files changed

+25
-16
lines changed

2 files changed

+25
-16
lines changed

Sources/TestingMacros/Support/ConditionArgumentParsing.swift

+16-16
Original file line numberDiff line numberDiff line change
@@ -312,17 +312,6 @@ private func _exprFromOptionalChainedExpr(_ expr: some ExprSyntaxProtocol) -> (E
312312
///
313313
/// - Returns: An instance of ``Condition`` describing `expr`.
314314
private func _parseCondition(from expr: FunctionCallExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition {
315-
// If the member function call involves the `try` or `await` keywords, assume
316-
// we cannot expand it. This check cannot handle expressions like
317-
// `try #expect(a.b(c))` where `b()` is throwing because the `try` keyword
318-
// is outside the macro expansion. SEE: rdar://109470248
319-
let containsTryOrAwait = expr.tokens(viewMode: .sourceAccurate).lazy
320-
.map(\.tokenKind)
321-
.contains { $0 == .keyword(.try) || $0 == .keyword(.await) }
322-
if containsTryOrAwait {
323-
return Condition(expression: expr)
324-
}
325-
326315
// We do not support function calls with trailing closures because the
327316
// transform required to forward them requires more information than is
328317
// available solely from the syntax tree.
@@ -477,6 +466,22 @@ private func _parseCondition(negating expr: ExprSyntax, isParenthetical: Bool, f
477466
///
478467
/// - Returns: An instance of ``Condition`` describing `expr`.
479468
private func _parseCondition(from expr: ExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition {
469+
// Handle closures with a single expression in them (e.g. { $0.foo() })
470+
if let closureExpr = expr.as(ClosureExprSyntax.self) {
471+
return _parseCondition(from: closureExpr, for: macro, in: context)
472+
}
473+
474+
// If the condition involves the `try` or `await` keywords, assume we cannot
475+
// expand it. This check cannot handle expressions like
476+
// `try #expect(a.b(c))` where `b()` is throwing because the `try` keyword is
477+
// outside the macro expansion. SEE: rdar://109470248
478+
let containsTryOrAwait = expr.tokens(viewMode: .sourceAccurate).lazy
479+
.map(\.tokenKind)
480+
.contains { $0 == .keyword(.try) || $0 == .keyword(.await) }
481+
if containsTryOrAwait {
482+
return Condition(expression: expr)
483+
}
484+
480485
if let infixOperator = expr.as(InfixOperatorExprSyntax.self),
481486
let op = infixOperator.operator.as(BinaryOperatorExprSyntax.self) {
482487
return _parseCondition(from: expr, leftOperand: infixOperator.leftOperand, operator: op, rightOperand: infixOperator.rightOperand, for: macro, in: context)
@@ -489,11 +494,6 @@ private func _parseCondition(from expr: ExprSyntax, for macro: some Freestanding
489494
return _parseCondition(from: asExpr, for: macro, in: context)
490495
}
491496

492-
// Handle closures with a single expression in them (e.g. { $0.foo() })
493-
if let closureExpr = expr.as(ClosureExprSyntax.self) {
494-
return _parseCondition(from: closureExpr, for: macro, in: context)
495-
}
496-
497497
// Handle function calls and member accesses.
498498
if let functionCallExpr = expr.as(FunctionCallExprSyntax.self) {
499499
return _parseCondition(from: functionCallExpr, for: macro, in: context)

Tests/TestingTests/MiscellaneousTests.swift

+9
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ struct SendableTests: Sendable {
123123

124124
@Test(.hidden, arguments: FixtureData.stringReturningClosureArray)
125125
func parameterizedAcceptingFunction(f: @Sendable () -> String) {}
126+
127+
@Test(.hidden) func throwKeywordInExpectation() throws {
128+
#expect(UInt() == UInt(try #require(Int("0"))))
129+
}
130+
131+
@Test(.hidden) func asyncKeywordInExpectation() async {
132+
func f() async -> Int { 0 }
133+
#expect(UInt() == UInt(await f()))
134+
}
126135
}
127136

128137
@Suite("Named Sendable test type", .hidden)

0 commit comments

Comments
 (0)