Skip to content

Preserve contextual type for literal arguments: expressions in @Test#1627

Open
ojun9 wants to merge 1 commit intoswiftlang:mainfrom
ojun9:fix/macro-literal-contextual-type
Open

Preserve contextual type for literal arguments: expressions in @Test#1627
ojun9 wants to merge 1 commit intoswiftlang:mainfrom
ojun9:fix/macro-literal-contextual-type

Conversation

@ojun9
Copy link
Contributor

@ojun9 ojun9 commented Mar 15, 2026

Preserve contextual type for literal arguments: expressions in @Test

Motivation:

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:

@Test(arguments: [
  (nil, 1),
  ("a", nil)
])
func f(s: String?, i: Int?) {}

During macro expansion, the arguments: expression is wrapped in a closure so the collection can be evaluated lazily:

arguments: { [...] }

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 nil cannot be inferred and the code fails to type-check.

The same code works if the array literal is explicitly cast:

@Test(arguments: [
  (nil, 1),
  ("a", nil)
] as [(String?, Int?)])
func f(s: String?, i: Int?) {}

In this case the generated code becomes:

arguments: { [...] as [(String?, Int?)] }

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:

arguments: { [...] as [(String?, Int?)] }

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:

  • Code and documentation should follow the style of the Style Guide.
  • If public symbols are renamed or modified, DocC references should be updated.

@grynspan
Copy link
Contributor

Type checking of arguments to the @Test macro occurs before the expansion itself. Did you test this change to confirm it actually works? The added test only checks the macro expansion (without any type checking).

Copy link
Contributor

@grynspan grynspan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional test coverage is needed too.

return nil
}

if testFunctionArguments.count == 1, expression.is(ArrayExprSyntax.self) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
guard !parameters.isEmpty else {
if parameters.isEmpty {

private func _contextualTypeForLiteralArgument(
for expression: ExprSyntax,
among testFunctionArguments: [Argument]
) -> String? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
) -> String? {
) -> TypeSyntax? {

// type itself, not tuple-shaped elements.
return "[\(parameter.baseTypeName)]"
}
let elementType = parameters.map(\.baseTypeName).joined(separator: ", ")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)]"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
///
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can simplify this part of the diff by modifying argumentExpr before creating the closure wrapper.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants