Skip to content

Commit e63d542

Browse files
authored
Diagnose when we incorrectly infer the type of a capture list item in an exit test. (#1152)
Follow-up to #1130, split out for clarity. This PR adds a custom diagnostic at compile time if we incorrectly infer the type of a captured function argument or `self` in an exit test. For example: ```swift func f(_ x: Int) async { let x = String(x) // local type of 'x' is String, not Int await #expect(processExitsWith: ...) { [x] in ... } } ``` This improves our feedback to the developer when we encounter a pattern like that. The developer will now see: > 🛑 Type of captured value 'x' is ambiguous ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 66701fb commit e63d542

File tree

5 files changed

+61
-7
lines changed

5 files changed

+61
-7
lines changed

Sources/Testing/Expectations/Expectation+Macro.swift

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,7 @@ public macro require(
580580
/// - Parameters:
581581
/// - value: The captured value.
582582
/// - name: The name of the capture list item corresponding to `value`.
583+
/// - expectedType: The type of `value`.
583584
///
584585
/// - Returns: `value` verbatim.
585586
///
@@ -588,7 +589,8 @@ public macro require(
588589
@freestanding(expression)
589590
public macro __capturedValue<T>(
590591
_ value: T,
591-
_ name: String
592+
_ name: String,
593+
_ expectedType: T.Type
592594
) -> T = #externalMacro(module: "TestingMacros", type: "ExitTestCapturedValueMacro") where T: Sendable & Codable
593595

594596
/// Emit a compile-time diagnostic when an unsupported value is captured by an
@@ -597,6 +599,7 @@ public macro __capturedValue<T>(
597599
/// - Parameters:
598600
/// - value: The captured value.
599601
/// - name: The name of the capture list item corresponding to `value`.
602+
/// - expectedType: The type of `value`.
600603
///
601604
/// - Returns: The result of a call to `fatalError()`. `value` is discarded at
602605
/// compile time.
@@ -606,5 +609,27 @@ public macro __capturedValue<T>(
606609
@freestanding(expression)
607610
public macro __capturedValue<T>(
608611
_ value: borrowing T,
609-
_ name: String
612+
_ name: String,
613+
_ expectedType: T.Type
610614
) -> Never = #externalMacro(module: "TestingMacros", type: "ExitTestBadCapturedValueMacro") where T: ~Copyable & ~Escapable
615+
616+
/// Emit a compile-time diagnostic when a value is captured by an exit test but
617+
/// we inferred the wrong type.
618+
///
619+
/// - Parameters:
620+
/// - value: The captured value.
621+
/// - name: The name of the capture list item corresponding to `value`.
622+
/// - expectedType: The _expected_ type of `value`, which will differ from the
623+
/// _actual_ type of `value`.
624+
///
625+
/// - Returns: The result of a call to `fatalError()`. `value` is discarded at
626+
/// compile time.
627+
///
628+
/// - Warning: This macro is used to implement the `#expect(processExitsWith:)`
629+
/// macro. Do not use it directly.
630+
@freestanding(expression)
631+
public macro __capturedValue<T, U>(
632+
_ value: borrowing T,
633+
_ name: String,
634+
_ expectedType: U.Type
635+
) -> T = #externalMacro(module: "TestingMacros", type: "ExitTestIncorrectlyCapturedValueMacro") where T: ~Copyable & ~Escapable, U: ~Copyable & ~Escapable

Sources/TestingMacros/ExitTestCapturedValueMacro.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
//
1010

11+
import SwiftParser
1112
public import SwiftSyntax
1213
import SwiftSyntaxBuilder
1314
public import SwiftSyntaxMacros
@@ -52,3 +53,26 @@ public struct ExitTestBadCapturedValueMacro: ExpressionMacro, Sendable {
5253
return #"Swift.fatalError("Unsupported")"#
5354
}
5455
}
56+
57+
/// The implementation of the `#__capturedValue()` macro when the type we
58+
/// inferred for the value was incorrect.
59+
///
60+
/// This type is used to implement the `#__capturedValue()` macro. Do not use it
61+
/// directly.
62+
public struct ExitTestIncorrectlyCapturedValueMacro: ExpressionMacro, Sendable {
63+
public static func expansion(
64+
of macro: some FreestandingMacroExpansionSyntax,
65+
in context: some MacroExpansionContext
66+
) throws -> ExprSyntax {
67+
let arguments = Array(macro.arguments)
68+
let expr = arguments[0].expression
69+
let nameExpr = arguments[1].expression.cast(StringLiteralExprSyntax.self)
70+
71+
// Diagnose that the type of 'expr' is invalid.
72+
let name = nameExpr.representedLiteralValue ?? expr.trimmedDescription
73+
let capture = ClosureCaptureSyntax(name: .identifier(name))
74+
context.diagnose(.typeOfCaptureIsAmbiguous(capture))
75+
76+
return expr
77+
}
78+
}

Sources/TestingMacros/Support/ClosureCaptureListParsing.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ struct CapturedValueInfo {
3636

3737
/// The expression to assign to the captured value with type-checking applied.
3838
var typeCheckedExpression: ExprSyntax {
39-
#"#__capturedValue(\#(expression.trimmed), \#(literal: name.trimmedDescription))"#
39+
#"#__capturedValue(\#(expression.trimmed), \#(literal: name.trimmedDescription), (\#(type.trimmed)).self)"#
4040
}
4141

4242
init(_ capture: ClosureCaptureSyntax, in context: some MacroExpansionContext) {

Sources/TestingMacros/TestingMacrosMain.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ struct TestingMacrosMain: CompilerPlugin {
3030
ExitTestRequireMacro.self,
3131
ExitTestCapturedValueMacro.self,
3232
ExitTestBadCapturedValueMacro.self,
33+
ExitTestIncorrectlyCapturedValueMacro.self,
3334
TagMacro.self,
3435
SourceLocationMacro.self,
3536
PragmaMacro.self,

Tests/TestingTests/ExitTestTests.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -479,12 +479,16 @@ private import _TestingInternals
479479
}
480480
}(i)
481481

482+
#if false // intentionally fails to compile
482483
// FAILS TO COMPILE: shadowing `i` with a variable of a different type will
483484
// prevent correct expansion (we need an equivalent of decltype() for that.)
484-
// let i = String(i)
485-
// await #expect(processExitsWith: .success) { [i] in
486-
// #expect(!i.isEmpty)
487-
// }
485+
func g(i: Int) async {
486+
let i = String(i)
487+
await #expect(processExitsWith: .success) { [i] in
488+
#expect(!i.isEmpty)
489+
}
490+
}
491+
#endif
488492
}
489493

490494
@Test("Capturing a literal expression")

0 commit comments

Comments
 (0)