Skip to content

Commit

Permalink
Simplify the logic for parsing and handling test arguments in the @test
Browse files Browse the repository at this point in the history
… macro
  • Loading branch information
stmontgomery committed Nov 22, 2024
1 parent 1df9545 commit 77c469b
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 34 deletions.
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[nonDisplayNameArguments.startIndex ..< 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

0 comments on commit 77c469b

Please sign in to comment.