Preserve contextual type for literal arguments: expressions in @Test#1627
Preserve contextual type for literal arguments: expressions in @Test#1627ojun9 wants to merge 1 commit intoswiftlang:mainfrom
arguments: expressions in @Test#1627Conversation
|
Type checking of arguments to the |
grynspan
left a comment
There was a problem hiding this comment.
Additional test coverage is needed too.
| return nil | ||
| } | ||
|
|
||
| if testFunctionArguments.count == 1, expression.is(ArrayExprSyntax.self) { |
There was a problem hiding this comment.
It should be safe to generalize this so that the argument count only needs to match the parameter count.
| } | ||
|
|
||
| let parameters = functionDecl.signature.parameterClause.parameters | ||
| guard !parameters.isEmpty else { |
There was a problem hiding this comment.
| guard !parameters.isEmpty else { | |
| if parameters.isEmpty { |
| private func _contextualTypeForLiteralArgument( | ||
| for expression: ExprSyntax, | ||
| among testFunctionArguments: [Argument] | ||
| ) -> String? { |
There was a problem hiding this comment.
| ) -> String? { | |
| ) -> TypeSyntax? { |
| // type itself, not tuple-shaped elements. | ||
| return "[\(parameter.baseTypeName)]" | ||
| } | ||
| let elementType = parameters.map(\.baseTypeName).joined(separator: ", ") |
There was a problem hiding this comment.
Can we construct an ArrayTypeSyntax here instead and leave the string interpolation to swift-syntax?
| if parameters.count == 1, let parameter = parameters.first { | ||
| // A single-parameter test expects collection elements of the parameter | ||
| // type itself, not tuple-shaped elements. | ||
| return "[\(parameter.baseTypeName)]" |
There was a problem hiding this comment.
Tokens from the original source need to be trimmed.
|
|
||
| /// The contextual type to explicitly apply to a literal `arguments:` | ||
| /// expression after it is wrapped in a closure for lazy evaluation. | ||
| /// |
There was a problem hiding this comment.
Need parameter and return callouts.
| arguments += testFunctionArguments.map { argument in | ||
| var copy = argument | ||
| copy.expression = .init(ClosureExprSyntax { argument.expression.trimmed }) | ||
| let argumentExpr = argument.expression.trimmed |
There was a problem hiding this comment.
We can simplify this part of the diff by modifying argumentExpr before creating the closure wrapper.
Preserve contextual type for literal
arguments:expressions in@TestMotivation:
Parameterized tests using
@Test(arguments:)may fail to compile when the collection is written as an array literal containing values that rely on contextual type inference.For example:
During macro expansion, the
arguments:expression is wrapped in a closure so the collection can be evaluated lazily:When the array literal is evaluated inside this closure, the contextual type that would normally be provided by the test function parameters is no longer available. As a result, values such as
nilcannot be inferred and the code fails to type-check.The same code works if the array literal is explicitly cast:
In this case the generated code becomes:
The explicit cast preserves the contextual type inside the closure and allows the literals to type-check.
Modifications:
When
arguments:is supplied as a single array literal, the macro derives the expected array type from the test function parameter list and applies it as an explicit cast inside the generated closure.For example, the generated expression becomes:
This preserves the contextual type required for literal inference while keeping the existing lazy evaluation behavior.
Macro expansion only has access to source syntax and does not observe the inferred collection type directly. For this reason, the array type is derived from the test function parameter list. The change is intentionally limited to the case where
arguments:is a single array literal so that other overloads remain unaffected.Additional tests verify that contextual types are preserved for empty array literals and tuple literals containing optionals.
Checklist: