Skip to content

Simplify the logic for parsing and handling test arguments in the @Test macro #834

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
53 changes: 25 additions & 28 deletions Sources/TestingMacros/Support/AttributeDiscovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,19 @@ struct AttributeInfo {
/// The traits applied to the attribute, if any.
var traits = [ExprSyntax]()

/// Test arguments passed to a parameterized test function, if any.
///
/// When non-`nil`, the value of this property is an array beginning with the
/// argument passed to this attribute for the parameter labeled `arguments:`
/// followed by all of the remaining, unlabeled arguments.
var testFunctionArguments: [Argument]?

/// Whether or not this attribute specifies arguments to the associated test
/// function.
var hasFunctionArguments: Bool {
otherArguments.lazy
.compactMap(\.label?.tokenKind)
.contains(.identifier("arguments"))
testFunctionArguments != nil
}

/// Additional arguments passed to the attribute, if any.
var otherArguments = [Argument]()

/// The source location of the attribute.
///
/// When parsing, the testing library uses the start of the attribute's name
Expand All @@ -98,6 +100,7 @@ struct AttributeInfo {
init(byParsing attribute: AttributeSyntax, on declaration: some SyntaxProtocol, in context: some MacroExpansionContext) {
self.attribute = attribute

var nonDisplayNameArguments: [Argument] = []
if let arguments = attribute.arguments, case let .argumentList(argumentList) = arguments {
// If the first argument is an unlabelled string literal, it's the display
// name of the test or suite. If it's anything else, including a nil
Expand All @@ -106,11 +109,11 @@ struct AttributeInfo {
let firstArgumentHasLabel = (firstArgument.label != nil)
if !firstArgumentHasLabel, let stringLiteral = firstArgument.expression.as(StringLiteralExprSyntax.self) {
displayName = stringLiteral
otherArguments = argumentList.dropFirst().map(Argument.init)
nonDisplayNameArguments = argumentList.dropFirst().map(Argument.init)
} else if !firstArgumentHasLabel, firstArgument.expression.is(NilLiteralExprSyntax.self) {
otherArguments = argumentList.dropFirst().map(Argument.init)
nonDisplayNameArguments = argumentList.dropFirst().map(Argument.init)
} else {
otherArguments = argumentList.map(Argument.init)
nonDisplayNameArguments = argumentList.map(Argument.init)
}
}
}
Expand All @@ -119,7 +122,7 @@ struct AttributeInfo {
// See _SelfRemover for more information. Rewriting a syntax tree discards
// location information from the copy, so only invoke the rewriter if the
// `Self` keyword is present somewhere.
otherArguments = otherArguments.map { argument in
nonDisplayNameArguments = nonDisplayNameArguments.map { argument in
var expr = argument.expression
if argument.expression.tokens(viewMode: .sourceAccurate).map(\.tokenKind).contains(.keyword(.Self)) {
let selfRemover = _SelfRemover(in: context)
Expand All @@ -131,15 +134,14 @@ struct AttributeInfo {
// Look for any traits in the remaining arguments and slice them off. Traits
// are the remaining unlabelled arguments. The first labelled argument (if
// present) is the start of subsequent context-specific arguments.
if !otherArguments.isEmpty {
if let labelledArgumentIndex = otherArguments.firstIndex(where: { $0.label != nil }) {
if !nonDisplayNameArguments.isEmpty {
if let labelledArgumentIndex = nonDisplayNameArguments.firstIndex(where: { $0.label != nil }) {
// There is an argument with a label, so splice there.
traits = otherArguments[otherArguments.startIndex ..< labelledArgumentIndex].map(\.expression)
otherArguments = Array(otherArguments[labelledArgumentIndex...])
traits = nonDisplayNameArguments[..<labelledArgumentIndex].map(\.expression)
testFunctionArguments = Array(nonDisplayNameArguments[labelledArgumentIndex...])
} else {
// No argument has a label, so all the remaining arguments are traits.
traits = otherArguments.map(\.expression)
otherArguments.removeAll(keepingCapacity: false)
traits = nonDisplayNameArguments.map(\.expression)
}
}

Expand Down Expand Up @@ -178,21 +180,16 @@ struct AttributeInfo {
}
}))

// Any arguments of the test declaration macro which specify test arguments
// need to be wrapped a closure so they may be evaluated lazily by the
// testing library at runtime. If any such arguments are present, they will
// begin with a labeled argument named `arguments:` and include all
// subsequent unlabeled arguments.
var otherArguments = self.otherArguments
if let argumentsIndex = otherArguments.firstIndex(where: { $0.label?.tokenKind == .identifier("arguments") }) {
for index in argumentsIndex ..< otherArguments.endIndex {
var argument = otherArguments[index]
argument.expression = .init(ClosureExprSyntax { argument.expression.trimmed })
otherArguments[index] = argument
// If there are any parameterized test function arguments, wrap each in a
// closure so they may be evaluated lazily at runtime.
if let testFunctionArguments {
arguments += testFunctionArguments.map { argument in
var copy = argument
copy.expression = .init(ClosureExprSyntax { argument.expression.trimmed })
return copy
}
}

arguments += otherArguments
arguments.append(Argument(label: "sourceLocation", expression: sourceLocation))

return LabeledExprListSyntax(arguments)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,7 @@ private func _diagnoseIssuesWithParallelizationTrait(_ traitExpr: MemberAccessEx
return
}

let hasArguments = attributeInfo.otherArguments.lazy
.compactMap(\.label?.textWithoutBackticks)
.contains("arguments")
if !hasArguments {
if !attributeInfo.hasFunctionArguments {
// Serializing a non-parameterized test function has no effect.
context.diagnose(.traitHasNoEffect(traitExpr, in: attributeInfo.attribute))
}
Expand Down
3 changes: 1 addition & 2 deletions Sources/TestingMacros/TestDeclarationMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -425,9 +425,8 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
// case the availability checks fail below.
let unavailableTestName = context.makeUniqueName(thunking: functionDecl)

// TODO: don't assume otherArguments is only parameterized function arguments
var attributeInfo = attributeInfo
attributeInfo.otherArguments = []
attributeInfo.testFunctionArguments = nil
result.append(
"""
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
Expand Down