Skip to content

Commit 67d757e

Browse files
committed
Move some code around and try to simplify the main macro expansion function again
1 parent 82a2638 commit 67d757e

File tree

3 files changed

+135
-169
lines changed

3 files changed

+135
-169
lines changed

Sources/TestingMacros/ConditionMacro.swift

+9-82
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ extension ConditionMacro {
160160
expandedFunctionName = .identifier("__checkEscapedCondition")
161161

162162
} else {
163+
if effectKeywordsToApply.contains(.await) {
164+
expandedFunctionName = .identifier("__checkConditionAsync")
165+
}
166+
163167
var expressionContextName = TokenSyntax.identifier("__ec")
164168
let isNameUsed = originalArgumentExpr.tokens(viewMode: .sourceAccurate).lazy
165169
.map(\.tokenKind)
@@ -169,90 +173,14 @@ extension ConditionMacro {
169173
let uniqueName = context.makeUniqueName("")
170174
expressionContextName = .identifier("\(expressionContextName)\(uniqueName)")
171175
}
172-
let (rewrittenArgumentExpr, rewrittenNodes, prefixCodeBlockItems) = insertCalls(
173-
toExpressionContextNamed: expressionContextName,
174-
into: originalArgumentExpr,
176+
let (closureExpr, rewrittenNodes) = rewrite(
177+
originalArgumentExpr,
178+
usingExpressionContextNamed: expressionContextName,
175179
for: macro,
176180
rootedAt: originalArgumentExpr,
181+
effectKeywordsToApply: effectKeywordsToApply,
177182
in: context
178183
)
179-
var argumentExpr = rewrittenArgumentExpr.cast(ExprSyntax.self)
180-
181-
// Insert additional effect keywords as needed. Use the helper
182-
// functions so we don't need to worry about the precise structure of
183-
// the expression being tested.
184-
if effectKeywordsToApply.contains(.await) {
185-
argumentExpr = "await Testing.__requiringAwait(\(argumentExpr))"
186-
expandedFunctionName = .identifier("__checkConditionAsync")
187-
}
188-
if isThrowing || effectKeywordsToApply.contains(.try) {
189-
argumentExpr = "try Testing.__requiringTry(\(argumentExpr))"
190-
}
191-
192-
// Construct the body of the closure that we'll pass to the expanded
193-
// function.
194-
var codeBlockItems = CodeBlockItemListSyntax {
195-
if prefixCodeBlockItems.isEmpty {
196-
CodeBlockItemSyntax(item: .expr(argumentExpr))
197-
.with(\.trailingTrivia, .newline)
198-
} else {
199-
prefixCodeBlockItems
200-
201-
// If we're inserting any additional code into the closure before
202-
// the rewritten argument, we can't elide the return keyword.
203-
CodeBlockItemSyntax(
204-
item: .stmt(
205-
StmtSyntax(
206-
ReturnStmtSyntax(
207-
expression: argumentExpr
208-
.with(\.leadingTrivia, .space)
209-
)
210-
)
211-
)
212-
).with(\.trailingTrivia, .newline)
213-
}
214-
}
215-
216-
// Replace any dollar identifiers we find.
217-
let closureArguments = rewriteClosureArguments(in: codeBlockItems)
218-
if let closureArguments {
219-
codeBlockItems = closureArguments.rewrittenNode.cast(CodeBlockItemListSyntax.self)
220-
}
221-
222-
// Enclose the code block in the final closure.
223-
let closureExpr = ClosureExprSyntax(
224-
signature: ClosureSignatureSyntax(
225-
capture: closureArguments?.captureList,
226-
parameterClause: .parameterClause(
227-
ClosureParameterClauseSyntax(
228-
parameters: ClosureParameterListSyntax {
229-
ClosureParameterSyntax(
230-
firstName: expressionContextName,
231-
colon: .colonToken().with(\.trailingTrivia, .space),
232-
type: TypeSyntax(
233-
AttributedTypeSyntax(
234-
specifiers: [
235-
.init(
236-
SimpleTypeSpecifierSyntax(specifier: .keyword(.inout))
237-
.with(\.trailingTrivia, .space)
238-
)
239-
],
240-
baseType: MemberTypeSyntax(
241-
baseType: IdentifierTypeSyntax(name: .identifier("Testing")),
242-
name: .identifier("__ExpectationContext")
243-
)
244-
)
245-
)
246-
)
247-
}
248-
)
249-
),
250-
inKeyword: .keyword(.in)
251-
.with(\.leadingTrivia, .space)
252-
.with(\.trailingTrivia, .newline)
253-
),
254-
statements: codeBlockItems
255-
)
256184
checkArguments.append(Argument(expression: closureExpr))
257185

258186
// Sort the rewritten nodes. This isn't strictly necessary for
@@ -556,8 +484,7 @@ extension ExitTestConditionMacro {
556484
arguments[trailingClosureIndex].expression = ExprSyntax(
557485
ClosureExprSyntax {
558486
for decl in decls {
559-
CodeBlockItemSyntax(item: .decl(decl))
560-
.with(\.trailingTrivia, .newline)
487+
decl.with(\.trailingTrivia, .newline)
561488
}
562489
}
563490
)

Sources/TestingMacros/Support/ConditionArgumentParsing.swift

+125-86
Original file line numberDiff line numberDiff line change
@@ -539,53 +539,126 @@ private final class _ContextInserter<C, M>: SyntaxRewriter where C: MacroExpansi
539539
#endif
540540
}
541541

542-
/// Insert calls to an expression context into a syntax tree.
543-
///
544-
/// - Parameters:
545-
/// - expressionContextName: The name of the instance of
546-
/// `__ExpectationContext` to call.
547-
/// - node: The root of a syntax tree to rewrite. This node may not itself be
548-
/// the root of the overall syntax tree—it's just the root of the subtree
549-
/// that we're rewriting.
550-
/// - macro: The macro expression.
551-
/// - effectiveRootNode: The node to treat as the root of the syntax tree for
552-
/// the purposes of generating expression ID values.
553-
/// - context: The macro context in which the expression is being parsed.
554-
///
555-
/// - Returns: A tuple containing the rewritten copy of `node`, a list of all
556-
/// the nodes within `node` (possibly including `node` itself) that were
557-
/// rewritten, and a code block containing code that should be inserted into
558-
/// the lexical scope of `node` _before_ its rewritten equivalent.
559-
func insertCalls(
560-
toExpressionContextNamed expressionContextName: TokenSyntax,
561-
into node: some SyntaxProtocol,
562-
for macro: some FreestandingMacroExpansionSyntax,
563-
rootedAt effectiveRootNode: some SyntaxProtocol,
564-
in context: some MacroExpansionContext
565-
) -> (Syntax, rewrittenNodes: Set<Syntax>, prefixCodeBlockItems: CodeBlockItemListSyntax) {
566-
if let node = node.as(ExprSyntax.self) {
567-
_diagnoseTrivialBooleanValue(from: node, for: macro, in: context)
568-
}
569-
570-
let contextInserter = _ContextInserter(in: context, for: macro, rootedAt: Syntax(effectiveRootNode), expressionContextName: expressionContextName)
571-
let result = contextInserter.rewrite(node)
572-
let rewrittenNodes = contextInserter.rewrittenNodes
573-
574-
let prefixCodeBlockItems = CodeBlockItemListSyntax {
575-
if !contextInserter.teardownItems.isEmpty {
576-
CodeBlockItemSyntax(
577-
item: .stmt(
578-
StmtSyntax(
579-
DeferStmtSyntax {
580-
contextInserter.teardownItems
542+
extension ConditionMacro {
543+
/// Rewrite and expand upon an expression node.
544+
///
545+
/// - Parameters:
546+
/// - node: The root of a syntax tree to rewrite. This node may not itself
547+
/// be the root of the overall syntax tree—it's just the root of the
548+
/// subtree that we're rewriting.
549+
/// - expressionContextName: The name of the instance of
550+
/// `__ExpectationContext` to call at runtime.
551+
/// - macro: The macro expression.
552+
/// - effectiveRootNode: The node to treat as the root of the syntax tree
553+
/// for the purposes of generating expression ID values.
554+
/// - effectKeywordsToApply: The set of effect keywords in the expanded
555+
/// expression or its lexical context that may apply to `node`.
556+
/// - context: The macro context in which the expression is being parsed.
557+
///
558+
/// - Returns: A tuple containing the rewritten copy of `node`, a list of all
559+
/// the nodes within `node` (possibly including `node` itself) that were
560+
/// rewritten, and a code block containing code that should be inserted into
561+
/// the lexical scope of `node` _before_ its rewritten equivalent.
562+
static func rewrite(
563+
_ node: some ExprSyntaxProtocol,
564+
usingExpressionContextNamed expressionContextName: TokenSyntax,
565+
for macro: some FreestandingMacroExpansionSyntax,
566+
rootedAt effectiveRootNode: some SyntaxProtocol,
567+
effectKeywordsToApply: Set<Keyword>,
568+
in context: some MacroExpansionContext
569+
) -> (ClosureExprSyntax, rewrittenNodes: Set<Syntax>) {
570+
_diagnoseTrivialBooleanValue(from: ExprSyntax(node), for: macro, in: context)
571+
572+
let contextInserter = _ContextInserter(in: context, for: macro, rootedAt: Syntax(effectiveRootNode), expressionContextName: expressionContextName)
573+
var expandedExpr = contextInserter.rewrite(node).cast(ExprSyntax.self)
574+
let rewrittenNodes = contextInserter.rewrittenNodes
575+
576+
// Insert additional effect keywords as needed. Use the helper functions so
577+
// we don't need to worry about the precise structure of the expression
578+
// being rewritten.
579+
if effectKeywordsToApply.contains(.await) {
580+
expandedExpr = "await Testing.__requiringAwait(\(expandedExpr))"
581+
}
582+
if isThrowing || effectKeywordsToApply.contains(.try) {
583+
expandedExpr = "try Testing.__requiringTry(\(expandedExpr))"
584+
}
585+
586+
// Construct the body of the closure that we'll pass to the expanded
587+
// function.
588+
var codeBlockItems = CodeBlockItemListSyntax {
589+
if contextInserter.teardownItems.isEmpty {
590+
expandedExpr.with(\.trailingTrivia, .newline)
591+
} else {
592+
// Insert a defer statement that runs any teardown items.
593+
DeferStmtSyntax {
594+
for teardownItem in contextInserter.teardownItems {
595+
teardownItem.with(\.trailingTrivia, .newline)
596+
}
597+
}.with(\.trailingTrivia, .newline)
598+
599+
// If we're inserting any additional code into the closure before
600+
// the rewritten argument, we can't elide the return keyword.
601+
ReturnStmtSyntax(
602+
expression: expandedExpr.with(\.leadingTrivia, .space)
603+
).with(\.trailingTrivia, .newline)
604+
}
605+
}
606+
607+
// Replace any dollar identifiers in the code block, then construct a
608+
// capture list for the closure (if needed.)
609+
var captureList: ClosureCaptureClauseSyntax?
610+
do {
611+
let dollarIDReplacer = _DollarIdentifierReplacer()
612+
codeBlockItems = dollarIDReplacer.rewrite(codeBlockItems).cast(CodeBlockItemListSyntax.self)
613+
if !dollarIDReplacer.dollarIdentifierTokenKinds.isEmpty {
614+
let dollarIdentifierTokens = dollarIDReplacer.dollarIdentifierTokenKinds.map { tokenKind in
615+
TokenSyntax(tokenKind, presence: .present)
616+
}
617+
captureList = ClosureCaptureClauseSyntax {
618+
for token in dollarIdentifierTokens {
619+
ClosureCaptureSyntax(name: _rewriteDollarIdentifier(token), expression: DeclReferenceExprSyntax(baseName: token))
620+
}
621+
}
622+
}
623+
}
624+
625+
// Enclose the code block in the final closure.
626+
let closureExpr = ClosureExprSyntax(
627+
signature: ClosureSignatureSyntax(
628+
capture: captureList,
629+
parameterClause: .parameterClause(
630+
ClosureParameterClauseSyntax(
631+
parameters: ClosureParameterListSyntax {
632+
ClosureParameterSyntax(
633+
firstName: expressionContextName,
634+
colon: .colonToken().with(\.trailingTrivia, .space),
635+
type: TypeSyntax(
636+
AttributedTypeSyntax(
637+
specifiers: [
638+
TypeSpecifierListSyntax.Element(
639+
SimpleTypeSpecifierSyntax(specifier: .keyword(.inout))
640+
.with(\.trailingTrivia, .space)
641+
)
642+
],
643+
baseType: MemberTypeSyntax(
644+
baseType: IdentifierTypeSyntax(name: .identifier("Testing")),
645+
name: .identifier("__ExpectationContext")
646+
)
647+
)
648+
)
649+
)
581650
}
582651
)
583-
)
584-
)
585-
}
586-
}.formatted().with(\.trailingTrivia, .newline).cast(CodeBlockItemListSyntax.self)
652+
),
653+
inKeyword: .keyword(.in)
654+
.with(\.leadingTrivia, .space)
655+
.with(\.trailingTrivia, .newline)
656+
),
657+
statements: codeBlockItems
658+
)
587659

588-
return (result, rewrittenNodes, prefixCodeBlockItems)
660+
return (closureExpr, rewrittenNodes)
661+
}
589662
}
590663

591664
// MARK: - Finding optional chains
@@ -668,57 +741,23 @@ private func _rewriteDollarIdentifier(_ token: TokenSyntax) -> TokenSyntax {
668741
/// A syntax rewriter that replaces _numeric_ dollar identifiers (e.g. `$0`)
669742
/// with normal (non-dollar) identifiers.
670743
private final class _DollarIdentifierReplacer: SyntaxRewriter {
671-
/// The dollar identifier tokens that have been rewritten.
672-
var dollarIdentifierTokens = Set<TokenSyntax>()
673-
674-
/// The node to treat as the root node when expanding expressions.
675-
var effectiveRootNode: Syntax
676-
677-
init(rootedAt effectiveRootNode: Syntax) {
678-
self.effectiveRootNode = effectiveRootNode
679-
}
680-
681-
override func visitAny(_ node: Syntax) -> Syntax? {
682-
// Do not recurse into closure expressions (except the root node) because
683-
// they will have their own argument/capture lists that won't conflict with
684-
// the enclosing scope's.
685-
if node.is(ClosureExprSyntax.self) && node != effectiveRootNode {
686-
return Syntax(node)
687-
}
688-
689-
return nil
690-
}
744+
/// The `tokenKind` properties of any dollar identifier tokens that have been
745+
/// rewritten.
746+
var dollarIdentifierTokenKinds = Set<TokenKind>()
691747

692748
override func visit(_ node: TokenSyntax) -> TokenSyntax {
693749
if case let .dollarIdentifier(id) = node.tokenKind, id.dropFirst().allSatisfy(\.isWholeNumber) {
694750
// This dollar identifier is numeric, so it's a closure argument.
695-
dollarIdentifierTokens.insert(node)
751+
dollarIdentifierTokenKinds.insert(node.tokenKind)
696752
return _rewriteDollarIdentifier(node)
697753
}
698754

699755
return node
700756
}
701-
}
702757

703-
/// Rewrite any implicit closure arguments (dollar identifiers such as `$0`) in
704-
/// the given node as normal (non-dollar) identifiers.
705-
///
706-
/// - Parameters:
707-
/// - node: The syntax node to rewrite.
708-
///
709-
/// - Returns: A rewritten copy of `node` as well as a closure capture list that
710-
/// can be used to transform the original dollar identifiers to their
711-
/// rewritten counterparts in a nested closure invocation.
712-
func rewriteClosureArguments(in node: some SyntaxProtocol) -> (rewrittenNode: Syntax, captureList: ClosureCaptureClauseSyntax)? {
713-
let replacer = _DollarIdentifierReplacer(rootedAt: Syntax(node))
714-
let result = replacer.rewrite(node)
715-
if replacer.dollarIdentifierTokens.isEmpty {
716-
return nil
717-
}
718-
let captureList = ClosureCaptureClauseSyntax {
719-
for token in replacer.dollarIdentifierTokens {
720-
ClosureCaptureSyntax(name: _rewriteDollarIdentifier(token), expression: DeclReferenceExprSyntax(baseName: token))
721-
}
758+
override func visit(_ node: ClosureExprSyntax) -> ExprSyntax {
759+
// Do not recurse into closure expressions because they will have their own
760+
// argument lists that won't conflict with the enclosing scope's.
761+
return ExprSyntax(node)
722762
}
723-
return (result, captureList)
724763
}

Tests/SubexpressionShowcase/SubexpressionShowcase.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func subexpressionShowcase() async throws {
5050
}
5151

5252
let closure: (Int) -> Void = {
53-
#expect($0 == 0x10)
53+
#expect(($0 + $0 + $0) == 0x10)
5454
}
5555
closure(11)
5656

0 commit comments

Comments
 (0)