Skip to content

Commit 4a5f7e5

Browse files
committed
Hook up effectful keywords discovery from lexical context
1 parent f168971 commit 4a5f7e5

File tree

4 files changed

+183
-133
lines changed

4 files changed

+183
-133
lines changed

Sources/TestingMacros/ConditionMacro.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ extension ConditionMacro {
131131
var expandedFunctionName = TokenSyntax.identifier("__checkCondition")
132132
var checkArguments = [Argument]()
133133
var effectKeywordsToApply: Set<Keyword> = []
134+
var effectKeywordsToApplyOverall: Set<Keyword> = []
134135
do {
135136
if let trailingClosureIndex {
136137
expandedFunctionName = .identifier("__checkClosureCall")
@@ -168,7 +169,10 @@ extension ConditionMacro {
168169

169170
} else if let firstArgument = macroArguments.first {
170171
let originalArgumentExpr = firstArgument.expression
171-
effectKeywordsToApply = findEffectKeywords(in: originalArgumentExpr, context: context)
172+
let effectKeywordsFromNode = findEffectKeywords(in: originalArgumentExpr)
173+
let effectKeywordsFromLexicalContext = findEffectKeywords(in: context)
174+
effectKeywordsToApply = effectKeywordsFromNode.union(effectKeywordsFromLexicalContext)
175+
effectKeywordsToApplyOverall = effectKeywordsToApply.subtracting(effectKeywordsFromLexicalContext)
172176

173177
var useEscapeHatch = false
174178
if let asExpr = originalArgumentExpr.as(AsExprSyntax.self), asExpr.questionOrExclamationMark == nil {
@@ -278,12 +282,8 @@ extension ConditionMacro {
278282
} else {
279283
"\(call).__expected()"
280284
}
281-
if effectKeywordsToApply.contains(.await) {
282-
call = "await \(call)"
283-
}
284-
if !isThrowing && effectKeywordsToApply.contains(.try) {
285-
call = "try \(call)"
286-
}
285+
286+
call = applyEffectfulKeywords(effectKeywordsToApplyOverall, to: call, insertThunkCalls: false)
287287
return call
288288
}
289289

Sources/TestingMacros/Support/EffectfulExpressionHandling.swift

Lines changed: 89 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,61 +14,105 @@ import SwiftSyntaxMacros
1414

1515
// MARK: - Finding effect keywords and expressions
1616

17+
/// Get the effect keyword corresponding to a given syntax node, if any.
18+
///
19+
/// - Parameters:
20+
/// - expr: The syntax node that may represent an effectful expression.
21+
///
22+
/// - Returns: The effect keyword corresponding to `expr`, if any.
23+
private func _effectKeyword(for expr: ExprSyntax) -> Keyword? {
24+
switch expr.kind {
25+
case .tryExpr:
26+
return .try
27+
case .awaitExpr:
28+
return .await
29+
case .consumeExpr:
30+
return .consume
31+
case .borrowExpr:
32+
return .borrow
33+
case .unsafeExpr:
34+
return .unsafe
35+
default:
36+
return nil
37+
}
38+
}
39+
40+
/// Determine how to descend further into a syntax node tree from a given node.
41+
///
42+
/// - Parameters:
43+
/// - node: The syntax node currently being walked.
44+
///
45+
/// - Returns: Whether or not to descend into `node` and visit its children.
46+
private func _continueKind(for node: Syntax) -> SyntaxVisitorContinueKind {
47+
switch node.kind {
48+
case .tryExpr, .awaitExpr, .consumeExpr, .borrowExpr, .unsafeExpr:
49+
// If this node represents an effectful expression, look inside it for
50+
// additional such expressions.
51+
return .visitChildren
52+
case .closureExpr, .functionDecl:
53+
// Do not delve into closures or function declarations.
54+
return .skipChildren
55+
case .variableDecl:
56+
// Delve into variable declarations.
57+
return .visitChildren
58+
default:
59+
// Do not delve into declarations other than variables.
60+
if node.isProtocol((any DeclSyntaxProtocol).self) {
61+
return .skipChildren
62+
}
63+
}
64+
65+
// Recurse into everything else.
66+
return .visitChildren
67+
}
68+
1769
/// A syntax visitor class that looks for effectful keywords in a given
1870
/// expression.
1971
private final class _EffectFinder: SyntaxAnyVisitor {
2072
/// The effect keywords discovered so far.
2173
var effectKeywords: Set<Keyword> = []
2274

2375
override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
24-
switch node.kind {
25-
case .tryExpr:
26-
effectKeywords.insert(.try)
27-
case .awaitExpr:
28-
effectKeywords.insert(.await)
29-
case .consumeExpr:
30-
effectKeywords.insert(.consume)
31-
case .borrowExpr:
32-
effectKeywords.insert(.borrow)
33-
case .unsafeExpr:
34-
effectKeywords.insert(.unsafe)
35-
case .closureExpr, .functionDecl:
36-
// Do not delve into closures or function declarations.
37-
return .skipChildren
38-
case .variableDecl:
39-
// Delve into variable declarations.
40-
return .visitChildren
41-
default:
42-
// Do not delve into declarations other than variables.
43-
if node.isProtocol((any DeclSyntaxProtocol).self) {
44-
return .skipChildren
45-
}
76+
if let expr = node.as(ExprSyntax.self), let keyword = _effectKeyword(for: expr) {
77+
effectKeywords.insert(keyword)
4678
}
4779

48-
// Recurse into everything else.
49-
return .visitChildren
80+
return _continueKind(for: node)
5081
}
5182
}
5283

5384
/// Find effectful keywords in a syntax node.
5485
///
5586
/// - Parameters:
5687
/// - node: The node to inspect.
57-
/// - context: The macro context in which the expression is being parsed.
5888
///
5989
/// - Returns: A set of effectful keywords such as `await` that are present in
6090
/// `node`.
6191
///
6292
/// This function does not descend into function declarations or closure
6393
/// expressions because they represent distinct lexical contexts and their
6494
/// effects are uninteresting in the context of `node` unless they are called.
65-
func findEffectKeywords(in node: some SyntaxProtocol, context: some MacroExpansionContext) -> Set<Keyword> {
66-
// TODO: gather any effects from the lexical context once swift-syntax-#3037 and related PRs land
95+
func findEffectKeywords(in node: some SyntaxProtocol) -> Set<Keyword> {
6796
let effectFinder = _EffectFinder(viewMode: .sourceAccurate)
6897
effectFinder.walk(node)
6998
return effectFinder.effectKeywords
7099
}
71100

101+
/// Find effectful keywords in a macro's lexical context.
102+
///
103+
/// - Parameters:
104+
/// - context: The macro context in which the expression is being parsed.
105+
///
106+
/// - Returns: A set of effectful keywords such as `await` that are present in
107+
/// `context` and would apply to an expression macro during its expansion.
108+
func findEffectKeywords(in context: some MacroExpansionContext) -> Set<Keyword> {
109+
let result = context.lexicalContext.reversed().lazy
110+
.prefix { _continueKind(for: $0) == .visitChildren }
111+
.compactMap { $0.as(ExprSyntax.self) }
112+
.compactMap(_effectKeyword(for:))
113+
return Set(result)
114+
}
115+
72116
extension BidirectionalCollection<Syntax> {
73117
/// The suffix of syntax nodes in this collection which are effectful
74118
/// expressions, such as those for `try` or `await`.
@@ -117,10 +161,13 @@ private func _makeCallToEffectfulThunk(_ thunkName: TokenSyntax, passing expr: s
117161
/// - Parameters:
118162
/// - effectfulKeywords: The effectful keywords to apply.
119163
/// - expr: The expression to apply the keywords and thunk functions to.
164+
/// - insertThunkCalls: Whether or not to also insert calls to thunks to
165+
/// ensure the inserted keywords do not generate warnings. If you aren't
166+
/// sure whether thunk calls are needed, pass `true`.
120167
///
121168
/// - Returns: A copy of `expr` if no changes are needed, or an expression that
122169
/// adds the keywords in `effectfulKeywords` to `expr`.
123-
func applyEffectfulKeywords(_ effectfulKeywords: Set<Keyword>, to expr: some ExprSyntaxProtocol) -> ExprSyntax {
170+
func applyEffectfulKeywords(_ effectfulKeywords: Set<Keyword>, to expr: some ExprSyntaxProtocol, insertThunkCalls: Bool = true) -> ExprSyntax {
124171
let originalExpr = expr
125172
var expr = ExprSyntax(expr.trimmed)
126173

@@ -135,33 +182,35 @@ func applyEffectfulKeywords(_ effectfulKeywords: Set<Keyword>, to expr: some Exp
135182
#endif
136183

137184
// First, add thunk function calls.
138-
if needAwait {
139-
expr = _makeCallToEffectfulThunk(.identifier("__requiringAwait"), passing: expr)
140-
}
141-
if needTry {
142-
expr = _makeCallToEffectfulThunk(.identifier("__requiringTry"), passing: expr)
143-
}
185+
if insertThunkCalls {
186+
if needAwait {
187+
expr = _makeCallToEffectfulThunk(.identifier("__requiringAwait"), passing: expr)
188+
}
189+
if needTry {
190+
expr = _makeCallToEffectfulThunk(.identifier("__requiringTry"), passing: expr)
191+
}
144192
#if compiler(>=6.2)
145-
if needUnsafe {
146-
expr = _makeCallToEffectfulThunk(.identifier("__requiringUnsafe"), passing: expr)
147-
}
193+
if needUnsafe {
194+
expr = _makeCallToEffectfulThunk(.identifier("__requiringUnsafe"), passing: expr)
195+
}
148196
#endif
197+
}
149198

150199
// Then add keyword expressions. (We do this separately so we end up writing
151200
// `try await __r(__r(self))` instead of `try __r(await __r(self))` which is
152201
// less accepted by the compiler.)
153202
if needAwait {
154203
expr = ExprSyntax(
155204
AwaitExprSyntax(
156-
awaitKeyword: .keyword(.await).with(\.trailingTrivia, .space),
205+
awaitKeyword: .keyword(.await, trailingTrivia: .space, presence: .present),
157206
expression: expr
158207
)
159208
)
160209
}
161210
if needTry {
162211
expr = ExprSyntax(
163212
TryExprSyntax(
164-
tryKeyword: .keyword(.try).with(\.trailingTrivia, .space),
213+
tryKeyword: .keyword(.try, trailingTrivia: .space, presence: .present),
165214
expression: expr
166215
)
167216
)
@@ -170,7 +219,7 @@ func applyEffectfulKeywords(_ effectfulKeywords: Set<Keyword>, to expr: some Exp
170219
if needUnsafe {
171220
expr = ExprSyntax(
172221
UnsafeExprSyntax(
173-
unsafeKeyword: .keyword(.unsafe).with(\.trailingTrivia, .space),
222+
unsafeKeyword: .keyword(.unsafe, trailingTrivia: .space, presence: .present),
174223
expression: expr
175224
)
176225
)

0 commit comments

Comments
 (0)