diff --git a/Sources/TestingMacros/Support/ConditionArgumentParsing.swift b/Sources/TestingMacros/Support/ConditionArgumentParsing.swift index e0ccda9a7..254f3d0aa 100644 --- a/Sources/TestingMacros/Support/ConditionArgumentParsing.swift +++ b/Sources/TestingMacros/Support/ConditionArgumentParsing.swift @@ -517,10 +517,8 @@ private func _parseCondition(from expr: ExprSyntax, for macro: some Freestanding /// - Returns: An instance of ``Condition`` describing `expr`. func parseCondition(from expr: ExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { // If the condition involves the `unsafe`, `try`, or `await` keywords, assume - // we cannot expand it. This check cannot handle expressions like - // `try #expect(a.b(c))` where `b()` is throwing because the `try` keyword is - // outside the macro expansion. SEE: rdar://109470248 - let effectKeywordsToApply = findEffectKeywords(in: expr, context: context) + // we cannot expand it. + let effectKeywordsToApply = findEffectKeywords(in: expr).union(findEffectKeywords(in: context)) guard effectKeywordsToApply.intersection([.unsafe, .try, .await]).isEmpty else { return Condition(expression: expr) } diff --git a/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift index a0b84e737..b093d1e77 100644 --- a/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift +++ b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift @@ -14,6 +14,58 @@ import SwiftSyntaxMacros // MARK: - Finding effect keywords and expressions +/// Get the effect keyword corresponding to a given syntax node, if any. +/// +/// - Parameters: +/// - expr: The syntax node that may represent an effectful expression. +/// +/// - Returns: The effect keyword corresponding to `expr`, if any. +private func _effectKeyword(for expr: ExprSyntax) -> Keyword? { + switch expr.kind { + case .tryExpr: + return .try + case .awaitExpr: + return .await + case .consumeExpr: + return .consume + case .borrowExpr: + return .borrow + case .unsafeExpr: + return .unsafe + default: + return nil + } +} + +/// Determine how to descend further into a syntax node tree from a given node. +/// +/// - Parameters: +/// - node: The syntax node currently being walked. +/// +/// - Returns: Whether or not to descend into `node` and visit its children. +private func _continueKind(for node: Syntax) -> SyntaxVisitorContinueKind { + switch node.kind { + case .tryExpr, .awaitExpr, .consumeExpr, .borrowExpr, .unsafeExpr: + // If this node represents an effectful expression, look inside it for + // additional such expressions. + return .visitChildren + case .closureExpr, .functionDecl: + // Do not delve into closures or function declarations. + return .skipChildren + case .variableDecl: + // Delve into variable declarations. + return .visitChildren + default: + // Do not delve into declarations other than variables. + if node.isProtocol((any DeclSyntaxProtocol).self) { + return .skipChildren + } + } + + // Recurse into everything else. + return .visitChildren +} + /// A syntax visitor class that looks for effectful keywords in a given /// expression. private final class _EffectFinder: SyntaxAnyVisitor { @@ -21,32 +73,11 @@ private final class _EffectFinder: SyntaxAnyVisitor { var effectKeywords: Set = [] override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { - switch node.kind { - case .tryExpr: - effectKeywords.insert(.try) - case .awaitExpr: - effectKeywords.insert(.await) - case .consumeExpr: - effectKeywords.insert(.consume) - case .borrowExpr: - effectKeywords.insert(.borrow) - case .unsafeExpr: - effectKeywords.insert(.unsafe) - case .closureExpr, .functionDecl: - // Do not delve into closures or function declarations. - return .skipChildren - case .variableDecl: - // Delve into variable declarations. - return .visitChildren - default: - // Do not delve into declarations other than variables. - if node.isProtocol((any DeclSyntaxProtocol).self) { - return .skipChildren - } + if let expr = node.as(ExprSyntax.self), let keyword = _effectKeyword(for: expr) { + effectKeywords.insert(keyword) } - // Recurse into everything else. - return .visitChildren + return _continueKind(for: node) } } @@ -54,7 +85,6 @@ private final class _EffectFinder: SyntaxAnyVisitor { /// /// - Parameters: /// - node: The node to inspect. -/// - context: The macro context in which the expression is being parsed. /// /// - Returns: A set of effectful keywords such as `await` that are present in /// `node`. @@ -62,13 +92,27 @@ private final class _EffectFinder: SyntaxAnyVisitor { /// This function does not descend into function declarations or closure /// expressions because they represent distinct lexical contexts and their /// effects are uninteresting in the context of `node` unless they are called. -func findEffectKeywords(in node: some SyntaxProtocol, context: some MacroExpansionContext) -> Set { - // TODO: gather any effects from the lexical context once swift-syntax-#3037 and related PRs land +func findEffectKeywords(in node: some SyntaxProtocol) -> Set { let effectFinder = _EffectFinder(viewMode: .sourceAccurate) effectFinder.walk(node) return effectFinder.effectKeywords } +/// Find effectful keywords in a macro's lexical context. +/// +/// - Parameters: +/// - context: The macro context in which the expression is being parsed. +/// +/// - Returns: A set of effectful keywords such as `await` that are present in +/// `context` and would apply to an expression macro during its expansion. +func findEffectKeywords(in context: some MacroExpansionContext) -> Set { + let result = context.lexicalContext.reversed().lazy + .prefix { _continueKind(for: $0) == .visitChildren } + .compactMap { $0.as(ExprSyntax.self) } + .compactMap(_effectKeyword(for:)) + return Set(result) +} + extension BidirectionalCollection { /// The suffix of syntax nodes in this collection which are effectful /// expressions, such as those for `try` or `await`. @@ -128,10 +172,13 @@ private func _makeCallToEffectfulThunk(_ thunkName: TokenSyntax, passing expr: s /// - Parameters: /// - effectfulKeywords: The effectful keywords to apply. /// - expr: The expression to apply the keywords and thunk functions to. +/// - insertThunkCalls: Whether or not to also insert calls to thunks to +/// ensure the inserted keywords do not generate warnings. If you aren't +/// sure whether thunk calls are needed, pass `true`. /// /// - Returns: A copy of `expr` if no changes are needed, or an expression that /// adds the keywords in `effectfulKeywords` to `expr`. -func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some ExprSyntaxProtocol) -> ExprSyntax { +func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some ExprSyntaxProtocol, insertThunkCalls: Bool = true) -> ExprSyntax { let originalExpr = expr var expr = ExprSyntax(expr.trimmed) @@ -141,14 +188,16 @@ func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some Exp let needUnsafe = isUnsafeKeywordSupported && effectfulKeywords.contains(.unsafe) && !expr.is(UnsafeExprSyntax.self) // First, add thunk function calls. - if needAwait { - expr = _makeCallToEffectfulThunk(.identifier("__requiringAwait"), passing: expr) - } - if needTry { - expr = _makeCallToEffectfulThunk(.identifier("__requiringTry"), passing: expr) - } - if needUnsafe { - expr = _makeCallToEffectfulThunk(.identifier("__requiringUnsafe"), passing: expr) + if insertThunkCalls { + if needAwait { + expr = _makeCallToEffectfulThunk(.identifier("__requiringAwait"), passing: expr) + } + if needTry { + expr = _makeCallToEffectfulThunk(.identifier("__requiringTry"), passing: expr) + } + if needUnsafe { + expr = _makeCallToEffectfulThunk(.identifier("__requiringUnsafe"), passing: expr) + } } // Then add keyword expressions. (We do this separately so we end up writing