Skip to content
Open
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
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
# Version History

# 3.1.0

## Added

- Added support for enumerations by the `@Arbitrary` macro. For the .static generation type, the case marked with `@ArbitraryEnumStaticCase` is used. For .dynamic — a random one.

- Implemented the auxiliary `@ArbitraryEnumStaticCase` macro, which helps the @Arbitrary macro choose an enumeration case for the static generation type.

- Added generation of an extension with the static .arbitrary() function by the Arbitrary macro.

- Added the auxiliary `@Empted` macro for generating an empty collection by default in the `@Arbitrary` macro.

- The `@Empted` macro can only be attached to `Array` and `Set`.

- Removed redundant generation of default in switch for an enum with a single case in the `@AutoEquatable` macro.

- The `accessModifier` parameter for the `@Arbitrary` macro.

- Support for typed errors in methods with throws for mocks.

- Support for the `@available` attribute for `@Mock` properties and methods.

## Technical changes

- Added support for method overloading for `@Mock` and `@AnyMockable`.

- Raised the lower bound of `swift-syntax` to 601.0.0.

- `@Mock` and `@Arbitrary` macros are wrapped in `#if DEBUG ... #endif`.

## Fixed

- Implemented ignoring of computed properties of models in the `@Arbitrary` macro.

- The return value of a nested Enum inside an extension of the `@Arbitrary` macro.

- Fixed generation of arbitrary for deeply nested types (e.g., One.Two.Three) in the `@Arbitrary` macro.

- Support for a generic type inside a generic clause `Result<T, Error>`.

# 3.0.1

## Technical changes
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ let package = Package(
),
],
dependencies: [
.package(url: "https://github.com/swiftlang/swift-syntax.git", "600.0.0"..<"601.0.1"),
.package(url: "https://github.com/swiftlang/swift-syntax.git", "601.0.0"..<"602.0.0"),
],
targets: [
.macro(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// AnyMockableMacro.swift
// TestingMacroCollection
//
// Copyright © 2025 Ozon. All rights reserved.
// Copyright © 2026 Ozon. All rights reserved.
//

import Foundation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// AnyMockableParameters.swift
// TestingMacroCollection
//
// Copyright © 2025 Ozon. All rights reserved.
// Copyright © 2026 Ozon. All rights reserved.
//

import Foundation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// AnyMockableParametersHandler.swift
// TestingMacroCollection
//
// Copyright © 2025 Ozon. All rights reserved.
// Copyright © 2026 Ozon. All rights reserved.
//

import SwiftSyntax
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// ArbitraryDefaultCaseMacro.swift
// TestingMacroCollection
//
// Copyright © 2026 Ozon. All rights reserved.
//

import Foundation
import SwiftSyntax
import SwiftSyntaxMacros

public struct ArbitraryDefaultCaseMacro: PeerMacro {
public static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
[]
}
}
269 changes: 269 additions & 0 deletions Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryEnumFlow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
//
// ArbitraryEnumFlow.swift
// TestingMacroCollection
//
// Copyright © 2026 Ozon. All rights reserved.
//

import Foundation
import SwiftSyntax
import SwiftSyntaxMacros

extension ArbitraryMacro {
/// Creates an `arbitrary` method for the enumeration.
///
/// - Parameters:
/// - accessModifier: Access modifier for the `arbitrary` method.
/// - enumDecl: Declaration of the enumeration for which the stub is being created.
/// - arbitraryConfig: The `arbitrary` type — `static` or `dynamic`.
/// - Returns: The `arbitrary` method.
/// - Throws: `ArbitraryMacroError`.
static func makeArbitraryMethodForEnum(
type: TypeSyntax? = nil,
accessModifier: DeclModifierSyntax,
enumDecl: any DeclGroupSyntax,
arbitraryConfig: ArbitraryConfig
) throws -> FunctionDeclSyntax {
// Check that the declaration is indeed from an enumeration.
// We do not accept `enumDecl: EnumDeclSyntax` as input for ease of use.
guard let enumDecl = enumDecl.as(EnumDeclSyntax.self) else {
throw ArbitraryMacroError.unsupportedType
}

// Create modifiers for the `arbitrary` function declaration.
let modifiers = [accessModifier, .init(name: .keyword(.static))]
.reduce(into: DeclModifierListSyntax()) { partialResult, modifier in
guard !modifier.isInternal else { return }
partialResult.append(modifier)
}

// <- Get all case declarations of the enumeration.
// Note: a single `case` declaration can contain multiple cases, e.g. `case a, b`
let allEnumCaseDeclarations = enumDecl
.memberBlock
.members
.compactMap { $0.decl.as(EnumCaseDeclSyntax.self) }

let allCaseElements = allEnumCaseDeclarations.flatMap(\.elements)
guard !allCaseElements.isEmpty else {
throw ArbitraryMacroError.enumHasNoCases
}
// ->

// Form the function signature.
let functionSignature = FunctionSignatureSyntax(
parameterClause: .init(parameters: []),
returnClause: getReturnClause(type: type, typeName: enumDecl.name)
)

// <- Form the function body.
let functionBody: CodeBlockSyntax = switch arbitraryConfig {
case .static:
try buildFunctionBodyForStatic(
enumDecl: enumDecl,
allEnumCaseDeclarations: allEnumCaseDeclarations
)
case .dynamic:
try buildFunctionBodyForDynamic(enumDecl: enumDecl, allCaseElements: allCaseElements)
}
// ->

// Form the function.
let functionDeclaration = FunctionDeclSyntax(
modifiers: modifiers,
name: .identifier(String.arbitrary),
signature: functionSignature,
body: functionBody
)

return functionDeclaration
}

/// Creates the body of the `arbitrary` function for `.dynamic` generation.
///
/// - Parameters:
/// - enumDecl: Declaration of the enumeration for which the stub is being created.
/// - allCaseElements: All cases of the enumeration.
/// - Returns: The function body consisting of two blocks: 1. `let allCases = [...]` 2. `return allCases.random()!`.
/// - Throws: `ArbitraryMacroError`.
private static func buildFunctionBodyForDynamic(
enumDecl: EnumDeclSyntax,
allCaseElements: [EnumCaseElementListSyntax.Element]
) throws -> CodeBlockSyntax {
// Create the elements of the `allCases` array.
let allCasesArrayElements = try allCaseElements.enumerated().map { index, caseElement in
// Create an element of the `allCases` array without the surrounding syntax.
let allCasesArrayElementWithoutSyntax = try buildCaseExpression(
caseElement: caseElement,
enumDecl: enumDecl,
arbitraryConfig: .dynamic
)

// Create an element of the `allCases` array with the surrounding syntax.
let allCasesArrayElementWithSyntax = ArrayElementSyntax(
expression: allCasesArrayElementWithoutSyntax,
trailingComma: index == allCaseElements.count - 1 ? nil : .commaToken()
)

return allCasesArrayElementWithSyntax
}

// Create the `allCases` array.
let allCasesArrayWithSyntax = ArrayExprSyntax(
leftSquare: .leftSquareToken(),
elements: ArrayElementListSyntax(allCasesArrayElements),
rightSquare: .rightSquareToken()
)

let allCasesVarName = "allCases"

// Create the `allCases` variable without the surrounding syntax.
let allCasesVarWithoutSyntax = VariableDeclSyntax(
.let,
name: PatternSyntax(stringLiteral: allCasesVarName),
type: TypeAnnotationSyntax(
type: ArrayTypeSyntax(
element: IdentifierTypeSyntax(name: enumDecl.name)
)
),
initializer: InitializerClauseSyntax(value: ExprSyntax(allCasesArrayWithSyntax))
)

// Create the `allCases` variable with the surrounding syntax.
let allCasesVarWithSyntax = DeclSyntax(allCasesVarWithoutSyntax)

// <- Create a call to the `randomElement` function on the `allCases` variable.
let randomElementCall = FunctionCallExprSyntax(
calledExpression: MemberAccessExprSyntax(
base: DeclReferenceExprSyntax(baseName: .identifier(allCasesVarName)),
period: .periodToken(),
declName: DeclReferenceExprSyntax(baseName: .identifier(.randomElementFunctionName))
),
leftParen: .leftParenToken(),
arguments: LabeledExprListSyntax(),
rightParen: .rightParenToken()
)

let forceUnwrap = ForceUnwrapExprSyntax(expression: randomElementCall)
let returnStatement = StmtSyntax(ReturnStmtSyntax(expression: ExprSyntax(forceUnwrap)))
// ->

let functionBody = CodeBlockSyntax(
statements: CodeBlockItemListSyntax([
CodeBlockItemSyntax(item: .decl(allCasesVarWithSyntax)),
CodeBlockItemSyntax(item: .stmt(returnStatement)),
])
)

return functionBody
}

/// Creates the body of the `arbitrary` function for `.static` generation.
///
/// - Parameters:
/// - enumDecl: Declaration of the enumeration for which the stub is being created.
/// - allEnumCaseDeclarations: All cases of the enumeration.
/// - Returns: The function body consisting of a single block: `return ...`.
/// - Throws: `ArbitraryMacroError`.
private static func buildFunctionBodyForStatic(
enumDecl: EnumDeclSyntax,
allEnumCaseDeclarations: [EnumCaseDeclSyntax]
) throws -> CodeBlockSyntax {
var selectedCaseElement: EnumCaseElementListSyntax.Element?

for enumCaseDecl in allEnumCaseDeclarations {
for attribute in enumCaseDecl.attributes {
if case .attribute(let attributeSyntax) = attribute,
attributeSyntax.attributeName.trimmedDescription == String.arbitraryDefaultCaseMacro {
guard let element = enumCaseDecl.elements.first else {
continue
}
selectedCaseElement = element
break
}
}

if selectedCaseElement != nil {
break
}
}

guard let selectedCaseElement else {
throw ArbitraryMacroError.enumWithStaticArbitraryTypeMustHasDefaultValue
}

let returnExpression = try buildCaseExpression(
caseElement: selectedCaseElement,
enumDecl: enumDecl,
arbitraryConfig: .static
)
let returnStatement = StmtSyntax(ReturnStmtSyntax(expression: returnExpression))

let functionBody = CodeBlockSyntax(
statements: [
CodeBlockItemSyntax(item: .stmt(returnStatement)), // `return ...`
]
)
return functionBody
}

/// Creates an `.arbitrary` call for an enumeration case.
///
/// - Parameters:
/// - caseElement: The enumeration case.
/// - enumDecl: Declaration of the enumeration.
/// - arbitraryConfig: The generation type.
/// - Returns: An expression calling `.arbitrary` for the `caseElement`.
/// - Throws: `ArbitraryMacroError`.
private static func buildCaseExpression(
caseElement: EnumCaseElementListSyntax.Element,
enumDecl: EnumDeclSyntax,
arbitraryConfig: ArbitraryConfig
) throws -> ExprSyntax {
if let parameters = caseElement.parameterClause?.parameters {
var argumentList = LabeledExprListSyntax()
for param in parameters.enumerated() {
let paramType = param.element.type
guard let defaultValueExpr = makeDefaultValueExprSyntaxForType(
paramType,
parentTypeName: enumDecl.name,
arbitraryConfig: arbitraryConfig
) else {
throw ArbitraryMacroError.unsupportedType
}
argumentList.append(
LabeledExprSyntax(
label: param.element.firstName,
colon: param.element.firstName != nil ? .colonToken() : nil,
expression: defaultValueExpr,
trailingComma: param.offset == parameters.count - 1 ? nil : .commaToken()
)
)
}
let functionCall = ExprSyntax(
FunctionCallExprSyntax(
calledExpression: MemberAccessExprSyntax(
period: .periodToken(),
declName: DeclReferenceExprSyntax(baseName: caseElement.name)
),
leftParen: .leftParenToken(),
arguments: argumentList,
rightParen: .rightParenToken()
)
)

return functionCall
// Handle the case when the enumeration case has no associated types.
// Form a reference to the enumeration case.
} else {
let memberAccess = ExprSyntax(
MemberAccessExprSyntax(
period: .periodToken(),
declName: DeclReferenceExprSyntax(baseName: caseElement.name)
)
)

return memberAccess
}
}
}
Loading