Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion Sources/TestingMacros/Support/AttributeDiscovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,12 @@ struct AttributeInfo {
if let testFunctionArguments {
arguments += testFunctionArguments.map { argument in
var copy = argument
copy.expression = .init(ClosureExprSyntax { argument.expression.trimmed })
let argumentExpr = argument.expression.trimmed
Copy link
Copy Markdown
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.

if let contextualType = _contextualTypeForLiteralArgument(for: argumentExpr, among: testFunctionArguments) {
copy.expression = .init(ClosureExprSyntax { "\(argumentExpr) as \(raw: contextualType)" as ExprSyntax })
} else {
copy.expression = .init(ClosureExprSyntax { argumentExpr })
}
return copy
}
}
Expand All @@ -180,4 +185,38 @@ struct AttributeInfo {

return LabeledExprListSyntax(arguments)
}

/// The contextual type to explicitly apply to a literal `arguments:`
/// expression after it is wrapped in a closure for lazy evaluation.
///
Comment thread
ojun9 marked this conversation as resolved.
/// Parameterized `@Test` declarations are modeled in terms of the collection
/// type supplied to the macro, but macro expansion only sees source syntax.
/// When the `arguments:` parameter is supplied as a single array literal,
/// reconstruct the array type from the test function's parameters so the
/// literal retains enough contextual type information after lazy wrapping.
private func _contextualTypeForLiteralArgument(
for expression: ExprSyntax,
among testFunctionArguments: [Argument]
) -> String? {
Copy link
Copy Markdown
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? {

guard let functionDecl = declaration.as(FunctionDeclSyntax.self) else {
return nil
}

let parameters = functionDecl.signature.parameterClause.parameters
guard !parameters.isEmpty else {
Copy link
Copy Markdown
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 {

return nil
}

if testFunctionArguments.count == 1, expression.is(ArrayExprSyntax.self) {
Copy link
Copy Markdown
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.

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
Copy Markdown
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.

}
let elementType = parameters.map(\.baseTypeName).joined(separator: ", ")
Copy link
Copy Markdown
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?

return "[(\(elementType))]"
}

return nil
}
}
29 changes: 29 additions & 0 deletions Tests/TestingMacrosTests/TestDeclarationMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,35 @@ struct TestDeclarationMacroTests {
}
}

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

@Test("Literal arguments preserve contextual types after lazy wrapping", arguments: parameterizedArgumentTypePreservationInputs)
func preservesParameterizedArgumentTypes(input: String, expectedOutput: String) throws {
let (output, _) = try parse(input, removeWhitespace: true)
#expect(output.contains(expectedOutput))
}

@Test("Display name is preserved",
arguments: [
#"@Test("Display Name") func f() {}"#,
Expand Down
Loading