Skip to content

Commit

Permalink
added support for array of output objects. (#8)
Browse files Browse the repository at this point in the history
* added support for array of output objects.

* code review changes
  • Loading branch information
mkowalski87 authored Nov 10, 2023
1 parent 067b489 commit 9634ce3
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
@attached(peer, names: arbitrary)
public macro Parametrize<T>(input: [T]) = #externalMacro(module: "XCTestParametrizedMacroMacros", type: "ParametrizeMacro")
public macro Parametrize<I>(input: [I]) = #externalMacro(module: "XCTestParametrizedMacroMacros", type: "ParametrizeMacro")

@attached(peer, names: arbitrary)
public macro Parametrize<I, O>(input: [I], output: [O]) = #externalMacro(module: "XCTestParametrizedMacroMacros", type: "ParametrizeMacro")
15 changes: 15 additions & 0 deletions Sources/XCTestParametrizedMacroClient/main.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import XCTestParametrizedMacro
import XCTest

enum Foo: Int {
case first = 1
case second = 2
case third = 3
}

func pow2(_ n: Int) -> Int {
n*n
}

class Test {
@Parametrize(input: [Foo.first, .second, .init(rawValue: 3)!])
func test_sample(input object: Foo) {
print(object.rawValue)
}

@Parametrize(input: [1,2,3], output: [1,4,9])
func testPow2(input n: Int, output result: Int) {
print("\(n) => \(result)")
}

@Parametrize(input: ["Swift","SwiftMacro"], output: [5, 10])
func testWordLength(input word: String, output length: Int) {
XCTAssertEqual(word.count, length)
}
}
34 changes: 34 additions & 0 deletions Sources/XCTestParametrizedMacroMacros/MacroDeclarationHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ struct MacroDeclarationHelper {
self.declaration = declaration
}

var funcName: TokenSyntax {
declaration.name
}

var funcStatements: CodeBlockItemListSyntax? {
declaration.body?.statements
}

/// Returns 'TokenSyntax' representing name of the input parameter.
var inputParamName: TokenSyntax? {
Expand All @@ -19,6 +26,16 @@ struct MacroDeclarationHelper {
declaration.signature.parameterClause.parameters.first?.type
}

/// Returns 'TokenSyntax' representing name of the output parameter.
var outputParamName: TokenSyntax? {
declaration.signature.parameterClause.parameters.last?.secondName
}

/// Returns 'TokenSyntax' representing type of the output object.
var outputParamType: TypeSyntax? {
declaration.signature.parameterClause.parameters.last?.type
}

var firstAttribute: AttributeSyntax? {
return declaration.attributes.first?.as(AttributeSyntax.self)
}
Expand All @@ -37,4 +54,21 @@ struct MacroDeclarationHelper {
}
}

var outputValues: ArrayElementListSyntax? {
get throws {
guard let firstMacroArgument = firstAttribute?.arguments?.as(LabeledExprListSyntax.self) else {
throw ParametrizeMacroError.macroAttributeNotAnArray
}

guard let outputArgument = firstMacroArgument.first(where: { $0.label?.text == "output" }) else {
return nil
}

guard let arrayOfValues = outputArgument.as(LabeledExprSyntax.self)?.expression.as(ArrayExprSyntax.self)?.elements else {
throw ParametrizeMacroError.macroAttributeNotAnArray
}

return arrayOfValues
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ enum ParametrizeMacroError: Error, CustomStringConvertible {
case functionInputParamTypeMissing
case functionBodyEmpty
case macroAttributeNotAnArray
case macroAttributeArraysMismatchSize

var description: String {
switch self {
Expand All @@ -18,7 +19,9 @@ enum ParametrizeMacroError: Error, CustomStringConvertible {
case .functionBodyEmpty:
return "Function must have a body."
case .macroAttributeNotAnArray:
return "Parametrize macro requires at least one attribute as array of input values."
return "Parametrize macro requires at least one attribute as array of input/output values."
case .macroAttributeArraysMismatchSize:
return "Size of the input array and output array should be the same."
}
}
}
95 changes: 95 additions & 0 deletions Sources/XCTestParametrizedMacroMacros/TestMethodsFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import Foundation
import SwiftSyntax

struct TestMethodsFactory {

let macroDeclarationHelper: MacroDeclarationHelper

var bodyFunc: String {
get throws {
guard let codeStatements = macroDeclarationHelper.funcStatements, codeStatements.count > 0 else {
throw ParametrizeMacroError.functionBodyEmpty
}
return codeStatements.map { "\($0.trimmed)" }.joined(separator: "\n")
}
}

func create() throws -> [DeclSyntax] {

let funcName = macroDeclarationHelper.funcName

guard let inputParamName = macroDeclarationHelper.inputParamName?.text else {
throw ParametrizeMacroError.functionInputParamSecondNameMissing
}

guard let inputParamType = macroDeclarationHelper.inputParamType else {
throw ParametrizeMacroError.functionInputParamTypeMissing
}

let outputParamName = macroDeclarationHelper.outputParamName?.text
let outputParamType = macroDeclarationHelper.outputParamType

let outputValues = try macroDeclarationHelper.outputValues
let inputValues = try macroDeclarationHelper.inputValues
if let outputValues = outputValues,
let outputParamName = outputParamName,
let outputParamType = outputParamType {
let input = inputValues.map { $0 }
let output = outputValues.map { $0 }
guard input.count == output.count else {
throw ParametrizeMacroError.macroAttributeArraysMismatchSize
}
return try zip(input, output).map { input, output in
"""
\(raw: buildTestMethodSignature(funcName: funcName, inputParamName: inputParamName, inputObject: input, outputParamName: outputParamName, outputObject: output))
\(raw: buildLocalVariables(inputParamName: inputParamName,
inputParamType: inputParamType,
inputObject: input,
outputParamName: outputParamName,
outputParamType: outputParamType,
outputObject: output))
\(raw: try bodyFunc)
}
"""
}
} else {
return try inputValues
.map {
"""
\(raw: buildTestMethodSignature(funcName: funcName, inputParamName: inputParamName, inputObject: $0))
\(raw: buildLocalVariables(inputParamName: inputParamName, inputParamType: inputParamType, inputObject: $0))
\(raw: try bodyFunc)
}
"""
}
}
}

func buildTestMethodSignature(funcName: TokenSyntax,
inputParamName: String,
inputObject: ArrayElementListSyntax.Element,
outputParamName: String? = nil,
outputObject: ArrayElementListSyntax.Element? = nil) -> String {
if let outputParamName = outputParamName, let outputObject = outputObject {
return "func \(funcName)_\(inputParamName.capitalizedFirst)_\(inputObject.asFunctionName)_\(outputParamName.capitalizedFirst)_\(outputObject.asFunctionName)() throws {"
} else {
return "func \(funcName)_\(inputParamName.capitalizedFirst)_\(inputObject.asFunctionName)() throws {"
}
}

func buildLocalVariables(inputParamName: String,
inputParamType: TypeSyntax,
inputObject: ArrayElementListSyntax.Element,
outputParamName: String? = nil,
outputParamType: TypeSyntax? = nil,
outputObject: ArrayElementListSyntax.Element? = nil) -> String {
var decl = "let \(inputParamName):\(inputParamType) = \(inputObject.expression)"
if let outputParamName = outputParamName,
let outputParamType = outputParamType,
let outputObject = outputObject {
decl.append("\n")
decl.append("let \(outputParamName):\(outputParamType) = \(outputObject.expression)")
}
return decl
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,7 @@ public struct ParametrizeMacro: PeerMacro {

let macroDeclarationHelper = MacroDeclarationHelper(declaration)

let funcName = declaration.name
guard let inputParamName = macroDeclarationHelper.inputParamName?.text else {
throw ParametrizeMacroError.functionInputParamSecondNameMissing
}

guard let inputParamType = macroDeclarationHelper.inputParamType else {
throw ParametrizeMacroError.functionInputParamTypeMissing
}

guard let codeStatements = declaration.body?.statements, codeStatements.count > 0 else {
throw ParametrizeMacroError.functionBodyEmpty
}

let textCode = codeStatements.map { "\($0.trimmed)" }.joined(separator: "\n")
return try macroDeclarationHelper.inputValues.map {
"""
func \(funcName)_\(raw: inputParamName.capitalizedFirst)_\(raw: $0.asFunctionName)() throws {
let \(raw: inputParamName):\(raw: inputParamType) = \($0.expression)
\(raw: textCode)
}
"""
}
return try TestMethodsFactory(macroDeclarationHelper: macroDeclarationHelper).create()
}
}

Expand Down
27 changes: 25 additions & 2 deletions Tests/XCTestParametrizedMacroTests/AttachmentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ final class AttachmentTests: XCTestCase {
}
""",
diagnostics: [
DiagnosticSpec(message: "Parametrize macro requires at least one attribute as array of input values.", line: 2, column: 5)
DiagnosticSpec(message: "Parametrize macro requires at least one attribute as array of input/output values.", line: 2, column: 5)
],
macros: testMacros
)
Expand All @@ -157,10 +157,33 @@ final class AttachmentTests: XCTestCase {
}
""",
diagnostics: [
DiagnosticSpec(message: "Parametrize macro requires at least one attribute as array of input values.", line: 2, column: 5)
DiagnosticSpec(message: "Parametrize macro requires at least one attribute as array of input/output values.", line: 2, column: 5)
],
macros: testMacros
)
}

func testParametrizeInputOutput_DifferentSizeOfArrays_ShouldFail() throws {
assertMacroExpansion(
"""
struct TestStruct {
@Parametrize(input: [1,2,3], output: [1,4])
func testPow2(input n: Int, output result: Int) {
XCTAssertEqual(pow2(n), result)
}
}
""",
expandedSource: """
struct TestStruct {
func testPow2(input n: Int, output result: Int) {
XCTAssertEqual(pow2(n), result)
}
}
""",
diagnostics: [
DiagnosticSpec(message: "Size of the input array and output array should be the same.", line: 2, column: 5)
],
macros: testMacros
)
}
}
105 changes: 105 additions & 0 deletions Tests/XCTestParametrizedMacroTests/InputOutputParametersTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest

import XCTestParametrizedMacroMacros

final class InputOutputParametersTests: XCTestCase {

let testMacros: [String: Macro.Type] = [
"Parametrize": ParametrizeMacro.self,
]

func testParametrizeInputOutput_SingleInts() throws {
assertMacroExpansion(
"""
struct TestStruct {
@Parametrize(input: [3], output: [9])
func testPow2(input n: Int, output result: Int) {
XCTAssertEqual(pow2(n),result)
}
}
""",
expandedSource: """
struct TestStruct {
func testPow2(input n: Int, output result: Int) {
XCTAssertEqual(pow2(n),result)
}
func testPow2_N_3_Result_9() throws {
let n: Int = 3
let result: Int = 9
XCTAssertEqual(pow2(n), result)
}
}
""",
macros: testMacros
)
}

func testParametrizeInputOutput_TwoInts() throws {
assertMacroExpansion(
"""
struct TestStruct {
@Parametrize(input: [4, 5], output: [16, 25])
func testPow2(input n: Int, output result: Int) {
XCTAssertEqual(pow2(n),result)
}
}
""",
expandedSource: """
struct TestStruct {
func testPow2(input n: Int, output result: Int) {
XCTAssertEqual(pow2(n),result)
}
func testPow2_N_4_Result_16() throws {
let n: Int = 4
let result: Int = 16
XCTAssertEqual(pow2(n), result)
}
func testPow2_N_5_Result_25() throws {
let n: Int = 5
let result: Int = 25
XCTAssertEqual(pow2(n), result)
}
}
""",
macros: testMacros
)
}

func testParametrizeInputOutput_TwoStringsTwoInts() throws {
assertMacroExpansion(
"""
struct TestStruct {
@Parametrize(input: ["Swift", "SwiftMacro"], output: [5, 10])
func testWordLength(input word: String, output length: Int) {
XCTAssertEqual(word.count, length)
}
}
""",
expandedSource: """
struct TestStruct {
func testWordLength(input word: String, output length: Int) {
XCTAssertEqual(word.count, length)
}
func testWordLength_Word_Swift_Length_5() throws {
let word: String = "Swift"
let length: Int = 5
XCTAssertEqual(word.count, length)
}
func testWordLength_Word_SwiftMacro_Length_10() throws {
let word: String = "SwiftMacro"
let length: Int = 10
XCTAssertEqual(word.count, length)
}
}
""",
macros: testMacros
)
}
}
1 change: 1 addition & 0 deletions Tests/XCTestParametrizedMacroTests/SimpleValuesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,5 @@ final class SimpleValuesTests: XCTestCase {
macros: testMacros
)
}

}

0 comments on commit 9634ce3

Please sign in to comment.