Skip to content

Commit 3a7fbbc

Browse files
porglezompktoso
authored andcommitted
Support async/throws functions in the @Traced macro
Based on the effects signature of the attached function we apply try/await as appropriate. But, if the function is async/throws but those effects aren't actually used in the function body, this causes a new warning because the closure isn't inferred to be async and/or throws. To avoid those warnings, we also apply matching effects specifiers to the withSpan closure.
1 parent 024afec commit 3a7fbbc

File tree

2 files changed

+201
-1
lines changed

2 files changed

+201
-1
lines changed

Sources/TracingMacrosImplementation/TracedMacro.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,36 @@ public struct TracedMacro: BodyMacro {
2929
throw MacroExpansionErrorMessage("expected a function with a body")
3030
}
3131

32+
// Construct a withSpan call matching the invocation of the @Traced macro
33+
3234
let operationName = StringLiteralExprSyntax(content: function.name.text)
3335
let withSpanCall: ExprSyntax = "withSpan(\(operationName))"
34-
let withSpanExpr: ExprSyntax = "\(withSpanCall) { span in \(body.statements) }"
36+
37+
// We want to explicitly specify the closure effect specifiers in order
38+
// to avoid warnings about unused try/await expressions.
39+
// We might as well explicitly specify the closure return type to help type inference.
40+
41+
let asyncClause = function.signature.effectSpecifiers?.asyncSpecifier
42+
let returnClause = function.signature.returnClause
43+
var throwsClause = function.signature.effectSpecifiers?.throwsClause
44+
// You aren't allowed to apply "rethrows" as a closure effect
45+
// specifier, so we have to convert this to a "throws" effect
46+
if throwsClause?.throwsSpecifier.tokenKind == .keyword(.rethrows) {
47+
throwsClause?.throwsSpecifier = .keyword(.throws)
48+
}
49+
var withSpanExpr: ExprSyntax = """
50+
\(withSpanCall) { span \(asyncClause)\(throwsClause)\(returnClause)in \(body.statements) }
51+
"""
52+
53+
// Apply a try / await as necessary to adapt the withSpan expression
54+
55+
if function.signature.effectSpecifiers?.asyncSpecifier != nil {
56+
withSpanExpr = "await \(withSpanExpr)"
57+
}
58+
59+
if function.signature.effectSpecifiers?.throwsClause != nil {
60+
withSpanExpr = "try \(withSpanExpr)"
61+
}
3562

3663
return ["\(withSpanExpr)"]
3764
}

Tests/TracingMacrosTests/TracedTests.swift

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,146 @@ final class TracedMacroTests: XCTestCase {
5656
)
5757
}
5858

59+
func test_tracedMacro_sync_throws() {
60+
assertMacroExpansion(
61+
"""
62+
@Traced
63+
func syncThrowingExample(param: Int) throws {
64+
struct ExampleError: Error {
65+
}
66+
throw ExampleError()
67+
}
68+
""",
69+
expandedSource: """
70+
func syncThrowingExample(param: Int) throws {
71+
try withSpan("syncThrowingExample") { span throws in
72+
struct ExampleError: Error {
73+
}
74+
throw ExampleError()
75+
}
76+
}
77+
""",
78+
macros: ["Traced": TracedMacro.self]
79+
)
80+
}
81+
82+
func test_tracedMacro_sync_rethrows() {
83+
assertMacroExpansion(
84+
"""
85+
@Traced
86+
func syncRethrowingExample(body: () throws -> Int) rethrows -> Int {
87+
print("Starting")
88+
let result = try body()
89+
print("Ending")
90+
return result
91+
}
92+
""",
93+
expandedSource: """
94+
func syncRethrowingExample(body: () throws -> Int) rethrows -> Int {
95+
try withSpan("syncRethrowingExample") { span throws -> Int in
96+
print("Starting")
97+
let result = try body()
98+
print("Ending")
99+
return result
100+
}
101+
}
102+
""",
103+
macros: ["Traced": TracedMacro.self]
104+
)
105+
}
106+
107+
func test_tracedMacro_async_nothrow() {
108+
assertMacroExpansion(
109+
"""
110+
@Traced
111+
func asyncNonthrowingExample(param: Int) async {
112+
print(param)
113+
}
114+
""",
115+
expandedSource: """
116+
func asyncNonthrowingExample(param: Int) async {
117+
await withSpan("asyncNonthrowingExample") { span async in
118+
print(param)
119+
}
120+
}
121+
""",
122+
macros: ["Traced": TracedMacro.self]
123+
)
124+
}
125+
126+
func test_tracedMacro_async_throws() {
127+
assertMacroExpansion(
128+
"""
129+
@Traced
130+
func asyncThrowingExample(param: Int) async throws {
131+
try await Task.sleep(for: .seconds(1))
132+
print("Hello")
133+
}
134+
""",
135+
expandedSource: """
136+
func asyncThrowingExample(param: Int) async throws {
137+
try await withSpan("asyncThrowingExample") { span async throws in
138+
try await Task.sleep(for: .seconds(1))
139+
print("Hello")
140+
}
141+
}
142+
""",
143+
macros: ["Traced": TracedMacro.self]
144+
)
145+
}
146+
147+
func test_tracedMacro_async_rethrows() {
148+
assertMacroExpansion(
149+
"""
150+
@Traced
151+
func asyncRethrowingExample(body: () async throws -> Int) async rethrows -> Int {
152+
try? await Task.sleep(for: .seconds(1))
153+
let result = try await body()
154+
span.attributes["result"] = result
155+
return result
156+
}
157+
""",
158+
expandedSource: """
159+
func asyncRethrowingExample(body: () async throws -> Int) async rethrows -> Int {
160+
try await withSpan("asyncRethrowingExample") { span async throws -> Int in
161+
try? await Task.sleep(for: .seconds(1))
162+
let result = try await body()
163+
span.attributes["result"] = result
164+
return result
165+
}
166+
}
167+
""",
168+
macros: ["Traced": TracedMacro.self]
169+
)
170+
}
171+
172+
// Testing that this expands correctly, but not including this as a
173+
// compile-test because withSpan doesn't currently support typed throws.
174+
func test_tracedMacro_async_typed_throws() {
175+
assertMacroExpansion(
176+
"""
177+
@Traced
178+
func asyncTypedThrowingExample<Err>(body: () async throws(Err) -> Int) async throws(Err) -> Int {
179+
try? await Task.sleep(for: .seconds(1))
180+
let result = try await body()
181+
span.attributes["result"] = result
182+
return result
183+
}
184+
""",
185+
expandedSource: """
186+
func asyncTypedThrowingExample<Err>(body: () async throws(Err) -> Int) async throws(Err) -> Int {
187+
try await withSpan("asyncTypedThrowingExample") { span async throws(Err) -> Int in
188+
try? await Task.sleep(for: .seconds(1))
189+
let result = try await body()
190+
span.attributes["result"] = result
191+
return result
192+
}
193+
}
194+
""",
195+
macros: ["Traced": TracedMacro.self]
196+
)
197+
}
198+
59199
func test_tracedMacro_accessSpan() {
60200
assertMacroExpansion(
61201
"""
@@ -83,6 +223,39 @@ func syncNonthrowingExample(param: Int) {
83223
print(param)
84224
}
85225

226+
@Traced
227+
func syncThrowingExample(param: Int) throws {
228+
struct ExampleError: Error {}
229+
throw ExampleError()
230+
}
231+
232+
@Traced
233+
func syncRethrowingExample(body: () throws -> Int) rethrows -> Int {
234+
print("Starting")
235+
let result = try body()
236+
print("Ending")
237+
return result
238+
}
239+
240+
@Traced
241+
func asyncNonthrowingExample(param: Int) async {
242+
print(param)
243+
}
244+
245+
@Traced
246+
func asyncThrowingExample(param: Int) async throws {
247+
try await Task.sleep(for: .seconds(1))
248+
print("Hello")
249+
}
250+
251+
@Traced
252+
func asyncRethrowingExample(body: () async throws -> Int) async rethrows -> Int {
253+
try? await Task.sleep(for: .seconds(1))
254+
let result = try await body()
255+
span.attributes["result"] = result
256+
return result
257+
}
258+
86259
@Traced
87260
func example(param: Int) {
88261
span.attributes["param"] = param

0 commit comments

Comments
 (0)