From d6c91438a98721fcbe213a1addc78adc6f05dad5 Mon Sep 17 00:00:00 2001 From: Golovin Dmitrii Mikhaylovich Date: Fri, 24 Apr 2026 14:15:16 +0300 Subject: [PATCH] Release 3.1.0 --- CHANGELOG.md | 40 + Package.swift | 2 +- .../AnyMockableMacro/AnyMockableMacro.swift | 2 +- .../AnyMockableParameters.swift | 2 +- .../AnyMockableParametersHandler.swift | 2 +- .../ArbitraryDefaultCaseMacro.swift | 20 + .../ArbitraryMacro/ArbitraryEnumFlow.swift | 269 +++++ .../ArbitraryExtensionMacro.swift | 78 ++ .../ArbitraryMacro+Extension.swift | 86 ++ .../ArbitraryMacro/ArbitraryMacro.swift | 116 +-- .../ArbitraryMacroContext.swift | 19 + .../ArbitraryMacro/ArbitraryMacroError.swift | 8 +- .../ArbitraryMacroInputParameters.swift | 69 ++ .../ArbitraryMacro/ArbitraryModelFlow.swift | 27 +- .../ArbitraryMacro/ArbitraryParameter.swift | 6 +- .../ArbitraryProtocolFlow.swift | 60 +- .../ArbitraryMacro/MemberArbitraryMacro.swift | 43 +- .../AutoEquatableMacro.swift | 8 +- .../Builders/ClearFunctionBlockBuilder.swift | 5 +- .../Builders/DeinitBlockBuilder.swift | 2 +- .../Builders/FunctionsBlockBuilder.swift | 29 +- .../Builders/InitializerBlockBuilder.swift | 2 +- .../Builders/SetPropertyFunctionBuilder.swift | 2 +- .../Builders/TypealiasesBlockBuilder.swift | 2 +- .../Builders/VariablesBlockBuilder.swift | 12 +- .../CoreExtensions/ArrayTypeSyntax+Ext.swift | 2 +- .../CoreExtensions/ConstantStrings.swift | 9 +- .../CoreExtensions/DeclGroupSyntax+Ext.swift | 2 +- .../DeclModifierSyntax+Ext.swift | 2 +- .../DeclReferenceExprSyntax+Ext.swift | 2 +- .../CoreExtensions/DeclSyntax+Ext.swift | 2 +- .../DictionaryTypeSyntax+Ext.swift | 2 +- .../ExprSyntaxProtocol+Expr.swift | 2 +- .../IdentifierTypeSyntax+Ext.swift | 2 +- .../InitializerClauseSyntax+Ext.swift | 2 +- .../InitializerDeclSyntax+Ext.swift | 2 +- .../ProtocolDeclSyntax+Ext.swift | 2 +- .../CoreExtensions/StructDeclSyntax+Ext.swift | 2 +- .../CoreExtensions/TupleTypeSyntax+Ext.swift | 2 +- .../CoreExtensions/TypeSyntax+Ext.swift | 2 +- .../VariableDeclSyntax+Ext.swift | 12 +- Sources/OzonTestingMacros/EmptedMacro.swift | 41 + .../FunctionBodyMockMacro.swift | 4 +- .../Helpers/AllowedAttributes.swift | 8 + .../OzonTestingMacros/Helpers/BuildType.swift | 14 + .../Helpers/ForceUnwrapped.swift | 2 +- .../Helpers/IdentifierPatternSyntax+Ext.swift | 2 +- .../Helpers/InheritedTypeListSyntax+Ext.swift | 2 +- .../Helpers/MacroArgumentExtracter.swift | 2 +- .../Helpers/String+Capitalized.swift | 2 +- .../Helpers/SyntaxAnalizer.swift | 7 +- .../Helpers/Trivia+Ext.swift | 22 + Sources/OzonTestingMacros/Helpers/Type.swift | 2 +- Sources/OzonTestingMacros/IgnoredMacro.swift | 2 +- .../OzonTestingMacros/MockAccessorMacro.swift | 2 +- .../MockMacro/MockMacro.swift | 12 +- .../MockMacro/MockMacroInputParameters.swift | 4 +- .../MockMacroInputParametersHandler.swift | 10 +- Sources/OzonTestingMacros/NilableMacro.swift | 2 +- .../PerformanceMeasureMacro.swift | 2 +- .../Plugin/TestingMacroCollectionPlugin.swift | 4 +- .../Rewriters/GenericTypeRewriter.swift | 24 +- .../Visitors/ClosureFinder.swift | 2 +- .../Visitors/GenericFinder.swift | 2 +- .../Visitors/PrettyNameTypeFinder.swift | 2 +- .../ArbitraryAccessModifier.swift | 12 + .../Arbitrary+Ext/Foundation+Arbitrary.swift | 2 +- .../AtomicLock/AtomicLock.swift | 2 +- .../Mock/AccessModifier.swift | 2 +- .../Mock/BuildType.swift | 13 + .../ProxyableMock/ProxyableMock.swift | 2 +- .../TestingMacroCollection.swift | 75 +- .../TestingMacroCollectionSandbox/main.swift | 137 ++- .../AnyMockableMacroTests.swift | 131 +++ .../ArbitraryMacroTests.swift | 945 +++++++++++++++++- .../AutoEquatableMacroTests.swift | 27 + .../MockMacroTests.swift | 435 ++++++++ .../TestingMacroCollectionTests.swift | 21 +- Versions.xcconfig | 2 +- 79 files changed, 2720 insertions(+), 222 deletions(-) create mode 100644 Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryDefaultCaseMacro.swift create mode 100644 Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryEnumFlow.swift create mode 100644 Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryExtensionMacro.swift create mode 100644 Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacro+Extension.swift create mode 100644 Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacroContext.swift create mode 100644 Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacroInputParameters.swift create mode 100644 Sources/OzonTestingMacros/EmptedMacro.swift create mode 100644 Sources/OzonTestingMacros/Helpers/AllowedAttributes.swift create mode 100644 Sources/OzonTestingMacros/Helpers/BuildType.swift create mode 100644 Sources/OzonTestingMacros/Helpers/Trivia+Ext.swift create mode 100644 Sources/TestingMacroCollection/Arbitrary+Ext/ArbitraryAccessModifier.swift create mode 100644 Sources/TestingMacroCollection/Mock/BuildType.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e854e7..64cac85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. + # 3.0.1 ## Technical changes diff --git a/Package.swift b/Package.swift index 6a30855..7607661 100644 --- a/Package.swift +++ b/Package.swift @@ -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( diff --git a/Sources/OzonTestingMacros/AnyMockableMacro/AnyMockableMacro.swift b/Sources/OzonTestingMacros/AnyMockableMacro/AnyMockableMacro.swift index 721b6d7..470d616 100644 --- a/Sources/OzonTestingMacros/AnyMockableMacro/AnyMockableMacro.swift +++ b/Sources/OzonTestingMacros/AnyMockableMacro/AnyMockableMacro.swift @@ -2,7 +2,7 @@ // AnyMockableMacro.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation diff --git a/Sources/OzonTestingMacros/AnyMockableMacro/AnyMockableParameters.swift b/Sources/OzonTestingMacros/AnyMockableMacro/AnyMockableParameters.swift index 2b4de68..a3b6f1b 100644 --- a/Sources/OzonTestingMacros/AnyMockableMacro/AnyMockableParameters.swift +++ b/Sources/OzonTestingMacros/AnyMockableMacro/AnyMockableParameters.swift @@ -2,7 +2,7 @@ // AnyMockableParameters.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation diff --git a/Sources/OzonTestingMacros/AnyMockableMacro/AnyMockableParametersHandler.swift b/Sources/OzonTestingMacros/AnyMockableMacro/AnyMockableParametersHandler.swift index 94f14cd..29b67ee 100644 --- a/Sources/OzonTestingMacros/AnyMockableMacro/AnyMockableParametersHandler.swift +++ b/Sources/OzonTestingMacros/AnyMockableMacro/AnyMockableParametersHandler.swift @@ -2,7 +2,7 @@ // AnyMockableParametersHandler.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryDefaultCaseMacro.swift b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryDefaultCaseMacro.swift new file mode 100644 index 0000000..b8f203e --- /dev/null +++ b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryDefaultCaseMacro.swift @@ -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] { + [] + } +} diff --git a/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryEnumFlow.swift b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryEnumFlow.swift new file mode 100644 index 0000000..eba23d8 --- /dev/null +++ b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryEnumFlow.swift @@ -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 + } + } +} diff --git a/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryExtensionMacro.swift b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryExtensionMacro.swift new file mode 100644 index 0000000..1f27c4f --- /dev/null +++ b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryExtensionMacro.swift @@ -0,0 +1,78 @@ +// +// ArbitraryExtensionMacro.swift +// TestingMacroCollection +// +// Copyright © 2026 Ozon. All rights reserved. +// + +import SwiftSyntax +import SwiftSyntaxMacros + +extension ArbitraryMacro: ExtensionMacro { + public static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) -> [ExtensionDeclSyntax] { + guard let declGroup = declaration.asProtocol(DeclGroupSyntax.self), let typeName = getTypeNameFromDecl(declGroup) else { + return [] + } + + do { + let context = try makeContext( + node: node, + declaration: declaration, + declGroup: declGroup, + typeName: typeName + ) + + let arbitraryMethod: FunctionDeclSyntax? = switch context.arbitraryType { + case .mock: + /// Generation of an `extension` for protocols is not required, as it cannot be used. + nil + case .model: + makeArbitraryMethodForModel( + type: .init(type), + accessModifier: context.accessModifier, + typeName: typeName, + parameters: context.parameters, + arbitraryConfig: context.arbitraryConfig, + declMembers: declGroup.memberBlock.members + ) + case .enumeration: + try makeArbitraryMethodForEnum( + type: .init(type), + accessModifier: declGroup.accessModifier, + enumDecl: declGroup, + arbitraryConfig: context.arbitraryConfig + ) + } + + guard let arbitraryMethod else { + return [] + } + + let leadingTrivia = Trivia.ifDebug.ifNeeded(context.buildType == .debug) + let trailingTrivia = Trivia.endif.ifNeeded(context.buildType == .debug) + let extensionDecl = ExtensionDeclSyntax( + leadingTrivia: leadingTrivia, + extendedType: type, + memberBlock: .init( + members: .init( + arrayLiteral: .init( + decl: arbitraryMethod + ) + ) + ), + trailingTrivia: trailingTrivia + ) + + return [extensionDecl] + } catch { + /// Return an empty array of declarations to avoid duplicate error reporting. + return [] + } + } +} diff --git a/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacro+Extension.swift b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacro+Extension.swift new file mode 100644 index 0000000..f0c9cc0 --- /dev/null +++ b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacro+Extension.swift @@ -0,0 +1,86 @@ +// +// ArbitraryMacro+Extension.swift +// TestingMacroCollection +// +// Copyright © 2026 Ozon. All rights reserved. +// + +import SwiftSyntax + +extension ArbitraryMacro { + static func makeContext( + node: AttributeSyntax, + declaration: some DeclSyntaxProtocol, + declGroup: DeclGroupSyntax, + typeName: TokenSyntax + ) throws -> ArbitraryMacroContext { + let arbitraryType = try defineArbitraryTypeForDecl(declaration) + let arbitraryInputParameters = ArbitraryMacroInputParameters( + node: node, + declGroup: declGroup + ) + + let parameters = declGroup.variables + .reduce(into: [ArbitraryParameter]()) { partialResult, variable in + guard let name = variable.name?.identifier, + let type = variable.type?.type else { return } + partialResult.append( + ArbitraryParameter( + name: name, + type: type, + isIgnored: variable.isIgnored, + isNilable: variable.isNilable, + isEmpted: variable.isEmpted, + isWithAccessor: variable.bindings.first?.accessorBlock != nil + ) + ) + } + + return ArbitraryMacroContext( + arbitraryType: arbitraryType, + arbitraryConfig: arbitraryInputParameters.arbitraryConfig, + accessModifier: arbitraryInputParameters.accessModifier, + buildType: arbitraryInputParameters.buildType, + typeName: typeName, + parameters: parameters, + declMembers: declGroup.memberBlock.members + ) + } + + static func defineArbitraryTypeForDecl(_ declaration: DeclSyntaxProtocol) throws -> ArbitaryType { + if declaration.is(ProtocolDeclSyntax.self) { + return .mock + } else if declaration.is(StructDeclSyntax.self) || + declaration.is(ClassDeclSyntax.self) || + declaration.is(ActorDeclSyntax.self) { + return .model + } else if declaration.is(EnumDeclSyntax.self) { + return .enumeration + } else { + throw ArbitraryMacroError.unsupportedType + } + } + + /// Extracts the type name of the declaration. + /// + /// - Parameter decl: Declaration from which to extract the type. + /// - Returns: Syntax of the declaration name. + static func getTypeNameFromDecl(_ decl: DeclGroupSyntax) -> TokenSyntax? { + switch decl.kind { + case .classDecl: + decl.as(ClassDeclSyntax.self)?.name + case .structDecl: + decl.as(StructDeclSyntax.self)?.name + case .extensionDecl: + decl.as(ExtensionDeclSyntax.self)?.extendedType.as(IdentifierTypeSyntax.self)?.name + case .actorDecl: + decl.as(ActorDeclSyntax.self)?.name + case .enumDecl: + decl.as(EnumDeclSyntax.self)?.name + case .protocolDecl: + decl.as(ProtocolDeclSyntax.self)?.name + default: + nil + } + } +} diff --git a/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacro.swift b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacro.swift index e8e8069..5c5b2ac 100644 --- a/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacro.swift +++ b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacro.swift @@ -2,7 +2,7 @@ // ArbitraryMacro.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation @@ -32,6 +32,8 @@ public struct ArbitraryMacro: PeerMacro { case mock /// Generates `Arbitrary` for the model using initializer. case model + /// Generates `Arbitrary` for one of the cases depending on the generation type. + case enumeration } enum ArbitraryConfig: String { @@ -49,41 +51,50 @@ public struct ArbitraryMacro: PeerMacro { guard let declGroup = declaration.asProtocol(DeclGroupSyntax.self), let typeName = getTypeNameFromDecl(declGroup) else { throw ArbitraryMacroError.unsupportedType } - let arbitraryType = try defineArbitraryTypeForDecl(declaration) - let arbitraryConfig = try defineArbitraryConfig(from: node) - let accessModifier = declGroup.accessModifier - let parameters = declGroup.variables - .reduce(into: [ArbitraryParameter]()) { partialResult, variable in - guard let name = variable.name?.identifier, let type = variable.type?.type else { return } + let context = try makeContext( + node: node, + declaration: declaration, + declGroup: declGroup, + typeName: typeName + ) - partialResult.append(.init(name: name, type: type, isIgnored: variable.isIgnored, isNilable: variable.isNilable)) - } - let arbitraryMethod: FunctionDeclSyntax = switch arbitraryType { + let arbitraryMethod: FunctionDeclSyntax = switch context.arbitraryType { case .mock: makeArbitraryMethodForMock( - accessModifier: accessModifier, + accessModifier: context.accessModifier, typeName: typeName, - parameters: parameters, - arbitraryConfig: arbitraryConfig, + parameters: context.parameters, + arbitraryConfig: context.arbitraryConfig, declMembers: declGroup.memberBlock.members ) case .model: makeArbitraryMethodForModel( - accessModifier: accessModifier, + accessModifier: context.accessModifier, typeName: typeName, - parameters: parameters, - arbitraryConfig: arbitraryConfig, + parameters: context.parameters, + arbitraryConfig: context.arbitraryConfig, declMembers: declGroup.memberBlock.members ) + case .enumeration: + try makeArbitraryMethodForEnum( + accessModifier: context.accessModifier, + enumDecl: declGroup, + arbitraryConfig: context.arbitraryConfig + ) } + let leadingTrivia = Trivia.ifDebug.ifNeeded(context.buildType == .debug) + let trailingTrivia = Trivia.endif.ifNeeded(context.buildType == .debug) + return [ .init( makeEnumDecl( - accessModifier: accessModifier, + leadingTrivia: leadingTrivia, + accessModifier: context.accessModifier, typeName: typeName.text, - arbitraryMethod: arbitraryMethod + arbitraryMethod: arbitraryMethod, + trailingTrivia: trailingTrivia ) ), ] @@ -98,9 +109,11 @@ public struct ArbitraryMacro: PeerMacro { /// - Returns: `enum` declaration with `arbitrary` method. /// static func makeEnumDecl( + leadingTrivia: Trivia? = nil, accessModifier: DeclModifierSyntax, typeName: String, - arbitraryMethod: FunctionDeclSyntax + arbitraryMethod: FunctionDeclSyntax, + trailingTrivia: Trivia? = nil, ) -> EnumDeclSyntax { var accessModifiers = DeclModifierListSyntax() @@ -109,9 +122,11 @@ public struct ArbitraryMacro: PeerMacro { } return EnumDeclSyntax( + leadingTrivia: leadingTrivia, modifiers: accessModifiers, name: .init(stringLiteral: typeName + String.arbitrary.capitalized), - memberBlock: .init(members: .init(arrayLiteral: .init(decl: arbitraryMethod))) + memberBlock: .init(members: .init(arrayLiteral: .init(decl: arbitraryMethod))), + trailingTrivia: trailingTrivia ) } @@ -172,61 +187,14 @@ public struct ArbitraryMacro: PeerMacro { } } - /// Defines the `Arbitrary` type: `static` or `dynamic`. - private static func defineArbitraryConfig(from node: AttributeSyntax) throws -> ArbitraryConfig { - let strokeType = node.arguments? - .as(LabeledExprListSyntax.self)? - .filter { $0.expression.as(MemberAccessExprSyntax.self) != nil }.first? - .expression - .as(MemberAccessExprSyntax.self)? - .declName - .baseName - .text ?? String.static - - if strokeType == String.static { - return .static - } else if strokeType == String.dynamic { - return .dynamic - } else { - throw ArbitraryMacroError.wrongArbitraryType + static func getReturnClause( + type: TypeSyntax?, + typeName: TokenSyntax + ) -> ReturnClauseSyntax { + guard let type else { + return ReturnClauseSyntax(type: IdentifierTypeSyntax(name: typeName)) } - } - /// Defines generated `Arbitrary` type. - private static func defineArbitraryTypeForDecl(_ declaration: DeclSyntaxProtocol) throws -> ArbitaryType { - if declaration.is(ProtocolDeclSyntax.self) { - return .mock - } else if declaration.is(StructDeclSyntax.self) || - declaration.is(ClassDeclSyntax.self) || - declaration.is(ActorDeclSyntax.self) { - return .model - } else { - throw ArbitraryMacroError.unsupportedType - } - } - - /// Returns the declaration type name. - /// - /// - Parameter decl: the declaration to extract the type name from. - /// - Returns: the syntax of the declaration's type name. - /// - Throws: an error if the macro is applied to an unsupported declaration. - /// - private static func getTypeNameFromDecl(_ decl: DeclGroupSyntax) -> TokenSyntax? { - switch decl.kind { - case .classDecl: - decl.as(ClassDeclSyntax.self)?.name - case .structDecl: - decl.as(StructDeclSyntax.self)?.name - case .extensionDecl: - decl.as(ExtensionDeclSyntax.self)?.extendedType.as(IdentifierTypeSyntax.self)?.name - case .actorDecl: - decl.as(ActorDeclSyntax.self)?.name - case .enumDecl: - decl.as(EnumDeclSyntax.self)?.name - case .protocolDecl: - decl.as(ProtocolDeclSyntax.self)?.name - default: - nil - } + return ReturnClauseSyntax(type: type) } } diff --git a/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacroContext.swift b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacroContext.swift new file mode 100644 index 0000000..8df3308 --- /dev/null +++ b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacroContext.swift @@ -0,0 +1,19 @@ +// +// ArbitraryMacroContext.swift +// TestingMacroCollection +// +// Copyright © 2026 Ozon. All rights reserved. +// + +import SwiftSyntax + +/// Context containing data about the declaration to which the macro is attached. +struct ArbitraryMacroContext { + let arbitraryType: ArbitraryMacro.ArbitaryType + let arbitraryConfig: ArbitraryMacro.ArbitraryConfig + let accessModifier: DeclModifierSyntax + let buildType: BuildType + let typeName: TokenSyntax + let parameters: [ArbitraryParameter] + let declMembers: MemberBlockItemListSyntax +} diff --git a/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacroError.swift b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacroError.swift index f3a0dc5..9714458 100644 --- a/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacroError.swift +++ b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacroError.swift @@ -2,13 +2,15 @@ // ArbitraryMacroError.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // /// Errors for the `Arbitrary` macro. enum ArbitraryMacroError: CustomStringConvertible, Error { case unsupportedType case wrongArbitraryType + case enumHasNoCases + case enumWithStaticArbitraryTypeMustHasDefaultValue var description: String { switch self { @@ -16,6 +18,10 @@ enum ArbitraryMacroError: CustomStringConvertible, Error { "@Arbitrary macro is attached to an unsupported declaration" case .wrongArbitraryType: "ArbitraryType can only be static or dynamic" + case .enumHasNoCases: + "@Arbitrary macro attached to an empty enum" + case .enumWithStaticArbitraryTypeMustHasDefaultValue: + "@Arbitrary(.static) macro attached to an enum without a case marked with @ArbitraryDefaultCase" } } } diff --git a/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacroInputParameters.swift b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacroInputParameters.swift new file mode 100644 index 0000000..b6b9183 --- /dev/null +++ b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryMacroInputParameters.swift @@ -0,0 +1,69 @@ +// +// ArbitraryMacroInputParameters.swift +// TestingMacroCollection +// +// Copyright © 2026 Ozon. All rights reserved. +// + +import SwiftSyntax + +struct ArbitraryMacroInputParameters { + private enum AccessModifier: String { + case auto + case `public` + case `internal` + + var modifierDecl: DeclModifierSyntax? { + switch self { + case .public: + .init(name: .keyword(.public)) + case .internal: + .init(name: .keyword(.internal)) + case .auto: + nil + } + } + } + + var accessModifier: DeclModifierSyntax { + if let am = _accessModifier.modifierDecl { + return am + } + + return declGroup.accessModifier + } + + var arbitraryConfig: ArbitraryMacro.ArbitraryConfig { + _arbitraryConfig + } + + let buildType: BuildType + + private let _arbitraryConfig: ArbitraryMacro.ArbitraryConfig + private let _accessModifier: AccessModifier + private let declGroup: DeclGroupSyntax + + init(node: AttributeSyntax, declGroup: DeclGroupSyntax) { + self.declGroup = declGroup + + let parametersFromMacro = MacroArgumentExtracter.extractArguments(from: node) + + if let arbitraryType = parametersFromMacro[.unlabeledParam] as? String { + _arbitraryConfig = ArbitraryMacro.ArbitraryConfig(rawValue: arbitraryType) ?? .static + } else { + _arbitraryConfig = .static + } + + if let accessModifier = parametersFromMacro[.accessModifier] as? String { + _accessModifier = AccessModifier(rawValue: accessModifier) ?? .auto + } else { + _accessModifier = .auto + } + + if let buildTypeValue = parametersFromMacro[.buildType] as? String { + buildType = BuildType(rawValue: buildTypeValue) ?? .debug + } else { + buildType = .debug + } + } +} diff --git a/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryModelFlow.swift b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryModelFlow.swift index 2513a7b..041c02d 100644 --- a/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryModelFlow.swift +++ b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryModelFlow.swift @@ -2,7 +2,7 @@ // ArbitraryModelFlow.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax @@ -10,6 +10,7 @@ import SwiftSyntaxMacros extension ArbitraryMacro { static func makeArbitraryMethodForModel( + type: TypeSyntax? = nil, accessModifier: DeclModifierSyntax, typeName: TokenSyntax, parameters: [ArbitraryParameter], @@ -23,12 +24,13 @@ extension ArbitraryMacro { partialResult.append(modifier) } - let params = parameters + let parametersToUse = parameters.filter { !$0.isWithAccessor } + let functionArgumentsSyntax = parametersToUse .enumerated() .reduce(into: LabeledExprListSyntax()) { partialResult, item in let offset = item.offset let parameter = item.element - let trailingComma: TokenSyntax? = offset == parameters.count - 1 ? nil : .commaToken() + let trailingComma: TokenSyntax? = offset == parametersToUse.count - 1 ? nil : .commaToken() partialResult.append( LabeledExprSyntax( @@ -41,9 +43,9 @@ extension ArbitraryMacro { } let initObjectSyntax = FunctionCallExprSyntax( - calledExpression: DeclReferenceExprSyntax(baseName: typeName), + calledExpression: DeclReferenceExprSyntax(baseName: getBaseName(type: type, typeName: typeName)), leftParen: .leftParenToken(), - arguments: params, + arguments: functionArgumentsSyntax, rightParen: .rightParenToken() ) let item = CodeBlockItemSyntax.Item(initObjectSyntax) @@ -56,13 +58,24 @@ extension ArbitraryMacro { signature: .init( parameterClause: makeArbitraryMethodSignatureParameterClause( parentTypeName: typeName, - parameters: parameters, + parameters: parametersToUse, arbitararyConfig: arbitraryConfig, declMembers: declMembers ), - returnClause: .init(type: IdentifierTypeSyntax(name: typeName)) + returnClause: getReturnClause(type: type, typeName: typeName) ), body: functionBody ) } + + private static func getBaseName( + type: TypeSyntax?, + typeName: TokenSyntax + ) -> TokenSyntax { + guard let type else { + return typeName + } + + return TokenSyntax(stringLiteral: type.name) + } } diff --git a/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryParameter.swift b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryParameter.swift index 73bf0f0..b0282da 100644 --- a/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryParameter.swift +++ b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryParameter.swift @@ -2,7 +2,7 @@ // ArbitraryParameter.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax @@ -17,4 +17,8 @@ struct ArbitraryParameter { let isIgnored: Bool /// When `true`, initializes the property as `nil`. let isNilable: Bool + /// Whether the default value should be empty, applicable to Collection. + let isEmpted: Bool + /// Whether the property contains an accessor declaration — `{ ... }`. + let isWithAccessor: Bool } diff --git a/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryProtocolFlow.swift b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryProtocolFlow.swift index af1f5c8..835abd9 100644 --- a/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryProtocolFlow.swift +++ b/Sources/OzonTestingMacros/ArbitraryMacro/ArbitraryProtocolFlow.swift @@ -2,7 +2,7 @@ // ArbitraryProtocolFlow.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax @@ -126,7 +126,8 @@ extension ArbitraryMacro { guard !parameter.isIgnored, let defaultValue = makeDefaultValueExprSyntaxForType( parameter.type, parentTypeName: parentTypeName, - arbitraryConfig: arbitraryConfig + arbitraryConfig: arbitraryConfig, + isEmpted: parameter.isEmpted ) else { return nil } @@ -140,12 +141,14 @@ extension ArbitraryMacro { /// - type: input argument type. /// - parentTypeName: name of the parent declaration. /// - arbitraryConfig: the `Arbitrary` type, can be `static` or `dynamic`. + /// - isEmpted: whether the default value should be empty. /// - Returns: default value expression. /// - private static func makeDefaultValueExprSyntaxForType( + static func makeDefaultValueExprSyntaxForType( _ type: TypeSyntax, parentTypeName: TokenSyntax, - arbitraryConfig: ArbitraryConfig + arbitraryConfig: ArbitraryConfig, + isEmpted: Bool = false ) -> ExprSyntaxProtocol? { let variableType = getVariableType(type) @@ -159,20 +162,16 @@ extension ArbitraryMacro { declName: DeclReferenceExprSyntax(baseName: .identifier(arbitraryIdentifier)) ) case .nested(let cleanType): - let typeName: String - let type = cleanType.as(MemberTypeSyntax.self) - let baseTypeName = type?.baseType.as(IdentifierTypeSyntax.self)?.name.text + let fullTypePath = collectFullNestedTypePathWithDots(cleanType) - if let type, let baseTypeName { - typeName = baseTypeName + "." + type.name.text - } else { + guard !fullTypePath.isEmpty else { return nil } return FunctionCallExprSyntax( calledExpression: MemberAccessExprSyntax( base: DeclReferenceExprSyntax( - baseName: .identifier(typeName + String.arbitrary.capitalized) + baseName: .identifier(fullTypePath + String.arbitrary.capitalized) ), declName: .init(baseName: .identifier(String.arbitrary)) ), @@ -239,9 +238,26 @@ extension ArbitraryMacro { parentTypeName: parentTypeName, arbitraryConfig: arbitraryConfig ) - case .array, - .set: - return ArrayExprSyntax(leftSquare: .leftSquareToken(), elements: .init([]), rightSquare: .rightSquareToken()) + case .array, .set: + guard !isEmpted else { + return ArrayExprSyntax(leftSquare: .leftSquareToken(), elements: .init([]), rightSquare: .rightSquareToken()) + } + + let arbitraryIdentifier = "\(String.arbitrary)" + + (typeCanBeStaticOrDynamic(type) ? "(.\(arbitraryConfig.rawValue))" : "()") + let expr = ExprSyntax( + MemberAccessExprSyntax( + period: .periodToken(), + declName: DeclReferenceExprSyntax(baseName: .identifier(arbitraryIdentifier)) + ) + ) + return ArrayExprSyntax( + leftSquare: .leftSquareToken(), + elements: .init(expressions: [ + expr, + ]), + rightSquare: .rightSquareToken() + ) case .tuple: guard let typesInTuple = type.as(TupleTypeSyntax.self)?.elements else { return nil } @@ -456,4 +472,20 @@ extension ArbitraryMacro { } } } + + private static func collectFullNestedTypePathWithDots(_ type: TypeSyntax) -> String { + var components: [String] = [] + + var currentType: TypeSyntax? = type + while let memberType = currentType?.as(MemberTypeSyntax.self) { + components.append(memberType.name.text) + currentType = memberType.baseType + } + + if let identifierType = currentType?.as(IdentifierTypeSyntax.self) { + components.append(identifierType.name.text) + } + + return components.reversed().joined(separator: ".") + } } diff --git a/Sources/OzonTestingMacros/ArbitraryMacro/MemberArbitraryMacro.swift b/Sources/OzonTestingMacros/ArbitraryMacro/MemberArbitraryMacro.swift index 14f40f1..1b3b640 100644 --- a/Sources/OzonTestingMacros/ArbitraryMacro/MemberArbitraryMacro.swift +++ b/Sources/OzonTestingMacros/ArbitraryMacro/MemberArbitraryMacro.swift @@ -2,7 +2,7 @@ // MemberArbitraryMacro.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax @@ -24,20 +24,38 @@ extension ArbitraryMacro: MemberMacro { conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext ) throws -> [DeclSyntax] { - guard !declaration.is(ProtocolDeclSyntax.self), !declaration.variables.isEmpty else { return [] } + guard !declaration.is(ProtocolDeclSyntax.self), + !declaration.is(EnumDeclSyntax.self), + !declaration.variables.isEmpty else { return [] } - let initializerType = defineInitializerType(declaration: declaration) + guard let declGroup = declaration.asProtocol(DeclGroupSyntax.self), + let typeName = getTypeNameFromDecl(declGroup) else { + return [] + } + + let context = try makeContext( + node: node, + declaration: declaration, + declGroup: declaration, + typeName: typeName + ) + + let initializerType = defineInitializerType( + declaration: declaration, + accessModifier: context.accessModifier + ) guard initializerType == .generated else { return [] } - let accessModifier = declaration.modifiers - .filter { $0.name.text == String.public }.isEmpty ? nil : DeclModifierSyntax(name: .keyword(.public)) let variables = declaration.variables.reduce(into: [(TokenSyntax, TypeSyntax)]()) { partialResult, variable in guard let name = variable.name?.identifier, let type = variable.type?.type else { return } partialResult.append((name, type)) } - let generatedInit = makeInit(variables: variables, accessModifier: accessModifier) + let generatedInit = makeInit( + variables: variables, + accessModifier: context.accessModifier.isInternal ? nil : context.accessModifier + ) return [.init(generatedInit)] } @@ -120,13 +138,18 @@ extension ArbitraryMacro: MemberMacro { /// Determines the initializer type to use. /// - /// - Parameter declaration: the declaration to analyze for initializer requirements. - /// - Returns: the appropriate initializer type. + /// - Parameters: + // - declaration: the declaration to analyze for initializer requirements. + /// - accessModifier: access modifier for the `arbitrary` method. + /// - Returns: the appropriate initializer type. /// - private static func defineInitializerType(declaration: DeclGroupSyntax) -> InitializerType { + private static func defineInitializerType( + declaration: DeclGroupSyntax, + accessModifier: DeclModifierSyntax + ) -> InitializerType { guard declaration.initializers.isEmpty else { return .base } - if declaration.is(StructDeclSyntax.self), !declaration.accessModifier.isPublic { + if declaration.is(StructDeclSyntax.self), !accessModifier.isPublic { return .base } else { return .generated diff --git a/Sources/OzonTestingMacros/AutoEquatableMacro.swift b/Sources/OzonTestingMacros/AutoEquatableMacro.swift index 4208e3f..6db34c4 100644 --- a/Sources/OzonTestingMacros/AutoEquatableMacro.swift +++ b/Sources/OzonTestingMacros/AutoEquatableMacro.swift @@ -2,7 +2,7 @@ // AutoEquatableMacro.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation @@ -283,7 +283,11 @@ public struct AutoEquatableMacro: ExtensionMacro { var caseList = cases .map { makeEnumCase($0, enumHasVariables: enumHasVariables) } .reduce(into: SwitchCaseListSyntax()) { $0.append(.switchCase($1)) } - caseList.append(.switchCase(makeDefaultCase())) + + // The switch generated above covers all cases for an enum with a single case. + if cases.count > 1 { + caseList.append(.switchCase(makeDefaultCase())) + } return caseList } diff --git a/Sources/OzonTestingMacros/Builders/ClearFunctionBlockBuilder.swift b/Sources/OzonTestingMacros/Builders/ClearFunctionBlockBuilder.swift index f0c0270..69723ef 100644 --- a/Sources/OzonTestingMacros/Builders/ClearFunctionBlockBuilder.swift +++ b/Sources/OzonTestingMacros/Builders/ClearFunctionBlockBuilder.swift @@ -2,7 +2,7 @@ // ClearFunctionBlockBuilder.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax @@ -25,8 +25,7 @@ final class ClearMethodBuilder { var propertyName: String { switch self { - case .collection(let propertyName), - .nilable(let propertyName): + case .collection(let propertyName), .nilable(let propertyName): propertyName } } diff --git a/Sources/OzonTestingMacros/Builders/DeinitBlockBuilder.swift b/Sources/OzonTestingMacros/Builders/DeinitBlockBuilder.swift index 607a883..2b4226f 100644 --- a/Sources/OzonTestingMacros/Builders/DeinitBlockBuilder.swift +++ b/Sources/OzonTestingMacros/Builders/DeinitBlockBuilder.swift @@ -2,7 +2,7 @@ // DeinitBlockBuilder.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/Builders/FunctionsBlockBuilder.swift b/Sources/OzonTestingMacros/Builders/FunctionsBlockBuilder.swift index 4b21af2..8f38e16 100644 --- a/Sources/OzonTestingMacros/Builders/FunctionsBlockBuilder.swift +++ b/Sources/OzonTestingMacros/Builders/FunctionsBlockBuilder.swift @@ -2,7 +2,7 @@ // FunctionsBlockBuilder.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax @@ -127,6 +127,7 @@ enum FunctionsBlockBuilder { partialResult.appending(parameter) } let isFuncThrowable = function.signature.effectSpecifiers?.throwsClause?.throwsSpecifier != nil + let errorType = function.signature.effectSpecifiers?.throwsClause?.type?.as(IdentifierTypeSyntax.self) let isAsyncFunc = function.signature.effectSpecifiers?.asyncSpecifier != nil let funcModifiers: DeclModifierListSyntax = if !functionsModifiers.isEmpty { @@ -182,6 +183,7 @@ enum FunctionsBlockBuilder { let errorProperty = VariablesFactory.makeErrorProperty( clearMethodBuilder: clearMethodBuilder, varName: propertiesShortName, + errorType: errorType, accessModifiers: _accessModifiers ) result.append(.init(decl: errorProperty)) @@ -191,7 +193,7 @@ enum FunctionsBlockBuilder { for: .error, propertyName: propertiesShortName, accessModifiers: accessModifiers, - type: TypeSyntax(stringLiteral: .error) + type: errorType ?? TypeSyntax(stringLiteral: .error) ) setPropertyFunctions.append(setErrorMethod) } @@ -213,6 +215,7 @@ enum FunctionsBlockBuilder { genericTypes: genericTypes, accessModifiers: closureAccessModifiers, isThrowable: isFuncThrowable, + errorType: errorType, isAsync: isAsyncFunc ) @@ -413,7 +416,11 @@ enum FunctionsBlockBuilder { } let codeBlockSyntax = CodeBlockSyntax(statements: statements) + let functionAttributes = function.attributes + .filter { ALLOWED_ATTRIBUTES.contains($0.as(AttributeSyntax.self)?.attributeName.name ?? "") } + return FunctionDeclSyntax( + attributes: functionAttributes, modifiers: accessModifiers, name: .identifier(function.name.text), genericParameterClause: genericParameterClause, @@ -481,11 +488,12 @@ enum FunctionsBlockBuilder { static func makeErrorProperty( clearMethodBuilder: ClearMethodBuilder?, varName: String, + errorType: IdentifierTypeSyntax?, accessModifiers: DeclModifierListSyntax ) -> VariableDeclSyntax { let name = varName + .error clearMethodBuilder?.addProperty(.nilable(propertyName: name)) - let type = OptionalTypeSyntax(wrappedType: IdentifierTypeSyntax(name: .identifier(.error))) + let type = OptionalTypeSyntax(wrappedType: errorType ?? IdentifierTypeSyntax(name: .identifier(.error))) return VariableDeclSyntax( modifiers: accessModifiers, Keyword.var, @@ -538,6 +546,7 @@ enum FunctionsBlockBuilder { genericTypes: [String], accessModifiers: DeclModifierListSyntax, isThrowable: Bool, + errorType: IdentifierTypeSyntax?, isAsync: Bool ) -> VariableDeclSyntax { let name = varName + .closure @@ -560,10 +569,16 @@ enum FunctionsBlockBuilder { ) ) } - let effectSpecifiers = TypeEffectSpecifiersSyntax( - asyncSpecifier: isAsync ? .keyword(.async) : nil, - throwsClause: isThrowable ? .init(throwsSpecifier: .keyword(.throws)) : nil - ) + var effectSpecifiers = TypeEffectSpecifiersSyntax(asyncSpecifier: isAsync ? .keyword(.async) : nil, throwsClause: nil) + + if isThrowable { + effectSpecifiers.throwsClause = .init(throwsSpecifier: .keyword(.throws), type: errorType) + if errorType != nil { + effectSpecifiers.throwsClause?.leftParen = .leftParenToken() + effectSpecifiers.throwsClause?.rightParen = .rightParenToken() + } + } + let functionTypeSyntax = FunctionTypeSyntax( parameters: .init(arguments), effectSpecifiers: effectSpecifiers, diff --git a/Sources/OzonTestingMacros/Builders/InitializerBlockBuilder.swift b/Sources/OzonTestingMacros/Builders/InitializerBlockBuilder.swift index 75a753f..7c2aaf2 100644 --- a/Sources/OzonTestingMacros/Builders/InitializerBlockBuilder.swift +++ b/Sources/OzonTestingMacros/Builders/InitializerBlockBuilder.swift @@ -2,7 +2,7 @@ // InitializerBlockBuilder.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/Builders/SetPropertyFunctionBuilder.swift b/Sources/OzonTestingMacros/Builders/SetPropertyFunctionBuilder.swift index 2984d02..dc2b76f 100644 --- a/Sources/OzonTestingMacros/Builders/SetPropertyFunctionBuilder.swift +++ b/Sources/OzonTestingMacros/Builders/SetPropertyFunctionBuilder.swift @@ -2,7 +2,7 @@ // SetPropertyFunctionBuilder.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/Builders/TypealiasesBlockBuilder.swift b/Sources/OzonTestingMacros/Builders/TypealiasesBlockBuilder.swift index 0ed17b6..cb0a66a 100644 --- a/Sources/OzonTestingMacros/Builders/TypealiasesBlockBuilder.swift +++ b/Sources/OzonTestingMacros/Builders/TypealiasesBlockBuilder.swift @@ -2,7 +2,7 @@ // TypealiasesBlockBuilder.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation diff --git a/Sources/OzonTestingMacros/Builders/VariablesBlockBuilder.swift b/Sources/OzonTestingMacros/Builders/VariablesBlockBuilder.swift index b80c73f..36f7424 100644 --- a/Sources/OzonTestingMacros/Builders/VariablesBlockBuilder.swift +++ b/Sources/OzonTestingMacros/Builders/VariablesBlockBuilder.swift @@ -2,7 +2,7 @@ // VariablesBlockBuilder.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax @@ -54,12 +54,15 @@ enum VariablesBlockBuilder { result.append( .init(decl: MissingDeclSyntax(placeholder: .init(stringLiteral: "\n // MARK: - \(name)\n"))) ) + let varAttributes = variable.attributes + .filter { ALLOWED_ATTRIBUTES.contains($0.as(AttributeSyntax.self)?.attributeName.name ?? "") } if isNonOptionalType { let mockVar = MemberBlockItemSyntax(decl: makeMockVarGetterSetter( name: name, type: type, - accessModifiers: accessModifiers + accessModifiers: accessModifiers, + attributes: varAttributes )) result.append(mockVar) } else { @@ -77,6 +80,7 @@ enum VariablesBlockBuilder { } let mockVar = VariableDeclSyntax( + attributes: varAttributes, modifiers: _accessModifiers, Keyword.var, name: .init(name), @@ -138,7 +142,8 @@ enum VariablesBlockBuilder { private static func makeMockVarGetterSetter( name: IdentifierPatternSyntax, type: TypeAnnotationSyntax, - accessModifiers: DeclModifierListSyntax + accessModifiers: DeclModifierListSyntax, + attributes: AttributeListSyntax ) -> VariableDeclSyntax { let accessorBlock = AccessorBlockSyntax( accessors: .accessors( @@ -156,6 +161,7 @@ enum VariablesBlockBuilder { ) let newVar = VariableDeclSyntax( + attributes: attributes, modifiers: accessModifiers, bindingSpecifier: .keyword(Keyword.var), bindings: .init([binding]) diff --git a/Sources/OzonTestingMacros/CoreExtensions/ArrayTypeSyntax+Ext.swift b/Sources/OzonTestingMacros/CoreExtensions/ArrayTypeSyntax+Ext.swift index cfe7c35..c378467 100644 --- a/Sources/OzonTestingMacros/CoreExtensions/ArrayTypeSyntax+Ext.swift +++ b/Sources/OzonTestingMacros/CoreExtensions/ArrayTypeSyntax+Ext.swift @@ -2,7 +2,7 @@ // ArrayTypeSyntax+Ext.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/CoreExtensions/ConstantStrings.swift b/Sources/OzonTestingMacros/CoreExtensions/ConstantStrings.swift index 806ec49..859487e 100644 --- a/Sources/OzonTestingMacros/CoreExtensions/ConstantStrings.swift +++ b/Sources/OzonTestingMacros/CoreExtensions/ConstantStrings.swift @@ -2,7 +2,7 @@ // ConstantStrings.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax @@ -68,12 +68,19 @@ extension String { static let auto = "auto" static let unchecked = "unchecked" static let defaultValue = "defaultValue" + static let buildType = "buildType" static let unlabeledParam = "unlabeled_param" static let mockAccessor = "MockAccessor" static let proxyableMock = "ProxyableMock" static let functionBodyMock = "FunctionBodyMock" + static let arbitraryDefaultCaseMacro = "ArbitraryDefaultCase" + static let arbitraryType = "arbitraryType" + static let accessModifier = "accessModifier" + static let randomElementFunctionName = "randomElement" + + static let empted = "Empted" static let underscore = "_" static let empty = "" } diff --git a/Sources/OzonTestingMacros/CoreExtensions/DeclGroupSyntax+Ext.swift b/Sources/OzonTestingMacros/CoreExtensions/DeclGroupSyntax+Ext.swift index 2f130ec..2072ffa 100644 --- a/Sources/OzonTestingMacros/CoreExtensions/DeclGroupSyntax+Ext.swift +++ b/Sources/OzonTestingMacros/CoreExtensions/DeclGroupSyntax+Ext.swift @@ -2,7 +2,7 @@ // DeclGroupSyntax+Ext.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/CoreExtensions/DeclModifierSyntax+Ext.swift b/Sources/OzonTestingMacros/CoreExtensions/DeclModifierSyntax+Ext.swift index be5865b..f41adea 100644 --- a/Sources/OzonTestingMacros/CoreExtensions/DeclModifierSyntax+Ext.swift +++ b/Sources/OzonTestingMacros/CoreExtensions/DeclModifierSyntax+Ext.swift @@ -2,7 +2,7 @@ // DeclModifierSyntax+Ext.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/CoreExtensions/DeclReferenceExprSyntax+Ext.swift b/Sources/OzonTestingMacros/CoreExtensions/DeclReferenceExprSyntax+Ext.swift index ab81539..a5c3372 100644 --- a/Sources/OzonTestingMacros/CoreExtensions/DeclReferenceExprSyntax+Ext.swift +++ b/Sources/OzonTestingMacros/CoreExtensions/DeclReferenceExprSyntax+Ext.swift @@ -2,7 +2,7 @@ // DeclReferenceExprSyntax+Ext.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/CoreExtensions/DeclSyntax+Ext.swift b/Sources/OzonTestingMacros/CoreExtensions/DeclSyntax+Ext.swift index f26dfb4..f331c6d 100644 --- a/Sources/OzonTestingMacros/CoreExtensions/DeclSyntax+Ext.swift +++ b/Sources/OzonTestingMacros/CoreExtensions/DeclSyntax+Ext.swift @@ -2,7 +2,7 @@ // DeclSyntax+Ext.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/CoreExtensions/DictionaryTypeSyntax+Ext.swift b/Sources/OzonTestingMacros/CoreExtensions/DictionaryTypeSyntax+Ext.swift index 3149051..6726449 100644 --- a/Sources/OzonTestingMacros/CoreExtensions/DictionaryTypeSyntax+Ext.swift +++ b/Sources/OzonTestingMacros/CoreExtensions/DictionaryTypeSyntax+Ext.swift @@ -2,7 +2,7 @@ // DictionaryTypeSyntax+Ext.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/CoreExtensions/ExprSyntaxProtocol+Expr.swift b/Sources/OzonTestingMacros/CoreExtensions/ExprSyntaxProtocol+Expr.swift index 274787f..61c4d57 100644 --- a/Sources/OzonTestingMacros/CoreExtensions/ExprSyntaxProtocol+Expr.swift +++ b/Sources/OzonTestingMacros/CoreExtensions/ExprSyntaxProtocol+Expr.swift @@ -2,7 +2,7 @@ // ExprSyntaxProtocol+Expr.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation diff --git a/Sources/OzonTestingMacros/CoreExtensions/IdentifierTypeSyntax+Ext.swift b/Sources/OzonTestingMacros/CoreExtensions/IdentifierTypeSyntax+Ext.swift index 4f0188c..895b8d4 100644 --- a/Sources/OzonTestingMacros/CoreExtensions/IdentifierTypeSyntax+Ext.swift +++ b/Sources/OzonTestingMacros/CoreExtensions/IdentifierTypeSyntax+Ext.swift @@ -2,7 +2,7 @@ // IdentifierTypeSyntax+Ext.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/CoreExtensions/InitializerClauseSyntax+Ext.swift b/Sources/OzonTestingMacros/CoreExtensions/InitializerClauseSyntax+Ext.swift index 0d960e8..a47a3d0 100644 --- a/Sources/OzonTestingMacros/CoreExtensions/InitializerClauseSyntax+Ext.swift +++ b/Sources/OzonTestingMacros/CoreExtensions/InitializerClauseSyntax+Ext.swift @@ -2,7 +2,7 @@ // InitializerClauseSyntax+Ext.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/CoreExtensions/InitializerDeclSyntax+Ext.swift b/Sources/OzonTestingMacros/CoreExtensions/InitializerDeclSyntax+Ext.swift index db4114e..d0e1aa6 100644 --- a/Sources/OzonTestingMacros/CoreExtensions/InitializerDeclSyntax+Ext.swift +++ b/Sources/OzonTestingMacros/CoreExtensions/InitializerDeclSyntax+Ext.swift @@ -2,7 +2,7 @@ // InitializerDeclSyntax+Ext.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/CoreExtensions/ProtocolDeclSyntax+Ext.swift b/Sources/OzonTestingMacros/CoreExtensions/ProtocolDeclSyntax+Ext.swift index f531702..070f31a 100644 --- a/Sources/OzonTestingMacros/CoreExtensions/ProtocolDeclSyntax+Ext.swift +++ b/Sources/OzonTestingMacros/CoreExtensions/ProtocolDeclSyntax+Ext.swift @@ -2,7 +2,7 @@ // ProtocolDeclSyntax+Ext.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/CoreExtensions/StructDeclSyntax+Ext.swift b/Sources/OzonTestingMacros/CoreExtensions/StructDeclSyntax+Ext.swift index 8330faa..dc56c20 100644 --- a/Sources/OzonTestingMacros/CoreExtensions/StructDeclSyntax+Ext.swift +++ b/Sources/OzonTestingMacros/CoreExtensions/StructDeclSyntax+Ext.swift @@ -2,7 +2,7 @@ // StructDeclSyntax+Ext.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/CoreExtensions/TupleTypeSyntax+Ext.swift b/Sources/OzonTestingMacros/CoreExtensions/TupleTypeSyntax+Ext.swift index 7cb6feb..593e5fb 100644 --- a/Sources/OzonTestingMacros/CoreExtensions/TupleTypeSyntax+Ext.swift +++ b/Sources/OzonTestingMacros/CoreExtensions/TupleTypeSyntax+Ext.swift @@ -2,7 +2,7 @@ // TupleTypeSyntax+Ext.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/CoreExtensions/TypeSyntax+Ext.swift b/Sources/OzonTestingMacros/CoreExtensions/TypeSyntax+Ext.swift index ee5dc48..3a33933 100644 --- a/Sources/OzonTestingMacros/CoreExtensions/TypeSyntax+Ext.swift +++ b/Sources/OzonTestingMacros/CoreExtensions/TypeSyntax+Ext.swift @@ -2,7 +2,7 @@ // TypeSyntax+Ext.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/CoreExtensions/VariableDeclSyntax+Ext.swift b/Sources/OzonTestingMacros/CoreExtensions/VariableDeclSyntax+Ext.swift index 0b61ac6..baf090c 100644 --- a/Sources/OzonTestingMacros/CoreExtensions/VariableDeclSyntax+Ext.swift +++ b/Sources/OzonTestingMacros/CoreExtensions/VariableDeclSyntax+Ext.swift @@ -2,7 +2,7 @@ // VariableDeclSyntax+Ext.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax @@ -43,4 +43,14 @@ extension VariableDeclSyntax { .text == String.nilable }) != nil } + + var isEmpted: Bool { + attributes.first(where: { + $0.as(AttributeSyntax.self)? + .attributeName + .as(IdentifierTypeSyntax.self)? + .name + .text == String.empted + }) != nil + } } diff --git a/Sources/OzonTestingMacros/EmptedMacro.swift b/Sources/OzonTestingMacros/EmptedMacro.swift new file mode 100644 index 0000000..4843651 --- /dev/null +++ b/Sources/OzonTestingMacros/EmptedMacro.swift @@ -0,0 +1,41 @@ +// +// EmptedMacro.swift +// TestingMacroCollection +// +// Copyright © 2026 Ozon. All rights reserved. +// + +import Foundation +import SwiftSyntax +import SwiftSyntaxMacros + +// MARK: - EmptedError + +enum EmptedError: CustomStringConvertible, Error { + case wrongDeclarationPinning + + var description: String { + switch self { + case .wrongDeclarationPinning: + "`@Empted` can only be attached to Array, Set" + } + } +} + +// MARK: - EmptedMacro + +public struct EmptedMacro: PeerMacro { + public static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + guard let varDecl = declaration.as(VariableDeclSyntax.self), + varDecl.type?.type.is(ArrayTypeSyntax.self) == true || + varDecl.type?.type.as(IdentifierTypeSyntax.self)?.name.text == String.set else { + throw EmptedError.wrongDeclarationPinning + } + + return [] + } +} diff --git a/Sources/OzonTestingMacros/FunctionBodyMockMacro.swift b/Sources/OzonTestingMacros/FunctionBodyMockMacro.swift index 40d3a12..99414e8 100644 --- a/Sources/OzonTestingMacros/FunctionBodyMockMacro.swift +++ b/Sources/OzonTestingMacros/FunctionBodyMockMacro.swift @@ -2,7 +2,7 @@ // FunctionBodyMockMacro.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation @@ -83,7 +83,7 @@ public struct FunctionBodyMockMacro: BodyMacro { let secondName = parameter.secondName let comma: TokenSyntax? = index == parameters.count - 1 ? nil : .commaToken() - var mockFunctionCallExpr = if let secondName { + let mockFunctionCallExpr = if let secondName { if firstName.text == "_" { LabeledExprSyntax( expression: DeclReferenceExprSyntax(baseName: secondName), diff --git a/Sources/OzonTestingMacros/Helpers/AllowedAttributes.swift b/Sources/OzonTestingMacros/Helpers/AllowedAttributes.swift new file mode 100644 index 0000000..323f23b --- /dev/null +++ b/Sources/OzonTestingMacros/Helpers/AllowedAttributes.swift @@ -0,0 +1,8 @@ +// +// AllowedAttributes.swift +// TestingMacroCollection +// +// Copyright © 2026 Ozon. All rights reserved. +// + +let ALLOWED_ATTRIBUTES = ["available"] diff --git a/Sources/OzonTestingMacros/Helpers/BuildType.swift b/Sources/OzonTestingMacros/Helpers/BuildType.swift new file mode 100644 index 0000000..b66f654 --- /dev/null +++ b/Sources/OzonTestingMacros/Helpers/BuildType.swift @@ -0,0 +1,14 @@ +// +// BuildType.swift +// TestingMacroCollection +// +// Copyright © 2026 Ozon. All rights reserved. +// + +import Foundation + +/// The build type in which the macro generation is running. +enum BuildType: String { + case debug + case prod +} diff --git a/Sources/OzonTestingMacros/Helpers/ForceUnwrapped.swift b/Sources/OzonTestingMacros/Helpers/ForceUnwrapped.swift index 732b112..41096c9 100644 --- a/Sources/OzonTestingMacros/Helpers/ForceUnwrapped.swift +++ b/Sources/OzonTestingMacros/Helpers/ForceUnwrapped.swift @@ -2,7 +2,7 @@ // ForceUnwrapped.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/Helpers/IdentifierPatternSyntax+Ext.swift b/Sources/OzonTestingMacros/Helpers/IdentifierPatternSyntax+Ext.swift index 27618db..6edb124 100644 --- a/Sources/OzonTestingMacros/Helpers/IdentifierPatternSyntax+Ext.swift +++ b/Sources/OzonTestingMacros/Helpers/IdentifierPatternSyntax+Ext.swift @@ -2,7 +2,7 @@ // IdentifierPatternSyntax+Ext.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/Helpers/InheritedTypeListSyntax+Ext.swift b/Sources/OzonTestingMacros/Helpers/InheritedTypeListSyntax+Ext.swift index a3c8fc5..33ce199 100644 --- a/Sources/OzonTestingMacros/Helpers/InheritedTypeListSyntax+Ext.swift +++ b/Sources/OzonTestingMacros/Helpers/InheritedTypeListSyntax+Ext.swift @@ -2,7 +2,7 @@ // InheritedTypeListSyntax+Ext.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/Helpers/MacroArgumentExtracter.swift b/Sources/OzonTestingMacros/Helpers/MacroArgumentExtracter.swift index c4a4e71..cec48bb 100644 --- a/Sources/OzonTestingMacros/Helpers/MacroArgumentExtracter.swift +++ b/Sources/OzonTestingMacros/Helpers/MacroArgumentExtracter.swift @@ -2,7 +2,7 @@ // MacroArgumentExtracter.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax diff --git a/Sources/OzonTestingMacros/Helpers/String+Capitalized.swift b/Sources/OzonTestingMacros/Helpers/String+Capitalized.swift index 1a54e7c..765bc8c 100644 --- a/Sources/OzonTestingMacros/Helpers/String+Capitalized.swift +++ b/Sources/OzonTestingMacros/Helpers/String+Capitalized.swift @@ -2,7 +2,7 @@ // String+Capitalized.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation diff --git a/Sources/OzonTestingMacros/Helpers/SyntaxAnalizer.swift b/Sources/OzonTestingMacros/Helpers/SyntaxAnalizer.swift index 722e41a..ee37ff2 100644 --- a/Sources/OzonTestingMacros/Helpers/SyntaxAnalizer.swift +++ b/Sources/OzonTestingMacros/Helpers/SyntaxAnalizer.swift @@ -2,7 +2,7 @@ // SyntaxAnalizer.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax @@ -36,5 +36,10 @@ extension SyntaxAnalizer { } } +// MARK: - SyntaxChildren.Element + SyntaxAnalizer + extension SyntaxChildren.Element: SyntaxAnalizer {} + +// MARK: - TypeSyntax + SyntaxAnalizer + extension TypeSyntax: SyntaxAnalizer {} diff --git a/Sources/OzonTestingMacros/Helpers/Trivia+Ext.swift b/Sources/OzonTestingMacros/Helpers/Trivia+Ext.swift new file mode 100644 index 0000000..d086589 --- /dev/null +++ b/Sources/OzonTestingMacros/Helpers/Trivia+Ext.swift @@ -0,0 +1,22 @@ +// +// Trivia+Ext.swift +// TestingMacroCollection +// +// Copyright © 2026 Ozon. All rights reserved. +// + +import SwiftSyntax + +extension Trivia { + static var ifDebug: Trivia { + [.unexpectedText("#if DEBUG\n")] + } + + static var endif: Trivia { + [.unexpectedText("\n#endif")] + } + + func ifNeeded(_ flag: Bool) -> Trivia? { + flag ? self : nil + } +} diff --git a/Sources/OzonTestingMacros/Helpers/Type.swift b/Sources/OzonTestingMacros/Helpers/Type.swift index 2db64ee..9770956 100644 --- a/Sources/OzonTestingMacros/Helpers/Type.swift +++ b/Sources/OzonTestingMacros/Helpers/Type.swift @@ -2,7 +2,7 @@ // Type.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // enum Type: String, CaseIterable { diff --git a/Sources/OzonTestingMacros/IgnoredMacro.swift b/Sources/OzonTestingMacros/IgnoredMacro.swift index a49643e..26364e0 100644 --- a/Sources/OzonTestingMacros/IgnoredMacro.swift +++ b/Sources/OzonTestingMacros/IgnoredMacro.swift @@ -2,7 +2,7 @@ // IgnoredMacro.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation diff --git a/Sources/OzonTestingMacros/MockAccessorMacro.swift b/Sources/OzonTestingMacros/MockAccessorMacro.swift index 207e847..1fe1c74 100644 --- a/Sources/OzonTestingMacros/MockAccessorMacro.swift +++ b/Sources/OzonTestingMacros/MockAccessorMacro.swift @@ -2,7 +2,7 @@ // MockAccessorMacro.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation diff --git a/Sources/OzonTestingMacros/MockMacro/MockMacro.swift b/Sources/OzonTestingMacros/MockMacro/MockMacro.swift index bec8e8f..b4887d1 100644 --- a/Sources/OzonTestingMacros/MockMacro/MockMacro.swift +++ b/Sources/OzonTestingMacros/MockMacro/MockMacro.swift @@ -2,7 +2,7 @@ // MockMacro.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation @@ -195,25 +195,31 @@ public struct MockMacro: PeerMacro { // Collects a syntax of the macro body. let memberBlock = MemberBlockSyntax(members: .init(members)) + let leadingTrivia = Trivia.ifDebug.ifNeeded(inputParameters.buildType == .debug) + let trailingTrivia = Trivia.endif.ifNeeded(inputParameters.buildType == .debug) return if isActor { // Generates an actor's declaration for the mock. [ .init(ActorDeclSyntax( + leadingTrivia: leadingTrivia, modifiers: accessModifiers, name: newTypeName, inheritanceClause: inheritedClause, - memberBlock: memberBlock + memberBlock: memberBlock, + trailingTrivia: trailingTrivia )), ] } else { // Generates a class declaration for mock. [ .init(ClassDeclSyntax( + leadingTrivia: leadingTrivia, modifiers: accessModifiers, name: newTypeName, inheritanceClause: inheritedClause, - memberBlock: memberBlock + memberBlock: memberBlock, + trailingTrivia: trailingTrivia )), ] } diff --git a/Sources/OzonTestingMacros/MockMacro/MockMacroInputParameters.swift b/Sources/OzonTestingMacros/MockMacro/MockMacroInputParameters.swift index 5e26613..fe09e66 100644 --- a/Sources/OzonTestingMacros/MockMacro/MockMacroInputParameters.swift +++ b/Sources/OzonTestingMacros/MockMacro/MockMacroInputParameters.swift @@ -2,7 +2,7 @@ // MockMacroInputParameters.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax @@ -72,4 +72,6 @@ struct MockMacroInputParameters { let sendableMode: SendableMode /// Default value generation for standard or collection types. let defaultValues: DefaultValue + /// Generation of the `#if debug` expression, for the .debug value. + let buildType: BuildType } diff --git a/Sources/OzonTestingMacros/MockMacro/MockMacroInputParametersHandler.swift b/Sources/OzonTestingMacros/MockMacro/MockMacroInputParametersHandler.swift index 5ec8910..ebf5990 100644 --- a/Sources/OzonTestingMacros/MockMacro/MockMacroInputParametersHandler.swift +++ b/Sources/OzonTestingMacros/MockMacro/MockMacroInputParametersHandler.swift @@ -2,7 +2,7 @@ // MockMacroInputParametersHandler.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftSyntax @@ -20,6 +20,7 @@ enum MockMacroInputParametersHandler { var accessModifier = AccessModifier.internal var sendableMode = SendableMode.auto var defaultValue = DefaultValue.static + var buildType = BuildType.debug let parametersFromMacro = MacroArgumentExtracter.extractArguments(from: node) @@ -43,12 +44,17 @@ enum MockMacroInputParametersHandler { defaultValue = DefaultValue(rawValue: defaultValueValue) ?? .static } + if let buildTypeValue = parametersFromMacro[.buildType] as? String { + buildType = BuildType(rawValue: buildTypeValue) ?? .debug + } + return .init( associatedTypes: associatedTypes, accessModifier: accessModifier, heritability: heritability, sendableMode: sendableMode, - defaultValues: defaultValue + defaultValues: defaultValue, + buildType: buildType ) } diff --git a/Sources/OzonTestingMacros/NilableMacro.swift b/Sources/OzonTestingMacros/NilableMacro.swift index 9cf3112..53c9352 100644 --- a/Sources/OzonTestingMacros/NilableMacro.swift +++ b/Sources/OzonTestingMacros/NilableMacro.swift @@ -2,7 +2,7 @@ // NilableMacro.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation diff --git a/Sources/OzonTestingMacros/PerformanceMeasureMacro.swift b/Sources/OzonTestingMacros/PerformanceMeasureMacro.swift index fcefe7f..43d4df5 100644 --- a/Sources/OzonTestingMacros/PerformanceMeasureMacro.swift +++ b/Sources/OzonTestingMacros/PerformanceMeasureMacro.swift @@ -2,7 +2,7 @@ // PerformanceMeasureMacro.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation diff --git a/Sources/OzonTestingMacros/Plugin/TestingMacroCollectionPlugin.swift b/Sources/OzonTestingMacros/Plugin/TestingMacroCollectionPlugin.swift index 89a0e05..98f120c 100644 --- a/Sources/OzonTestingMacros/Plugin/TestingMacroCollectionPlugin.swift +++ b/Sources/OzonTestingMacros/Plugin/TestingMacroCollectionPlugin.swift @@ -2,7 +2,7 @@ // TestingMacroCollectionPlugin.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation @@ -21,6 +21,8 @@ struct TestingMacroCollectionPlugin: CompilerPlugin { FunctionBodyMockMacro.self, IgnoredMacro.self, NilableMacro.self, + EmptedMacro.self, ArbitraryMacro.self, + ArbitraryDefaultCaseMacro.self, ] } diff --git a/Sources/OzonTestingMacros/Rewriters/GenericTypeRewriter.swift b/Sources/OzonTestingMacros/Rewriters/GenericTypeRewriter.swift index d17a720..dc638fc 100644 --- a/Sources/OzonTestingMacros/Rewriters/GenericTypeRewriter.swift +++ b/Sources/OzonTestingMacros/Rewriters/GenericTypeRewriter.swift @@ -2,7 +2,7 @@ // GenericTypeRewriter.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation @@ -23,6 +23,28 @@ final class GenericTypeRewriter: SyntaxRewriter { return TypeSyntax(anyToken) } + if let genericClause = token.genericArgumentClause { + let newArguments = genericClause.arguments.map { argument in + let newType = visit(argument.argument) + return GenericArgumentSyntax( + argument: newType, + trailingComma: argument.trailingComma + ) + } + + let newClause = GenericArgumentClauseSyntax( + leftAngle: genericClause.leftAngle, + arguments: GenericArgumentListSyntax(newArguments), + rightAngle: genericClause.rightAngle + ) + + return TypeSyntax( + token + .with(\.name, token.name) + .with(\.genericArgumentClause, newClause) + ) + } + return TypeSyntax(token) } } diff --git a/Sources/OzonTestingMacros/Visitors/ClosureFinder.swift b/Sources/OzonTestingMacros/Visitors/ClosureFinder.swift index 8c10ef6..8877ec5 100644 --- a/Sources/OzonTestingMacros/Visitors/ClosureFinder.swift +++ b/Sources/OzonTestingMacros/Visitors/ClosureFinder.swift @@ -2,7 +2,7 @@ // ClosureFinder.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation diff --git a/Sources/OzonTestingMacros/Visitors/GenericFinder.swift b/Sources/OzonTestingMacros/Visitors/GenericFinder.swift index d9b6e79..21883af 100644 --- a/Sources/OzonTestingMacros/Visitors/GenericFinder.swift +++ b/Sources/OzonTestingMacros/Visitors/GenericFinder.swift @@ -2,7 +2,7 @@ // GenericFinder.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation diff --git a/Sources/OzonTestingMacros/Visitors/PrettyNameTypeFinder.swift b/Sources/OzonTestingMacros/Visitors/PrettyNameTypeFinder.swift index d13fc61..833aeb4 100644 --- a/Sources/OzonTestingMacros/Visitors/PrettyNameTypeFinder.swift +++ b/Sources/OzonTestingMacros/Visitors/PrettyNameTypeFinder.swift @@ -2,7 +2,7 @@ // PrettyNameTypeFinder.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation diff --git a/Sources/TestingMacroCollection/Arbitrary+Ext/ArbitraryAccessModifier.swift b/Sources/TestingMacroCollection/Arbitrary+Ext/ArbitraryAccessModifier.swift new file mode 100644 index 0000000..b9e478a --- /dev/null +++ b/Sources/TestingMacroCollection/Arbitrary+Ext/ArbitraryAccessModifier.swift @@ -0,0 +1,12 @@ +// +// ArbitraryAccessModifier.swift +// TestingMacroCollection +// +// Copyright © 2026 Ozon. All rights reserved. +// + +public enum ArbitraryAccessModifier { + case auto + case `public` + case `internal` +} diff --git a/Sources/TestingMacroCollection/Arbitrary+Ext/Foundation+Arbitrary.swift b/Sources/TestingMacroCollection/Arbitrary+Ext/Foundation+Arbitrary.swift index 8532d3c..cbb95d4 100644 --- a/Sources/TestingMacroCollection/Arbitrary+Ext/Foundation+Arbitrary.swift +++ b/Sources/TestingMacroCollection/Arbitrary+Ext/Foundation+Arbitrary.swift @@ -2,7 +2,7 @@ // Foundation+Arbitrary.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import SwiftUI diff --git a/Sources/TestingMacroCollection/AtomicLock/AtomicLock.swift b/Sources/TestingMacroCollection/AtomicLock/AtomicLock.swift index 2632d25..2fd1898 100644 --- a/Sources/TestingMacroCollection/AtomicLock/AtomicLock.swift +++ b/Sources/TestingMacroCollection/AtomicLock/AtomicLock.swift @@ -2,7 +2,7 @@ // AtomicLock.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation diff --git a/Sources/TestingMacroCollection/Mock/AccessModifier.swift b/Sources/TestingMacroCollection/Mock/AccessModifier.swift index f0186f4..2dd156c 100644 --- a/Sources/TestingMacroCollection/Mock/AccessModifier.swift +++ b/Sources/TestingMacroCollection/Mock/AccessModifier.swift @@ -2,7 +2,7 @@ // AccessModifier.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // import Foundation diff --git a/Sources/TestingMacroCollection/Mock/BuildType.swift b/Sources/TestingMacroCollection/Mock/BuildType.swift new file mode 100644 index 0000000..67c1c45 --- /dev/null +++ b/Sources/TestingMacroCollection/Mock/BuildType.swift @@ -0,0 +1,13 @@ +// +// BuildType.swift +// TestingMacroCollection +// +// Copyright © 2026 Ozon. All rights reserved. +// + +/// The build type in which the mock is needed. +/// For a debug build, the mock will be wrapped in `#if debug`. +public enum BuildType { + case debug + case prod +} diff --git a/Sources/TestingMacroCollection/ProxyableMock/ProxyableMock.swift b/Sources/TestingMacroCollection/ProxyableMock/ProxyableMock.swift index 3c6134b..b7ac1a0 100644 --- a/Sources/TestingMacroCollection/ProxyableMock/ProxyableMock.swift +++ b/Sources/TestingMacroCollection/ProxyableMock/ProxyableMock.swift @@ -2,7 +2,7 @@ // ProxyableMock.swift // TestingMacroCollection // -// Copyright © 2025 Ozon. All rights reserved. +// Copyright © 2026 Ozon. All rights reserved. // // MARK: - ProxyableMock diff --git a/Sources/TestingMacroCollection/TestingMacroCollection.swift b/Sources/TestingMacroCollection/TestingMacroCollection.swift index ff65496..5980c81 100644 --- a/Sources/TestingMacroCollection/TestingMacroCollection.swift +++ b/Sources/TestingMacroCollection/TestingMacroCollection.swift @@ -30,13 +30,17 @@ /// - heritability: mock's heritability. Can be inherited or `final'. /// - sendableMode: sendable mock generation mode. /// - defaultValue: default value generation for non-optional properties. Default: `.static`. +/// - buildType: The build type for which the mock is needed. For a `.debug` build, the mock will be wrapped in `#if debug`. +/// Defaults to `debug`. +/// @attached(peer, names: suffixed(Mock)) public macro Mock( associatedTypes: [String: String] = [:], _ accessModifier: AccessModifier = .internal, heritability: Heritability = .final, sendableMode: SendableMode = .auto, - defaultValue: DefaultValue = .static + defaultValue: DefaultValue = .static, + buildType: BuildType = .debug ) = #externalMacro( module: "OzonTestingMacros", type: "MockMacro" @@ -221,14 +225,77 @@ public macro Nilable() = #externalMacro(module: "OzonTestingMacros", type: "Nila /// Model(id: id, name: name, count: count) /// } /// } +/// +/// /// Generated `extension` +/// extension Model { +/// public static func arbitrary(id: UUID = .arbitrary(), name: String = .arbitrary(.static), count: Int = +/// .arbitrary(.static) -> Model { +/// Model(id: id, name: name, count: count) +/// } +/// } /// ``` /// -/// - Parameter arbitraryType: the `Arbitrary` type. When using the `.dynamic` function, it generates random values of the -/// `Foundation` types. +/// - Parameters: +/// - arbitraryType: the `Arbitrary` type. When using the `.dynamic` function, it generates random values of the `Foundation` +/// types. +/// - accessModifier: Access modifier for the generated enum. Defaults to auto, meaning it inherits the modifier from the +/// attached type. +/// - buildType: The build type. When set to `debug`, the generated `enum` and `extension` will be wrapped in `#if DEBUG`. +/// Defaults to `debug`. /// @attached(peer, names: suffixed(Arbitrary)) @attached(member, names: arbitrary) -public macro Arbitrary(_ arbitraryType: ArbitraryType = .static) = #externalMacro( +@attached(extension, names: named(arbitrary)) +public macro Arbitrary( + _ arbitraryType: ArbitraryType = .static, + accessModifier: ArbitraryAccessModifier = .auto, + buildType: BuildType = .debug +) = #externalMacro( module: "OzonTestingMacros", type: "ArbitraryMacro" ) + +/// Auxiliary macro. +/// Specifies the case of an enumeration (`enum`) for generating the default value in the stub +/// when using the `.static` generation type. +/// +/// Example. For: +/// ``` +/// @Arbitrary +/// enum MyEnum { +/// case a +/// @ArbitraryDefaultCase +/// case b +/// } +/// ``` +/// +/// The following will be generated: +/// ``` +/// enum MyEnumArbitrary { +/// static func arbitrary() -> MyEnum { +/// return .b +/// } +/// } +/// ``` +@attached(peer) +public macro ArbitraryDefaultCase() = #externalMacro( + module: "OzonTestingMacros", + type: "ArbitraryDefaultCaseMacro" +) + +/// The macro itself does nothing. This is an auxiliary macro that, +/// when attached to a declaration, lets the Arbitrary macro know that it should +/// use `[]` — Empty, for the default value of collection properties. +/// +/// ``` +/// @Arbitrary +/// class ViewModel { +/// let id: String +/// @Empted var array: [String] +/// } +/// ``` +/// +/// In this example, the `Arbitrary` macro will generate a default value for +/// `array: [String] = []` +@attached(peer) +public macro Empted() = #externalMacro(module: "OzonTestingMacros", type: "EmptedMacro") diff --git a/Sources/TestingMacroCollectionSandbox/main.swift b/Sources/TestingMacroCollectionSandbox/main.swift index c167cc8..84b63ea 100644 --- a/Sources/TestingMacroCollectionSandbox/main.swift +++ b/Sources/TestingMacroCollectionSandbox/main.swift @@ -5,8 +5,8 @@ // Copyright © 2025 Ozon. All rights reserved. // -import Foundation import TestingMacroCollection +import Foundation // MARK: - Playground @@ -14,9 +14,12 @@ import TestingMacroCollection // MARK: - Mock +public enum BadError: Error {} + @Mock(.open, heritability: .inheritable, sendableMode: .enabled, defaultValue: .static) protocol Generic { - func test_simple(value: T) -> T + @available(iOS 14.0, macOS 11.0, *) + func test_simple(value: T) async throws(BadError) -> T func test_complex(value: [T], argument: (T, E)) -> [String: E] func test_closure(closure: @escaping (T, E) -> E) func test_closure_with_return_value(closure: @escaping (T, E) -> E) -> [T] @@ -25,13 +28,14 @@ protocol Generic { @Mock protocol GenericActor: Actor { func test_simple(value: T) async -> T - func test_complex(value: [T], argument: (T, E)) async throws -> [String: E] + func test_complex(value: [T], argument: (T, E)) async throws(BadError) -> [String: E] func test_closure(closure: @escaping (@Sendable (T, E) -> E)) async func test_closure_with_return_value(closure: @escaping (@Sendable (T, E) -> E)) async -> [T] } @Mock(heritability: .inheritable, sendableMode: .enabled, defaultValue: .static) protocol IParentService: Sendable { + @available(swift 5.0) var session: URLSession { get set } func download(path: String) async throws -> Data @@ -41,14 +45,14 @@ protocol IParentService: Sendable { protocol MethodOverloading { func job(arg: T) func job(arg: T?) - + func jobOne(_ arg1: Character, _ arg2: SecTrust, _ arg3: SecCertificate) func jobOne(_ arg1: Int?, _ arg2: [Bool], _ arg3: some IParentService) func jobOne(_ arg1: [String: Int], _ arg2: (Bool, Int), _ arg3: @Sendable @escaping () -> Void) } struct Outer { - struct Inner {} + struct Inner { } } @Mock(sendableMode: .enabled) @@ -57,7 +61,7 @@ protocol IService: IParentService { var dictionary: [String: Any] { get set } var nested: Outer.Inner { get set } - func upload(data: String) async throws -> Bool + func upload(data: String) async throws(BadError) -> Bool } @Mock(.public) @@ -80,7 +84,7 @@ protocol ISuperDuperSlowService: ISuperDuperFastService { var veryVeryVeryBigData: Data { get async } var nested: Outer.Inner { get async } - func superSlowMethod(bigFile: Data) async -> [Data] + func superSlowMethod(bigFile: Data) async throws(BadError) -> [Data] } @AnyMockable @@ -89,7 +93,7 @@ actor MediumService: ISuperDuperSlowService { var nested: Outer.Inner var veryVeryVeryLittleData: (String, Data) - func superSlowMethod(bigFile: Data) async -> [Data] {} + func superSlowMethod(bigFile: Data) async throws(BadError) -> [Data] {} func superFastDownload(target: String) async {} @@ -100,34 +104,26 @@ actor MediumService: ISuperDuperSlowService { class MethodOverloadingAnyMockable: MethodOverloading { func job(arg: T) {} func job(arg: T?) {} - + func jobOne(_ arg1: Character, _ arg2: SecTrust, _ arg3: SecCertificate) {} func jobOne(_ arg1: Int?, _ arg2: [Bool], _ arg3: some IParentService) {} - func jobOne(_ arg1: [String: Int], _ arg2: (Bool, Int), _ arg3: @escaping () -> Void) {} + func jobOne(_ arg1: [String : Int], _ arg2: (Bool, Int), _ arg3: @escaping () -> Void) {} } @AnyMockable class AnyMockableWithGeneric: Generic { - func test_closure(closure: @escaping (T, E) -> E) { - mock.test_closure(closure: closure) - } - - func test_closure_with_return_value(closure: @escaping (T, E) -> E) -> [T] { - mock.test_closure_with_return_value(closure: closure) - } - - func test_simple(value: T) -> T { - mock.test_simple(value: value) - } - - func test_complex(value: [T], argument: (T, E)) -> [String: E] { - mock.test_complex(value: value, argument: argument) - } + func test_closure(closure: @escaping (T, E) -> E) {} + + func test_closure_with_return_value(closure: @escaping (T, E) -> E) -> [T] {} + + func test_simple(value: T) async throws(BadError) -> T {} + + func test_complex(value: [T], argument: (T, E)) -> [String : E] {} } // MARK: - PerformanceMeasure -let closure = {} +let closure = { } let executionTime = #performanceMeasure { closure() @@ -222,3 +218,92 @@ class Parent { let onetwoOptional: One.Two? let onetwoUnwraped: One.Two! } + +@Arbitrary +struct OneOne { + let two: [OneOne.TwoTwo] + let three: OneOne.Three + + @Arbitrary + struct Three { + let three: [Four] + } +} + +extension OneOne { + @Arbitrary + struct TwoTwo { + let two: String + } +} + +extension OneOne.TwoTwo { + @Arbitrary + struct FourFour { + let four: String + } +} + +@Arbitrary +struct Four { + let four: String +} + +@Arbitrary +struct ServiceModel {} + +@Mock +@Arbitrary +protocol AnotherServiceProtocol {} + +@Arbitrary() +class Service { + let name: String + let model: ServiceModel + let anotherProtocol: AnotherServiceProtocol + + init( + name: String, + model: ServiceModel, + anotherProtocol: AnotherServiceProtocol + ) { + self.name = name + self.model = model + self.anotherProtocol = anotherProtocol + } +} + +@Arbitrary(.dynamic) +enum MyEnum { + case a(name1: StructInEnum, name2: String) + case b +} + +@Arbitrary +struct StructInEnum { + let string: String +} + +@Arbitrary +class ClassWithEnum { + let int: Int + let enumVar: MyEnum + let nestedEnum: ClassWithEnum.NestedEnum +} + +extension ClassWithEnum { + @Arbitrary() + enum NestedEnum { + @ArbitraryDefaultCase case a + case b + } +} + +public struct PublicStruct {} + +public extension PublicStruct { + @Arbitrary(accessModifier: .internal) + struct InnerPublicStruct { + public let string: String + } +} diff --git a/Tests/TestingMacroCollectionTests/AnyMockableMacroTests.swift b/Tests/TestingMacroCollectionTests/AnyMockableMacroTests.swift index 530bb56..4114da7 100644 --- a/Tests/TestingMacroCollectionTests/AnyMockableMacroTests.swift +++ b/Tests/TestingMacroCollectionTests/AnyMockableMacroTests.swift @@ -11,6 +11,137 @@ import SwiftSyntaxMacrosTestSupport import XCTest final class AnyMockableMacroTests: XCTestCase { + func testAnyMockable_withGenericInGenericClause() { + assertMacroExpansion( + """ + @AnyMockable + class Service: IService { + func download(completion: @escaping (Result) -> Void) {} + } + """, + expandedSource: + """ + class Service: IService { + func download(completion: @escaping (Result) -> Void) { + mock.download(completion: completion) + } + + internal let mock = Mock() + + internal final class Mock: @unchecked Sendable { + + private let lock = AtomicLock() + + // MARK: - download + + fileprivate func download(completion: @escaping (Result) -> Void) { + lock.performLockedAction { + downloadCompletionCallsCount += 1 + downloadCompletionReceivedArguments.append(completion as! (Result) -> Void) + } + downloadCompletionClosure?(completion as! (Result) -> Void) + } + var downloadCompletionCallsCount = 0 + var downloadCompletionReceivedArguments: [(Result) -> Void] = [] + var downloadCompletionClosure: ((@escaping (Result) -> Void) -> Void)? + } + } + + extension Service: ProxyableMock { + } + """, + macros: testMacros + ) + } + + func testAnyMockable_withTypedErrorActor() { + assertMacroExpansion( + """ + @AnyMockable + actor ActorWithMethodThatThrowsBadError: ProtocolWithMethodThatThrowsBadError { + func throwBadError() async throws(BadError) + } + """, + expandedSource: + """ + actor ActorWithMethodThatThrowsBadError: ProtocolWithMethodThatThrowsBadError { + func throwBadError() async throws(BadError) { + try await mock.throwBadError() + } + + internal let mock = Mock() + + internal final class Mock: @unchecked Sendable { + + private let lock = AtomicLock() + + // MARK: - throwBadError + + fileprivate func throwBadError() async throws(BadError) { + throwBadErrorCallsCount += 1 + if let throwBadErrorError { + throw throwBadErrorError + } + try await throwBadErrorClosure?() + } + var throwBadErrorCallsCount = 0 + var throwBadErrorError: BadError? + var throwBadErrorClosure: (() async throws(BadError) -> Void)? + } + } + + extension ActorWithMethodThatThrowsBadError: ProxyableMock { + } + """, + macros: testMacros + ) + } + + func testAnyMockable_withTypedError() { + assertMacroExpansion( + """ + @AnyMockable + class ClassWithMethodThatThrowsBadError: ProtocolWithMethodThatThrowsBadError { + func throwBadError() throws(BadError) + } + """, + expandedSource: + """ + class ClassWithMethodThatThrowsBadError: ProtocolWithMethodThatThrowsBadError { + func throwBadError() throws(BadError) { + try mock.throwBadError() + } + + internal let mock = Mock() + + internal final class Mock: @unchecked Sendable { + + private let lock = AtomicLock() + + // MARK: - throwBadError + + fileprivate func throwBadError() throws(BadError) { + lock.performLockedAction { + throwBadErrorCallsCount += 1 + } + if let throwBadErrorError { + throw throwBadErrorError + } + try throwBadErrorClosure?() + } + var throwBadErrorCallsCount = 0 + var throwBadErrorError: BadError? + var throwBadErrorClosure: (() throws(BadError) -> Void)? + } + } + + extension ClassWithMethodThatThrowsBadError: ProxyableMock { + } + """, + macros: testMacros + ) + } + func testAnyMockable_withEqualSignatures() { assertMacroExpansion( """ diff --git a/Tests/TestingMacroCollectionTests/ArbitraryMacroTests.swift b/Tests/TestingMacroCollectionTests/ArbitraryMacroTests.swift index 2021680..96718e3 100644 --- a/Tests/TestingMacroCollectionTests/ArbitraryMacroTests.swift +++ b/Tests/TestingMacroCollectionTests/ArbitraryMacroTests.swift @@ -11,7 +11,261 @@ import SwiftSyntaxMacrosTestSupport import XCTest final class ArbitraryMacroTests: XCTestCase { - func testArbotraru_whenPropertyIsNilable() { + func testArbitraryMacro_withBuildType() { + assertMacroExpansion( + """ + @Arbitrary(buildType: .prod) + struct ProdModel {} + + @Arbitrary + struct DebugModel {} + + @Arbitrary(buildType: .debug) + struct AlsoDebugModel {} + """, + expandedSource: + """ + struct ProdModel {} + + enum ProdModelArbitrary { + static func arbitrary() -> ProdModel { + ProdModel () + } + } + struct DebugModel {} + + #if DEBUG + enum DebugModelArbitrary { + static func arbitrary() -> DebugModel { + DebugModel () + } + } + #endif + struct AlsoDebugModel {} + + #if DEBUG + enum AlsoDebugModelArbitrary { + static func arbitrary() -> AlsoDebugModel { + AlsoDebugModel () + } + } + #endif + + extension ProdModel { + static func arbitrary() -> ProdModel { + ProdModel() + } + } + + #if DEBUG + extension DebugModel { + static func arbitrary() -> DebugModel { + DebugModel() + } + } + #endif + + #if DEBUG + extension AlsoDebugModel { + static func arbitrary() -> AlsoDebugModel { + AlsoDebugModel() + } + } + #endif + """, + macros: testMacros + ) + } + + func testArbitrary_whenAccessModifierIsDefined_Auto() { + assertMacroExpansion( + """ + @Arbitrary(accessModifier: .auto) + public struct PublicStruct { + public let id: String + } + + @Arbitrary(accessModifier: .auto) + struct InternalStruct { + let id: String + } + """, + expandedSource: + """ + public struct PublicStruct { + public let id: String + + public init(id: String) { + self.id = id + } + } + + #if DEBUG + + public enum PublicStructArbitrary { + public static func arbitrary(id: String = .arbitrary(.static)) -> PublicStruct { + PublicStruct (id: id) + } + } + #endif + struct InternalStruct { + let id: String + } + + #if DEBUG + enum InternalStructArbitrary { + static func arbitrary(id: String = .arbitrary(.static)) -> InternalStruct { + InternalStruct (id: id) + } + } + #endif + + #if DEBUG + extension PublicStruct { + public static func arbitrary(id: String = .arbitrary(.static)) -> PublicStruct { + PublicStruct(id: id) + } + } + #endif + + #if DEBUG + extension InternalStruct { + static func arbitrary(id: String = .arbitrary(.static)) -> InternalStruct { + InternalStruct(id: id) + } + } + #endif + """, + macros: testMacros + ) + } + + func testArbitrary_whenAccessModifierIsDefined_Public() { + assertMacroExpansion( + """ + @Arbitrary(accessModifier: .public) + struct PublicStruct { + let id: String + } + + @Arbitrary + public struct AutoNotDefinedStruct { + public let id: String + } + """, + expandedSource: + """ + struct PublicStruct { + let id: String + + public init(id: String) { + self.id = id + } + } + + #if DEBUG + public enum PublicStructArbitrary { + public static func arbitrary(id: String = .arbitrary(.static)) -> PublicStruct { + PublicStruct (id: id) + } + } + #endif + public struct AutoNotDefinedStruct { + public let id: String + + public init(id: String) { + self.id = id + } + } + + #if DEBUG + + public enum AutoNotDefinedStructArbitrary { + public static func arbitrary(id: String = .arbitrary(.static)) -> AutoNotDefinedStruct { + AutoNotDefinedStruct (id: id) + } + } + #endif + + #if DEBUG + extension PublicStruct { + public static func arbitrary(id: String = .arbitrary(.static)) -> PublicStruct { + PublicStruct(id: id) + } + } + #endif + + #if DEBUG + extension AutoNotDefinedStruct { + public static func arbitrary(id: String = .arbitrary(.static)) -> AutoNotDefinedStruct { + AutoNotDefinedStruct(id: id) + } + } + #endif + """, + macros: testMacros + ) + } + + func testArbitrary_whenAccessModifierIsDefined_Internal() { + assertMacroExpansion( + """ + @Arbitrary(accessModifier: .internal) + public struct InternalStruct { + public let id: String + } + + @Arbitrary + struct AutoNotDefinedStruct { + let id: String + } + """, + expandedSource: + """ + public struct InternalStruct { + public let id: String + } + + #if DEBUG + enum InternalStructArbitrary { + static func arbitrary(id: String = .arbitrary(.static)) -> InternalStruct { + InternalStruct (id: id) + } + } + #endif + struct AutoNotDefinedStruct { + let id: String + } + + #if DEBUG + enum AutoNotDefinedStructArbitrary { + static func arbitrary(id: String = .arbitrary(.static)) -> AutoNotDefinedStruct { + AutoNotDefinedStruct (id: id) + } + } + #endif + + #if DEBUG + extension InternalStruct { + static func arbitrary(id: String = .arbitrary(.static)) -> InternalStruct { + InternalStruct(id: id) + } + } + #endif + + #if DEBUG + extension AutoNotDefinedStruct { + static func arbitrary(id: String = .arbitrary(.static)) -> AutoNotDefinedStruct { + AutoNotDefinedStruct(id: id) + } + } + #endif + """, + macros: testMacros + ) + } + + func testArbitrary_whenPropertyIsNilable() { assertMacroExpansion( """ @Arbitrary(.dynamic) @@ -32,11 +286,21 @@ final class ArbitraryMacroTests: XCTestCase { } } + #if DEBUG enum ClassModelArbitrary { static func arbitrary(optionalType: CustomType? = nil, forceUnwrappedType: CustomType! = nil) -> ClassModel { ClassModel (optionalType: optionalType, forceUnwrappedType: forceUnwrappedType) } } + #endif + + #if DEBUG + extension ClassModel { + static func arbitrary(optionalType: CustomType? = nil, forceUnwrappedType: CustomType! = nil) -> ClassModel { + ClassModel(optionalType: optionalType, forceUnwrappedType: forceUnwrappedType) + } + } + #endif """, macros: testMacros ) @@ -80,38 +344,46 @@ final class ArbitraryMacroTests: XCTestCase { class One { class Two {} + #if DEBUG enum TwoArbitrary { static func arbitrary() -> Two { Two () } } + #endif } class Outer { actor InnerActor { } + #if DEBUG enum InnerActorArbitrary { static func arbitrary() -> InnerActor { InnerActor () } } + #endif let innerActor: InnerActor class InnerClass { } + #if DEBUG enum InnerClassArbitrary { static func arbitrary() -> InnerClass { InnerClass () } } + #endif let innerClass: InnerClass struct InnerStruct { } + #if DEBUG enum InnerStructArbitrary { static func arbitrary() -> InnerStruct { InnerStruct () } } + #endif let innerStruct: InnerStruct @@ -134,11 +406,102 @@ final class ArbitraryMacroTests: XCTestCase { } } + #if DEBUG enum OuterArbitrary { static func arbitrary(innerActor: Outer.InnerActor = Outer.InnerActorArbitrary.arbitrary(), innerClass: Outer.InnerClass = Outer.InnerClassArbitrary.arbitrary(), innerStruct: Outer.InnerStruct = Outer.InnerStructArbitrary.arbitrary(), onetwo: One.Two = One.TwoArbitrary.arbitrary(), onetwoOptional: One.Two? = One.TwoArbitrary.arbitrary(), onetwoUnwrapped: One.Two! = One.TwoArbitrary.arbitrary(), innerClassOptional: Outer.InnerClass? = Outer.InnerClassArbitrary.arbitrary(), innerClassUnwrapped: Outer.InnerClass! = Outer.InnerClassArbitrary.arbitrary()) -> Outer { Outer (innerActor: innerActor, innerClass: innerClass, innerStruct: innerStruct, onetwo: onetwo, onetwoOptional: onetwoOptional, onetwoUnwrapped: onetwoUnwrapped, innerClassOptional: innerClassOptional, innerClassUnwrapped: innerClassUnwrapped) } } + #endif + + #if DEBUG + extension One.Two { + static func arbitrary() -> One.Two { + One.Two() + } + } + #endif + + #if DEBUG + extension Outer.InnerActor { + static func arbitrary() -> Outer.InnerActor { + Outer.InnerActor() + } + } + #endif + + #if DEBUG + extension Outer.InnerClass { + static func arbitrary() -> Outer.InnerClass { + Outer.InnerClass() + } + } + #endif + + #if DEBUG + extension Outer.InnerStruct { + static func arbitrary() -> Outer.InnerStruct { + Outer.InnerStruct() + } + } + #endif + + #if DEBUG + extension Outer { + static func arbitrary(innerActor: Outer.InnerActor = Outer.InnerActorArbitrary.arbitrary(), innerClass: Outer.InnerClass = Outer.InnerClassArbitrary.arbitrary(), innerStruct: Outer.InnerStruct = Outer.InnerStructArbitrary.arbitrary(), onetwo: One.Two = One.TwoArbitrary.arbitrary(), onetwoOptional: One.Two? = One.TwoArbitrary.arbitrary(), onetwoUnwrapped: One.Two! = One.TwoArbitrary.arbitrary(), innerClassOptional: Outer.InnerClass? = Outer.InnerClassArbitrary.arbitrary(), innerClassUnwrapped: Outer.InnerClass! = Outer.InnerClassArbitrary.arbitrary()) -> Outer { + Outer(innerActor: innerActor, innerClass: innerClass, innerStruct: innerStruct, onetwo: onetwo, onetwoOptional: onetwoOptional, onetwoUnwrapped: onetwoUnwrapped, innerClassOptional: innerClassOptional, innerClassUnwrapped: innerClassUnwrapped) + } + } + #endif + """, + macros: testMacros + ) + } + + func testArbitrary_deepNestedTypes() { + assertMacroExpansion( + """ + struct One { + struct Two { + struct Three { + let three: String + } + } + } + + @Arbitrary + struct DeepNestedModel { + let oneTwoThree: One.Two.Three + } + """, + expandedSource: + """ + struct One { + struct Two { + struct Three { + let three: String + } + } + } + struct DeepNestedModel { + let oneTwoThree: One.Two.Three + } + + #if DEBUG + enum DeepNestedModelArbitrary { + static func arbitrary(oneTwoThree: One.Two.Three = One.Two.ThreeArbitrary.arbitrary()) -> DeepNestedModel { + DeepNestedModel (oneTwoThree: oneTwoThree) + } + } + #endif + + #if DEBUG + extension DeepNestedModel { + static func arbitrary(oneTwoThree: One.Two.Three = One.Two.ThreeArbitrary.arbitrary()) -> DeepNestedModel { + DeepNestedModel(oneTwoThree: oneTwoThree) + } + } + #endif """, macros: testMacros ) @@ -178,11 +541,21 @@ final class ArbitraryMacroTests: XCTestCase { let bool: Bool } + #if DEBUG enum SimpleModelArbitrary { static func arbitrary(string: String = .arbitrary(.static), int: Int = .arbitrary(.static), decimal: Decimal = .arbitrary(.static), timeInterval: TimeInterval = .arbitrary(.static), date: Date = .arbitrary(.static), float: Float = .arbitrary(.static), url: URL = .arbitrary(), data: Data = .arbitrary(), error: NSError = .arbitrary(), uuid: UUID = .arbitrary(), bool: Bool = .arbitrary(.static)) -> SimpleModel { SimpleModel (string: string, int: int, decimal: decimal, timeInterval: timeInterval, date: date, float: float, url: url, data: data, error: error, uuid: uuid, bool: bool) } } + #endif + + #if DEBUG + extension SimpleModel { + static func arbitrary(string: String = .arbitrary(.static), int: Int = .arbitrary(.static), decimal: Decimal = .arbitrary(.static), timeInterval: TimeInterval = .arbitrary(.static), date: Date = .arbitrary(.static), float: Float = .arbitrary(.static), url: URL = .arbitrary(), data: Data = .arbitrary(), error: NSError = .arbitrary(), uuid: UUID = .arbitrary(), bool: Bool = .arbitrary(.static)) -> SimpleModel { + SimpleModel(string: string, int: int, decimal: decimal, timeInterval: timeInterval, date: date, float: float, url: url, data: data, error: error, uuid: uuid, bool: bool) + } + } + #endif """, macros: testMacros ) @@ -220,11 +593,21 @@ final class ArbitraryMacroTests: XCTestCase { let uuid: UUID? } + #if DEBUG enum SimpleModelArbitrary { static func arbitrary(string: String? = .arbitrary(.static), int: Int? = .arbitrary(.static), decimal: Decimal? = .arbitrary(.static), timeInterval: TimeInterval? = .arbitrary(.static), date: Date? = .arbitrary(.static), float: Float? = .arbitrary(.static), url: URL? = .arbitrary(), data: Data? = .arbitrary(), error: NSError? = .arbitrary(), uuid: UUID? = .arbitrary()) -> SimpleModel { SimpleModel (string: string, int: int, decimal: decimal, timeInterval: timeInterval, date: date, float: float, url: url, data: data, error: error, uuid: uuid) } } + #endif + + #if DEBUG + extension SimpleModel { + static func arbitrary(string: String? = .arbitrary(.static), int: Int? = .arbitrary(.static), decimal: Decimal? = .arbitrary(.static), timeInterval: TimeInterval? = .arbitrary(.static), date: Date? = .arbitrary(.static), float: Float? = .arbitrary(.static), url: URL? = .arbitrary(), data: Data? = .arbitrary(), error: NSError? = .arbitrary(), uuid: UUID? = .arbitrary()) -> SimpleModel { + SimpleModel(string: string, int: int, decimal: decimal, timeInterval: timeInterval, date: date, float: float, url: url, data: data, error: error, uuid: uuid) + } + } + #endif """, macros: testMacros ) @@ -262,11 +645,21 @@ final class ArbitraryMacroTests: XCTestCase { let uuid: UUID! } + #if DEBUG enum SimpleModelArbitrary { static func arbitrary(string: String! = .arbitrary(), int: Int! = .arbitrary(), decimal: Decimal! = .arbitrary(), timeInterval: TimeInterval! = .arbitrary(), date: Date! = .arbitrary(), float: Float! = .arbitrary(), url: URL! = .arbitrary(), data: Data! = .arbitrary(), error: NSError! = .arbitrary(), uuid: UUID! = .arbitrary()) -> SimpleModel { SimpleModel (string: string, int: int, decimal: decimal, timeInterval: timeInterval, date: date, float: float, url: url, data: data, error: error, uuid: uuid) } } + #endif + + #if DEBUG + extension SimpleModel { + static func arbitrary(string: String! = .arbitrary(), int: Int! = .arbitrary(), decimal: Decimal! = .arbitrary(), timeInterval: TimeInterval! = .arbitrary(), date: Date! = .arbitrary(), float: Float! = .arbitrary(), url: URL! = .arbitrary(), data: Data! = .arbitrary(), error: NSError! = .arbitrary(), uuid: UUID! = .arbitrary()) -> SimpleModel { + SimpleModel(string: string, int: int, decimal: decimal, timeInterval: timeInterval, date: date, float: float, url: url, data: data, error: error, uuid: uuid) + } + } + #endif """, macros: testMacros ) @@ -290,6 +683,7 @@ final class ArbitraryMacroTests: XCTestCase { let forceUnwrappedClosure: ((String, Int) -> Bool)! } + #if DEBUG enum SimpleModelArbitrary { static func arbitrary(simpleClosure: @escaping () -> Void = { }, optionalClosure: ((String, Int) -> CustomType)? = { _, _ in @@ -300,6 +694,20 @@ final class ArbitraryMacroTests: XCTestCase { SimpleModel (simpleClosure: simpleClosure, optionalClosure: optionalClosure, forceUnwrappedClosure: forceUnwrappedClosure) } } + #endif + + #if DEBUG + extension SimpleModel { + static func arbitrary(simpleClosure: @escaping () -> Void = { + }, optionalClosure: ((String, Int) -> CustomType)? = { _, _ in + CustomTypeArbitrary.arbitrary() + }, forceUnwrappedClosure: ((String, Int) -> Bool)! = { _, _ in + Bool.arbitrary(.static) + }) -> SimpleModel { + SimpleModel(simpleClosure: simpleClosure, optionalClosure: optionalClosure, forceUnwrappedClosure: forceUnwrappedClosure) + } + } + #endif """, macros: testMacros ) @@ -327,11 +735,63 @@ final class ArbitraryMacroTests: XCTestCase { let _dictionary: Dictionary } + #if DEBUG enum SimpleModelArbitrary { - static func arbitrary(array: [String] = [], _array: Array = [], set: Set = [], dictionary: [String: String] = [:], _dictionary: Dictionary = [:]) -> SimpleModel { + static func arbitrary(array: [String] = [.arbitrary()], _array: Array = [.arbitrary()], set: Set = [.arbitrary()], dictionary: [String: String] = [:], _dictionary: Dictionary = [:]) -> SimpleModel { SimpleModel (array: array, _array: _array, set: set, dictionary: dictionary, _dictionary: _dictionary) } } + #endif + + #if DEBUG + extension SimpleModel { + static func arbitrary(array: [String] = [.arbitrary()], _array: Array = [.arbitrary()], set: Set = [.arbitrary()], dictionary: [String: String] = [:], _dictionary: Dictionary = [:]) -> SimpleModel { + SimpleModel(array: array, _array: _array, set: set, dictionary: dictionary, _dictionary: _dictionary) + } + } + #endif + """, + macros: testMacros + ) + } + + func testArbitrary_simpleModel_withEmptedCollections() { + assertMacroExpansion( + """ + @Arbitrary + struct SimpleModel { + @Empted let array: [String] + let _array: Array + @Empted let set: Set + let dictionary: [String: String] + let _dictionary: Dictionary + } + """, + expandedSource: + """ + struct SimpleModel { + @Empted let array: [String] + let _array: Array + @Empted let set: Set + let dictionary: [String: String] + let _dictionary: Dictionary + } + + #if DEBUG + enum SimpleModelArbitrary { + static func arbitrary(array: [String] = [], _array: Array = [.arbitrary()], set: Set = [], dictionary: [String: String] = [:], _dictionary: Dictionary = [:]) -> SimpleModel { + SimpleModel (array: array, _array: _array, set: set, dictionary: dictionary, _dictionary: _dictionary) + } + } + #endif + + #if DEBUG + extension SimpleModel { + static func arbitrary(array: [String] = [], _array: Array = [.arbitrary()], set: Set = [], dictionary: [String: String] = [:], _dictionary: Dictionary = [:]) -> SimpleModel { + SimpleModel(array: array, _array: _array, set: set, dictionary: dictionary, _dictionary: _dictionary) + } + } + #endif """, macros: testMacros ) @@ -351,11 +811,21 @@ final class ArbitraryMacroTests: XCTestCase { let tuple: (String, Model) } + #if DEBUG enum SimpleModelArbitrary { static func arbitrary(tuple: (String, Model) = (.arbitrary(.static), ModelArbitrary.arbitrary())) -> SimpleModel { SimpleModel (tuple: tuple) } } + #endif + + #if DEBUG + extension SimpleModel { + static func arbitrary(tuple: (String, Model) = (.arbitrary(.static), ModelArbitrary.arbitrary())) -> SimpleModel { + SimpleModel(tuple: tuple) + } + } + #endif """, macros: testMacros ) @@ -377,11 +847,21 @@ final class ArbitraryMacroTests: XCTestCase { let customType: CustomType } + #if DEBUG enum SimpleModelArbitrary { static func arbitrary(model: Model = ModelArbitrary.arbitrary(), customType: CustomType = CustomTypeArbitrary.arbitrary()) -> SimpleModel { SimpleModel (model: model, customType: customType) } } + #endif + + #if DEBUG + extension SimpleModel { + static func arbitrary(model: Model = ModelArbitrary.arbitrary(), customType: CustomType = CustomTypeArbitrary.arbitrary()) -> SimpleModel { + SimpleModel(model: model, customType: customType) + } + } + #endif """, macros: testMacros ) @@ -409,8 +889,9 @@ final class ArbitraryMacroTests: XCTestCase { var url: URL { get set } } + #if DEBUG enum ProtocolModelArbitrary { - static func arbitrary(array: [String] = [], model: Model = ModelArbitrary.arbitrary(), tuple: (String, [String]) = (.arbitrary(.static), []), int: Int = .arbitrary(.static), url: URL = .arbitrary()) -> ProtocolModel { + static func arbitrary(array: [String] = [.arbitrary()], model: Model = ModelArbitrary.arbitrary(), tuple: (String, [String]) = (.arbitrary(.static), [.arbitrary()]), int: Int = .arbitrary(.static), url: URL = .arbitrary()) -> ProtocolModel { let mock = ProtocolModelMock() mock.array = array mock.model = model @@ -420,6 +901,7 @@ final class ArbitraryMacroTests: XCTestCase { return mock } } + #endif """, macros: testMacros ) @@ -443,11 +925,22 @@ final class ArbitraryMacroTests: XCTestCase { } } + #if DEBUG + public enum SimpleModelArbitrary { public static func arbitrary(model: Model = ModelArbitrary.arbitrary()) -> SimpleModel { SimpleModel (model: model) } } + #endif + + #if DEBUG + extension SimpleModel { + public static func arbitrary(model: Model = ModelArbitrary.arbitrary()) -> SimpleModel { + SimpleModel(model: model) + } + } + #endif """, macros: testMacros ) @@ -471,11 +964,21 @@ final class ArbitraryMacroTests: XCTestCase { } } + #if DEBUG enum SimpleModelArbitrary { static func arbitrary(model: Model = ModelArbitrary.arbitrary()) -> SimpleModel { SimpleModel (model: model) } } + #endif + + #if DEBUG + extension SimpleModel { + static func arbitrary(model: Model = ModelArbitrary.arbitrary()) -> SimpleModel { + SimpleModel(model: model) + } + } + #endif """, macros: testMacros ) @@ -502,11 +1005,22 @@ final class ArbitraryMacroTests: XCTestCase { } } + #if DEBUG + public enum SimpleModelArbitrary { public static func arbitrary(model: Model = ModelArbitrary.arbitrary(), wrappedObject: Wrapper) -> SimpleModel { SimpleModel (model: model, wrappedObject: wrappedObject) } } + #endif + + #if DEBUG + extension SimpleModel { + public static func arbitrary(model: Model = ModelArbitrary.arbitrary(), wrappedObject: Wrapper) -> SimpleModel { + SimpleModel(model: model, wrappedObject: wrappedObject) + } + } + #endif """, macros: testMacros ) @@ -530,11 +1044,21 @@ final class ArbitraryMacroTests: XCTestCase { let bool: Bool } + #if DEBUG enum SimpleModelArbitrary { static func arbitrary(string: String = .arbitrary(.dynamic), uuid: UUID = .arbitrary(), bool: Bool = .arbitrary(.dynamic)) -> SimpleModel { SimpleModel (string: string, uuid: uuid, bool: bool) } } + #endif + + #if DEBUG + extension SimpleModel { + static func arbitrary(string: String = .arbitrary(.dynamic), uuid: UUID = .arbitrary(), bool: Bool = .arbitrary(.dynamic)) -> SimpleModel { + SimpleModel(string: string, uuid: uuid, bool: bool) + } + } + #endif """, macros: testMacros ) @@ -556,11 +1080,21 @@ final class ArbitraryMacroTests: XCTestCase { let uuid: UUID } + #if DEBUG enum SimpleModelArbitrary { static func arbitrary(string: String = .arbitrary(.static), uuid: UUID = .arbitrary()) -> SimpleModel { SimpleModel (string: string, uuid: uuid) } } + #endif + + #if DEBUG + extension SimpleModel { + static func arbitrary(string: String = .arbitrary(.static), uuid: UUID = .arbitrary()) -> SimpleModel { + SimpleModel(string: string, uuid: uuid) + } + } + #endif """, macros: testMacros ) @@ -583,20 +1117,417 @@ final class ArbitraryMacroTests: XCTestCase { ) } - func testArbitrary_whenAppliedToEnum() { + func testArbitrary_whenAppliedToEnumWithoutCases() { assertMacroExpansion( """ @Arbitrary - enum Model {} + enum MyEnum {} """, expandedSource: """ - enum Model {} + enum MyEnum {} """, diagnostics: [ - .init(message: "@Arbitrary macro is attached to an unsupported declaration", line: 1, column: 1) + .init(message: "@Arbitrary macro attached to an empty enum", line: 1, column: 1) ], macros: testMacros ) } + + func testArbitrary_whenAppliedToEnumForDynamic() { + assertMacroExpansion( + """ + @Arbitrary(.dynamic) + enum MyEnum { + case a(name1: StructInEnum, name2: String) + case b + } + + @Arbitrary + struct StructInEnum { + let string: String + } + """, + expandedSource: + """ + enum MyEnum { + case a(name1: StructInEnum, name2: String) + case b + } + + #if DEBUG + enum MyEnumArbitrary { + static func arbitrary() -> MyEnum { + let allCases: [MyEnum ] = [.a(name1: StructInEnumArbitrary.arbitrary(), name2: .arbitrary(.dynamic)), .b] + return allCases.randomElement()! + } + } + #endif + struct StructInEnum { + let string: String + } + + #if DEBUG + enum StructInEnumArbitrary { + static func arbitrary(string: String = .arbitrary(.static)) -> StructInEnum { + StructInEnum (string: string) + } + } + #endif + + #if DEBUG + extension MyEnum { + static func arbitrary() -> MyEnum { + let allCases: [MyEnum ] = [.a(name1: StructInEnumArbitrary.arbitrary(), name2: .arbitrary(.dynamic)), .b] + return allCases.randomElement()! + } + } + #endif + + #if DEBUG + extension StructInEnum { + static func arbitrary(string: String = .arbitrary(.static)) -> StructInEnum { + StructInEnum(string: string) + } + } + #endif + """, + macros: testMacros + ) + } + + func testArbitrary_whenAppliedToEnumWithoutDefaultCaseForStatic() { + assertMacroExpansion( + """ + @Arbitrary + enum MyEnum { + case a + case b + } + """, + expandedSource: + """ + enum MyEnum { + case a + case b + } + """, + diagnostics: [ + .init( + message: "@Arbitrary(.static) macro attached to an enum without a case marked with @ArbitraryDefaultCase", + line: 1, + column: 1 + ) + ], + macros: testMacros + ) + } + + func testArbitrary_whenAppliedToEnumWithCasesWithoutAssociatedValues() { + assertMacroExpansion( + """ + @Arbitrary + enum MyEnum { + case a + @ArbitraryDefaultCase + case b + } + """, + expandedSource: + """ + enum MyEnum { + case a + @ArbitraryDefaultCase + case b + } + + #if DEBUG + enum MyEnumArbitrary { + static func arbitrary() -> MyEnum { + return .b + } + } + #endif + + #if DEBUG + extension MyEnum { + static func arbitrary() -> MyEnum { + return .b + } + } + #endif + """, + macros: testMacros + ) + } + + func testArbitrary_whenAppliedToEnumWithCasesWithAssociatedValuesWithoutNames() { + assertMacroExpansion( + """ + @Arbitrary() + enum MyEnum { + @ArbitraryDefaultCase + case a(MyStruct, String) + case b + } + + @Arbitrary() + struct MyStruct { + let string: String + } + """, + expandedSource: + """ + enum MyEnum { + @ArbitraryDefaultCase + case a(MyStruct, String) + case b + } + + #if DEBUG + enum MyEnumArbitrary { + static func arbitrary() -> MyEnum { + return .a(MyStructArbitrary.arbitrary(), .arbitrary(.static)) + } + } + #endif + struct MyStruct { + let string: String + } + + #if DEBUG + enum MyStructArbitrary { + static func arbitrary(string: String = .arbitrary(.static)) -> MyStruct { + MyStruct (string: string) + } + } + #endif + + #if DEBUG + extension MyEnum { + static func arbitrary() -> MyEnum { + return .a(MyStructArbitrary.arbitrary(), .arbitrary(.static)) + } + } + #endif + + #if DEBUG + extension MyStruct { + static func arbitrary(string: String = .arbitrary(.static)) -> MyStruct { + MyStruct(string: string) + } + } + #endif + """, + macros: testMacros + ) + } + + func testArbitrary_whenAppliedToEnumWithCasesWithAssociatedValuesWithNames() { + assertMacroExpansion( + """ + @Arbitrary() + enum MyEnum { + @ArbitraryDefaultCase + case a(name1: MyStruct, name2: String) + case b + } + + @Arbitrary() + struct MyStruct { + let string: String + } + """, + expandedSource: + """ + enum MyEnum { + @ArbitraryDefaultCase + case a(name1: MyStruct, name2: String) + case b + } + + #if DEBUG + enum MyEnumArbitrary { + static func arbitrary() -> MyEnum { + return .a(name1: MyStructArbitrary.arbitrary(), name2: .arbitrary(.static)) + } + } + #endif + struct MyStruct { + let string: String + } + + #if DEBUG + enum MyStructArbitrary { + static func arbitrary(string: String = .arbitrary(.static)) -> MyStruct { + MyStruct (string: string) + } + } + #endif + + #if DEBUG + extension MyEnum { + static func arbitrary() -> MyEnum { + return .a(name1: MyStructArbitrary.arbitrary(), name2: .arbitrary(.static)) + } + } + #endif + + #if DEBUG + extension MyStruct { + static func arbitrary(string: String = .arbitrary(.static)) -> MyStruct { + MyStruct(string: string) + } + } + #endif + """, + macros: testMacros + ) + } + + func testArbitrary_whenAppliedToTypeWithEnumInProperties() { + assertMacroExpansion( + """ + @Arbitrary() + enum MyEnum { + @ArbitraryDefaultCase + case a + case b + } + + @Arbitrary() + struct MyStruct { + let myEnum: MyEnum + } + """, + expandedSource: + """ + enum MyEnum { + @ArbitraryDefaultCase + case a + case b + } + + #if DEBUG + enum MyEnumArbitrary { + static func arbitrary() -> MyEnum { + return .a + } + } + #endif + struct MyStruct { + let myEnum: MyEnum + } + + #if DEBUG + enum MyStructArbitrary { + static func arbitrary(myEnum: MyEnum = MyEnumArbitrary.arbitrary()) -> MyStruct { + MyStruct (myEnum: myEnum) + } + } + #endif + + #if DEBUG + extension MyEnum { + static func arbitrary() -> MyEnum { + return .a + } + } + #endif + + #if DEBUG + extension MyStruct { + static func arbitrary(myEnum: MyEnum = MyEnumArbitrary.arbitrary()) -> MyStruct { + MyStruct(myEnum: myEnum) + } + } + #endif + """, + macros: testMacros + ) + } + + func testArbitrary_whenAppliedToTypeWithComputedProperties() { + assertMacroExpansion( + """ + @Arbitrary + struct StructWithComputedProperty { + var computedProperty: Bool { true } + } + """, + expandedSource: + """ + struct StructWithComputedProperty { + var computedProperty: Bool { true } + } + + #if DEBUG + enum StructWithComputedPropertyArbitrary { + static func arbitrary() -> StructWithComputedProperty { + StructWithComputedProperty () + } + } + #endif + + #if DEBUG + extension StructWithComputedProperty { + static func arbitrary() -> StructWithComputedProperty { + StructWithComputedProperty() + } + } + #endif + """, + macros: testMacros + ) + } + + func testArbitrary_whenAppliedToNestedEnum() { + assertMacroExpansion( + """ + struct MyStruct { + let myEnum: MyEnum + } + + extension MyStruct { + @Arbitrary() + enum MyEnum { + @ArbitraryDefaultCase + case a + case b + } + } + """, + expandedSource: + """ + struct MyStruct { + let myEnum: MyEnum + } + + extension MyStruct { + enum MyEnum { + @ArbitraryDefaultCase + case a + case b + } + + #if DEBUG + enum MyEnumArbitrary { + static func arbitrary() -> MyEnum { + return .a + } + } + #endif + } + + #if DEBUG + extension MyStruct.MyEnum { + static func arbitrary() -> MyStruct.MyEnum { + return .a + } + } + #endif + """, + macros: testMacros + ) + } } diff --git a/Tests/TestingMacroCollectionTests/AutoEquatableMacroTests.swift b/Tests/TestingMacroCollectionTests/AutoEquatableMacroTests.swift index 7e96f16..49a9c26 100644 --- a/Tests/TestingMacroCollectionTests/AutoEquatableMacroTests.swift +++ b/Tests/TestingMacroCollectionTests/AutoEquatableMacroTests.swift @@ -233,4 +233,31 @@ final class AutoEquatableMacroTests: XCTestCase { macros: testMacros ) } + + func testAutoEquatableMacro_enumWithOneCase() { + assertMacroExpansion( + """ + @AutoEquatable + enum Route { + case main + } + """, + expandedSource: + """ + enum Route { + case main + } + + extension Route: Equatable { + static func == (lhs: Route, rhs: Route) -> Bool { + switch (lhs, rhs) { + case (.main, .main): + return true + } + } + } + """, + macros: testMacros + ) + } } diff --git a/Tests/TestingMacroCollectionTests/MockMacroTests.swift b/Tests/TestingMacroCollectionTests/MockMacroTests.swift index e5396e9..cf9d018 100644 --- a/Tests/TestingMacroCollectionTests/MockMacroTests.swift +++ b/Tests/TestingMacroCollectionTests/MockMacroTests.swift @@ -11,6 +11,343 @@ import SwiftSyntaxMacrosTestSupport import XCTest final class MockMacroTests: XCTestCase { + func testMockMacro_withGenericInGenericClause() { + assertMacroExpansion( + """ + @Mock + protocol Service { + func download(completion: @escaping (Result) -> Void) + } + """, + expandedSource: + """ + protocol Service { + func download(completion: @escaping (Result) -> Void) + } + + #if DEBUG + final class ServiceMock: Service { + + // MARK: - Default Empty Init + + init() { + } + + // MARK: - Deinit + + func clearFunctionProperties() { + downloadCompletionReceivedArguments = [] + downloadCompletionClosure = nil + } + + deinit { + clearFunctionProperties() + } + + private let lock = AtomicLock() + + // MARK: - download + + func download(completion: @escaping (Result) -> Void) { + lock.performLockedAction { + downloadCompletionCallsCount += 1 + downloadCompletionReceivedArguments.append(completion as! (Result) -> Void) + } + downloadCompletionClosure?(completion as! (Result) -> Void) + } + var downloadCompletionCallsCount = 0 + var downloadCompletionReceivedArguments: [(Result) -> Void] = [] + var downloadCompletionClosure: ((@escaping (Result) -> Void) -> Void)? + } + #endif + """, + macros: testMacros + ) + } + + func testMockMacro_withAvailableAttribute() { + assertMacroExpansion( + """ + @Mock + protocol NotAvailableService { + @available(iOS 14.0, macOS 11.0, *) + func download() + + @inlinable + func upload() + } + """, + expandedSource: + """ + protocol NotAvailableService { + @available(iOS 14.0, macOS 11.0, *) + func download() + + @inlinable + func upload() + } + + #if DEBUG + final class NotAvailableServiceMock: NotAvailableService { + + // MARK: - Default Empty Init + + init() { + } + + // MARK: - Deinit + + func clearFunctionProperties() { + downloadClosure = nil + uploadClosure = nil + } + + deinit { + clearFunctionProperties() + } + + private let lock = AtomicLock() + + // MARK: - download + + @available(iOS 14.0, macOS 11.0, *) func download() { + lock.performLockedAction { + downloadCallsCount += 1 + } + downloadClosure?() + } + var downloadCallsCount = 0 + var downloadClosure: (() -> Void)? + + // MARK: - upload + + func upload() { + lock.performLockedAction { + uploadCallsCount += 1 + } + uploadClosure?() + } + var uploadCallsCount = 0 + var uploadClosure: (() -> Void)? + } + #endif + """, + macros: testMacros + ) + } + + func testMockMacro_withTypedErrorActor() { + assertMacroExpansion( + """ + @Mock + protocol WithBadError: Actor { + func throwBadError() async throws(BadError) + } + """, + expandedSource: + """ + protocol WithBadError: Actor { + func throwBadError() async throws(BadError) + } + + #if DEBUG + actor WithBadErrorMock: WithBadError { + + // MARK: - Default Empty Init + + init() { + } + + // MARK: - Deinit + + func clearFunctionProperties() async { + throwBadErrorError = nil + throwBadErrorClosure = nil + } + + deinit { + Task { [weak self] in + await self?.clearFunctionProperties() + } + } + + // MARK: - throwBadError + + func throwBadError() async throws(BadError) { + throwBadErrorCallsCount += 1 + if let throwBadErrorError { + throw throwBadErrorError + } + try await throwBadErrorClosure?() + } + var throwBadErrorCallsCount = 0 + private var throwBadErrorError: BadError? + private var throwBadErrorClosure: (() async throws(BadError) -> Void)? + + func setThrowBadErrorError(_ error: BadError) { + throwBadErrorError = error + } + func setThrowBadErrorClosure(_ closure: @escaping (@Sendable () async throws(BadError) -> Void)) { + throwBadErrorClosure = closure + } + } + #endif + """, + macros: testMacros + ) + } + + func testMockMacro_withTypedError() { + assertMacroExpansion( + """ + @Mock + protocol WithBadError { + func throwBadError() throws(BadError) + } + + @Mock + protocol WithBadGenericError { + func throwBadGenericError() throws(BadError) + } + """, + expandedSource: + """ + protocol WithBadError { + func throwBadError() throws(BadError) + } + + #if DEBUG + final class WithBadErrorMock: WithBadError { + + // MARK: - Default Empty Init + + init() { + } + + // MARK: - Deinit + + func clearFunctionProperties() { + throwBadErrorError = nil + throwBadErrorClosure = nil + } + + deinit { + clearFunctionProperties() + } + + private let lock = AtomicLock() + + // MARK: - throwBadError + + func throwBadError() throws(BadError) { + lock.performLockedAction { + throwBadErrorCallsCount += 1 + } + if let throwBadErrorError { + throw throwBadErrorError + } + try throwBadErrorClosure?() + } + var throwBadErrorCallsCount = 0 + var throwBadErrorError: BadError? + var throwBadErrorClosure: (() throws(BadError) -> Void)? + } + #endif + protocol WithBadGenericError { + func throwBadGenericError() throws(BadError) + } + + #if DEBUG + final class WithBadGenericErrorMock: WithBadGenericError { + + // MARK: - Default Empty Init + + init() { + } + + // MARK: - Deinit + + func clearFunctionProperties() { + throwBadGenericErrorError = nil + throwBadGenericErrorClosure = nil + } + + deinit { + clearFunctionProperties() + } + + private let lock = AtomicLock() + + // MARK: - throwBadGenericError + + func throwBadGenericError() throws(BadError) { + lock.performLockedAction { + throwBadGenericErrorCallsCount += 1 + } + if let throwBadGenericErrorError { + throw throwBadGenericErrorError + } + try throwBadGenericErrorClosure?() + } + var throwBadGenericErrorCallsCount = 0 + var throwBadGenericErrorError: BadError? + var throwBadGenericErrorClosure: (() throws(BadError) -> Void)? + } + #endif + """, + macros: testMacros + ) + } + + func testMockMacro_withBuildType() { + assertMacroExpansion( + """ + @Mock(buildType: .debug) + protocol DebugMock {} + + @Mock(buildType: .prod) + protocol ProdMock {} + + @Mock + protocol AlsoDebugMock {} + """, + expandedSource: """ + protocol DebugMock {} + + #if DEBUG + final class DebugMockMock: DebugMock { + + // MARK: - Default Empty Init + + init() { + } + } + #endif + protocol ProdMock {} + + final class ProdMockMock: ProdMock { + + // MARK: - Default Empty Init + + init() { + } + } + protocol AlsoDebugMock {} + + #if DEBUG + final class AlsoDebugMockMock: AlsoDebugMock { + + // MARK: - Default Empty Init + + init() { + } + } + #endif + + """, macros: testMacros + ) + } + func testMockMacro_withEqualSignatures() { assertMacroExpansion( """ @@ -35,6 +372,7 @@ final class MockMacroTests: XCTestCase { func jobOne(_ arg1: [String: Int], _ arg2: (Bool, Int), _ arg3: @Sendable @escaping () -> Void) } + #if DEBUG final class MethodOverloadingMock: MethodOverloading { // MARK: - Default Empty Init @@ -128,6 +466,7 @@ final class MockMacroTests: XCTestCase { var jobOneArg1DictStringIntArg2TupleArg3AttributedFunctionReceivedArguments: [([String: Int], (Bool, Int), @Sendable () -> Void)] = [] var jobOneArg1DictStringIntArg2TupleArg3AttributedFunctionClosure: (([String: Int], (Bool, Int), @Sendable @escaping () -> Void) -> Void)? } + #endif """, macros: testMacros ) @@ -153,6 +492,7 @@ final class MockMacroTests: XCTestCase { func test_closure_with_return_value(closure: @escaping (@Sendable (T, E) -> E)) async -> [T] } + #if DEBUG actor GenericActorMock: GenericActor { // MARK: - Default Empty Init @@ -274,6 +614,7 @@ final class MockMacroTests: XCTestCase { test_closure_with_return_valueClosureReturnValue = returnValue } } + #endif """, macros: testMacros ) @@ -299,6 +640,7 @@ final class MockMacroTests: XCTestCase { func test_closure_with_return_value(closure: @escaping (T, E) -> E) -> [T] } + #if DEBUG final class GenericMock: Generic { // MARK: - Default Empty Init @@ -395,6 +737,7 @@ final class MockMacroTests: XCTestCase { var test_closure_with_return_valueClosureClosure: ((@escaping (Any, Any) -> Any) -> [Any])? var test_closure_with_return_valueClosureReturnValue: [Any]! } + #endif """, macros: testMacros ) @@ -422,6 +765,7 @@ final class MockMacroTests: XCTestCase { var tuple: (String, NSError) { get set } } + #if DEBUG final class IServiceMock: IService { // MARK: - int @@ -490,6 +834,7 @@ final class MockMacroTests: XCTestCase { clearVariableProperties() } } + #endif """, macros: testMacros ) @@ -553,6 +898,7 @@ final class MockMacroTests: XCTestCase { var tuple: (String, NSError) { get set } } + #if DEBUG final class IServiceMock: IService { // MARK: - int @@ -855,6 +1201,7 @@ final class MockMacroTests: XCTestCase { clearVariableProperties() } } + #endif """, macros: testMacros ) @@ -876,6 +1223,7 @@ final class MockMacroTests: XCTestCase { var state: State { get set } } + #if DEBUG open class ViewModelMock: ViewModel { // MARK: - Typealiases @@ -912,6 +1260,7 @@ final class MockMacroTests: XCTestCase { clearVariableProperties() } } + #endif """, macros: testMacros ) @@ -933,6 +1282,7 @@ final class MockMacroTests: XCTestCase { var state: State { get set } } + #if DEBUG public final class ViewModelMock: ViewModel { // MARK: - Typealiases @@ -965,6 +1315,7 @@ final class MockMacroTests: XCTestCase { clearVariableProperties() } } + #endif """, macros: testMacros ) @@ -986,6 +1337,7 @@ final class MockMacroTests: XCTestCase { var state: State { get set } } + #if DEBUG final class ViewModelMock: ViewModel { // MARK: - Typealiases @@ -1018,6 +1370,7 @@ final class MockMacroTests: XCTestCase { clearVariableProperties() } } + #endif """, macros: testMacros ) @@ -1067,6 +1420,7 @@ final class MockMacroTests: XCTestCase { func throwsReturningMethod() async throws -> Wrapper? } + #if DEBUG actor ActorMock: Actor { // MARK: - simpleType @@ -1276,6 +1630,7 @@ final class MockMacroTests: XCTestCase { throwsReturningMethodReturnValue = returnValue } } + #endif """, macros: testMacros ) @@ -1295,6 +1650,7 @@ final class MockMacroTests: XCTestCase { init(argument: String) } + #if DEBUG actor IServiceMock: IService { // MARK: - Protocol Inits @@ -1308,6 +1664,7 @@ final class MockMacroTests: XCTestCase { init() { } } + #endif """, macros: testMacros ) @@ -1327,6 +1684,7 @@ final class MockMacroTests: XCTestCase { init(argument: String) } + #if DEBUG class IServiceMock: IService { // MARK: - Protocol Inits @@ -1353,6 +1711,7 @@ final class MockMacroTests: XCTestCase { clearVariableProperties() } } + #endif """, macros: testMacros ) @@ -1374,6 +1733,7 @@ final class MockMacroTests: XCTestCase { init() } + #if DEBUG final class IServiceMock: IService { // MARK: - Protocol Inits @@ -1385,6 +1745,7 @@ final class MockMacroTests: XCTestCase { init() { } } + #endif """, macros: testMacros ) @@ -1404,6 +1765,7 @@ final class MockMacroTests: XCTestCase { init(argument: String) } + #if DEBUG final class IServiceMock: IService { // MARK: - Protocol Inits @@ -1417,6 +1779,7 @@ final class MockMacroTests: XCTestCase { init() { } } + #endif """, macros: testMacros ) @@ -1440,6 +1803,7 @@ final class MockMacroTests: XCTestCase { var delegate: IServiceDelegate? { get set } } + #if DEBUG final class IServiceMock: IService { // MARK: - ignoredDelegate @@ -1461,6 +1825,7 @@ final class MockMacroTests: XCTestCase { init() { } } + #endif """, macros: testMacros ) @@ -1486,6 +1851,7 @@ final class MockMacroTests: XCTestCase { func anotherSomeMethod() -> any IProtocol } + #if DEBUG final class IServiceMock: IService { // MARK: - object @@ -1554,6 +1920,7 @@ final class MockMacroTests: XCTestCase { var anotherSomeMethodClosure: (() -> any IProtocol)? var anotherSomeMethodReturnValue: (any IProtocol)! } + #endif """, macros: testMacros ) @@ -1573,6 +1940,7 @@ final class MockMacroTests: XCTestCase { func download(url: URL) -> Downloadable.File } + #if DEBUG final class IServiceMock: IService { // MARK: - Default Empty Init @@ -1612,6 +1980,7 @@ final class MockMacroTests: XCTestCase { var downloadUrlClosure: ((URL) -> Downloadable.File)? var downloadUrlReturnValue: Downloadable.File! } + #endif """, macros: testMacros ) @@ -1631,6 +2000,7 @@ final class MockMacroTests: XCTestCase { var nested: Outer.Inner { get set } } + #if DEBUG final class IServiceMock: IService { // MARK: - nested @@ -1660,6 +2030,7 @@ final class MockMacroTests: XCTestCase { clearVariableProperties() } } + #endif """, macros: testMacros ) @@ -1679,6 +2050,7 @@ final class MockMacroTests: XCTestCase { var closure: () -> Void { get set } } + #if DEBUG final class IServiceMock: IService { // MARK: - closure @@ -1708,6 +2080,7 @@ final class MockMacroTests: XCTestCase { clearVariableProperties() } } + #endif """, macros: testMacros ) @@ -1727,6 +2100,7 @@ final class MockMacroTests: XCTestCase { func makeWork() } + #if DEBUG public final class IServiceMock: IParentServiceMock, IService { // MARK: - Default Empty Init @@ -1758,6 +2132,7 @@ final class MockMacroTests: XCTestCase { public var makeWorkCallsCount = 0 public var makeWorkClosure: (() -> Void)? } + #endif """, macros: testMacros ) @@ -1785,6 +2160,7 @@ final class MockMacroTests: XCTestCase { var tuple: (String?, Int) { get set } } + #if DEBUG final class IServiceMock: IService { // MARK: - worker @@ -1866,6 +2242,7 @@ final class MockMacroTests: XCTestCase { clearVariableProperties() } } + #endif """, macros: testMacros ) @@ -1885,6 +2262,7 @@ final class MockMacroTests: XCTestCase { func download(path: String, completion: @escaping ([String]) -> Void) } + #if DEBUG final class IServiceMock: IService { // MARK: - Default Empty Init @@ -1918,6 +2296,7 @@ final class MockMacroTests: XCTestCase { var downloadPathCompletionReceivedArguments: [(String, ([String]) -> Void)] = [] var downloadPathCompletionClosure: ((String, @escaping ([String]) -> Void) -> Void)? } + #endif """, macros: testMacros ) @@ -1937,6 +2316,7 @@ final class MockMacroTests: XCTestCase { func download(completion: @escaping ([String]) -> Void) } + #if DEBUG final class IServiceMock: IService { // MARK: - Default Empty Init @@ -1970,6 +2350,7 @@ final class MockMacroTests: XCTestCase { var downloadCompletionReceivedArguments: [([String]) -> Void] = [] var downloadCompletionClosure: ((@escaping ([String]) -> Void) -> Void)? } + #endif """, macros: testMacros ) @@ -1989,6 +2370,7 @@ final class MockMacroTests: XCTestCase { func makeWork() } + #if DEBUG final class IServiceMock: IService { // MARK: - Default Empty Init @@ -2019,6 +2401,7 @@ final class MockMacroTests: XCTestCase { var makeWorkCallsCount = 0 var makeWorkClosure: (() -> Void)? } + #endif """, macros: testMacros ) @@ -2038,6 +2421,7 @@ final class MockMacroTests: XCTestCase { func makeWork() } + #if DEBUG class IServiceMock: IService { // MARK: - Default Empty Init @@ -2072,6 +2456,7 @@ final class MockMacroTests: XCTestCase { var makeWorkCallsCount = 0 var makeWorkClosure: (() -> Void)? } + #endif """, macros: testMacros ) @@ -2096,6 +2481,7 @@ final class MockMacroTests: XCTestCase { func doWork() async throws -> Bool } + #if DEBUG public actor IServiceMock: IService { // MARK: - customValue @@ -2166,6 +2552,7 @@ final class MockMacroTests: XCTestCase { doWorkReturnValue = returnValue } } + #endif """, macros: testMacros ) @@ -2188,6 +2575,7 @@ final class MockMacroTests: XCTestCase { func makeWork(first argument: String, secondArgument: String) -> String } + #if DEBUG open class IServiceMock: IService { // MARK: - customValue @@ -2235,6 +2623,7 @@ final class MockMacroTests: XCTestCase { open var makeWorkFirstArgumentSecondArgumentClosure: ((String, String) -> String)? open var makeWorkFirstArgumentSecondArgumentReturnValue: String! } + #endif """, macros: testMacros ) @@ -2257,6 +2646,7 @@ final class MockMacroTests: XCTestCase { var customValue: Value? { get set } } + #if DEBUG final class IServiceMock: IService { // MARK: - Typealiases @@ -2271,6 +2661,7 @@ final class MockMacroTests: XCTestCase { init() { } } + #endif """, macros: testMacros ) @@ -2300,6 +2691,7 @@ final class MockMacroTests: XCTestCase { func upload(fileAndPath: (URL, String)?) } + #if DEBUG final class IServiceMock: IService { // MARK: - Default Empty Init @@ -2405,6 +2797,7 @@ final class MockMacroTests: XCTestCase { var uploadFileAndPathReceivedArguments: [(URL, String)?] = [] var uploadFileAndPathClosure: (((URL, String)?) -> Void)? } + #endif """, macros: testMacros ) @@ -2424,6 +2817,7 @@ final class MockMacroTests: XCTestCase { func funcWithTuple(arg: (String, Int)) } + #if DEBUG final class IServiceMock: IService { // MARK: - Default Empty Init @@ -2457,6 +2851,7 @@ final class MockMacroTests: XCTestCase { var funcWithTupleArgReceivedArguments: [(String, Int)] = [] var funcWithTupleArgClosure: (((String, Int)) -> Void)? } + #endif """, macros: testMacros ) @@ -2476,6 +2871,7 @@ final class MockMacroTests: XCTestCase { func funcWithTuple() -> (String, Int) } + #if DEBUG final class IServiceMock: IService { // MARK: - Default Empty Init @@ -2512,6 +2908,7 @@ final class MockMacroTests: XCTestCase { var funcWithTupleClosure: (() -> (String, Int))? var funcWithTupleReturnValue: (String, Int)! } + #endif """, macros: testMacros ) @@ -2535,6 +2932,7 @@ final class MockMacroTests: XCTestCase { func doWork() async } + #if DEBUG actor IServiceMock: IService { // MARK: - name @@ -2587,6 +2985,7 @@ final class MockMacroTests: XCTestCase { doWorkClosure = closure } } + #endif """, macros: testMacros ) @@ -2612,6 +3011,7 @@ final class MockMacroTests: XCTestCase { func doWork(input: Input) -> Output } + #if DEBUG final class IServiceMock: IService { // MARK: - Typealiases @@ -2655,6 +3055,7 @@ final class MockMacroTests: XCTestCase { var doWorkInputClosure: ((Input) -> Output)? var doWorkInputReturnValue: Output! } + #endif """, macros: testMacros ) @@ -2674,6 +3075,7 @@ final class MockMacroTests: XCTestCase { func doWork() async throws -> String } + #if DEBUG final class IServiceMock: IService { // MARK: - Default Empty Init @@ -2713,6 +3115,7 @@ final class MockMacroTests: XCTestCase { var doWorkClosure: (() async throws -> String)? var doWorkReturnValue: String! } + #endif """, macros: testMacros ) @@ -2731,6 +3134,7 @@ final class MockMacroTests: XCTestCase { func doWork() throws } + #if DEBUG final class IServiceMock: IService { // MARK: - Default Empty Init @@ -2766,6 +3170,7 @@ final class MockMacroTests: XCTestCase { var doWorkError: Error? var doWorkClosure: (() throws -> Void)? } + #endif """, macros: testMacros ) @@ -2785,6 +3190,7 @@ final class MockMacroTests: XCTestCase { func doWork() async } + #if DEBUG final class IServiceMock: IService { // MARK: - Default Empty Init @@ -2813,6 +3219,7 @@ final class MockMacroTests: XCTestCase { var doWorkCallsCount = 0 var doWorkClosure: (() async -> Void)? } + #endif """, macros: testMacros ) @@ -2832,6 +3239,7 @@ final class MockMacroTests: XCTestCase { func doWork(arg: String?) } + #if DEBUG final class IServiceMock: IService { // MARK: - Default Empty Init @@ -2865,6 +3273,7 @@ final class MockMacroTests: XCTestCase { var doWorkArgReceivedArguments: [String?] = [] var doWorkArgClosure: ((String?) -> Void)? } + #endif """, macros: testMacros ) @@ -2884,6 +3293,7 @@ final class MockMacroTests: XCTestCase { func doWork() -> String? } + #if DEBUG final class IServiceMock: IService { // MARK: - Default Empty Init @@ -2920,6 +3330,7 @@ final class MockMacroTests: XCTestCase { var doWorkClosure: (() -> String?)? var doWorkReturnValue: String? } + #endif """, macros: testMacros ) @@ -2953,6 +3364,7 @@ final class MockMacroTests: XCTestCase { func doWorkWithArgsAndReturnValue(string: String) -> [String: String] } + #if DEBUG final class IServiceMock: IService { // MARK: - worker @@ -3062,6 +3474,7 @@ final class MockMacroTests: XCTestCase { var doWorkWithArgsAndReturnValueStringClosure: ((String) -> [String: String])? var doWorkWithArgsAndReturnValueStringReturnValue: [String: String]! } + #endif """, macros: testMacros ) @@ -3081,6 +3494,7 @@ final class MockMacroTests: XCTestCase { var worker: String { get set } } + #if DEBUG final class IServiceMock: IService { // MARK: - worker @@ -3110,6 +3524,7 @@ final class MockMacroTests: XCTestCase { clearVariableProperties() } } + #endif """, macros: testMacros ) @@ -3129,6 +3544,7 @@ final class MockMacroTests: XCTestCase { var worker: String? { get set } } + #if DEBUG final class IServiceMock: IService { // MARK: - worker @@ -3140,6 +3556,7 @@ final class MockMacroTests: XCTestCase { init() { } } + #endif """, macros: testMacros ) @@ -3159,6 +3576,7 @@ final class MockMacroTests: XCTestCase { var worker: String! { get set } } + #if DEBUG final class IServiceMock: IService { // MARK: - worker @@ -3170,6 +3588,7 @@ final class MockMacroTests: XCTestCase { init() { } } + #endif """, macros: testMacros ) @@ -3188,6 +3607,7 @@ final class MockMacroTests: XCTestCase { func doWork() } + #if DEBUG final class IServiceMock: IService { // MARK: - Default Empty Init @@ -3218,6 +3638,7 @@ final class MockMacroTests: XCTestCase { var doWorkCallsCount = 0 var doWorkClosure: (() -> Void)? } + #endif """, macros: testMacros ) @@ -3236,6 +3657,7 @@ final class MockMacroTests: XCTestCase { func doWorkWithArgs(string: String, arg2: Bool) } + #if DEBUG final class IServiceMock: IService { // MARK: - Default Empty Init @@ -3269,6 +3691,7 @@ final class MockMacroTests: XCTestCase { var doWorkWithArgsStringArg2ReceivedArguments: [(String, Bool)] = [] var doWorkWithArgsStringArg2Closure: ((String, Bool) -> Void)? } + #endif """, macros: testMacros ) @@ -3287,6 +3710,7 @@ final class MockMacroTests: XCTestCase { func doWorkWithReturnValue() -> String } + #if DEBUG final class IServiceMock: IService { // MARK: - Default Empty Init @@ -3323,6 +3747,7 @@ final class MockMacroTests: XCTestCase { var doWorkWithReturnValueClosure: (() -> String)? var doWorkWithReturnValueReturnValue: String! } + #endif """, macros: testMacros ) @@ -3341,6 +3766,7 @@ final class MockMacroTests: XCTestCase { func doWorkWithArgsAndReturnValue(string: String) -> String } + #if DEBUG final class IServiceMock: IService { // MARK: - Default Empty Init @@ -3380,6 +3806,7 @@ final class MockMacroTests: XCTestCase { var doWorkWithArgsAndReturnValueStringClosure: ((String) -> String)? var doWorkWithArgsAndReturnValueStringReturnValue: String! } + #endif """, macros: testMacros ) @@ -3416,6 +3843,7 @@ final class MockMacroTests: XCTestCase { func makeWork() } + #if DEBUG final class IServiceMock: IService, @unchecked Sendable { // MARK: - Default Empty Init @@ -3446,6 +3874,7 @@ final class MockMacroTests: XCTestCase { var makeWorkCallsCount = 0 var makeWorkClosure: (() -> Void)? } + #endif """, macros: testMacros ) @@ -3464,6 +3893,7 @@ final class MockMacroTests: XCTestCase { func makeWork() } + #if DEBUG final class IServiceMock: IService, @unchecked Sendable { // MARK: - Default Empty Init @@ -3494,6 +3924,7 @@ final class MockMacroTests: XCTestCase { var makeWorkCallsCount = 0 var makeWorkClosure: (() -> Void)? } + #endif """, macros: testMacros ) @@ -3512,6 +3943,7 @@ final class MockMacroTests: XCTestCase { func makeWork() } + #if DEBUG final class IServiceMock: IService { // MARK: - Default Empty Init @@ -3542,6 +3974,7 @@ final class MockMacroTests: XCTestCase { var makeWorkCallsCount = 0 var makeWorkClosure: (() -> Void)? } + #endif """, macros: testMacros ) @@ -3560,6 +3993,7 @@ final class MockMacroTests: XCTestCase { func makeWork() async } + #if DEBUG actor IServiceMock: IService { // MARK: - Default Empty Init @@ -3592,6 +4026,7 @@ final class MockMacroTests: XCTestCase { makeWorkClosure = closure } } + #endif """, macros: testMacros ) diff --git a/Tests/TestingMacroCollectionTests/TestingMacroCollectionTests.swift b/Tests/TestingMacroCollectionTests/TestingMacroCollectionTests.swift index 0779419..effc3ff 100644 --- a/Tests/TestingMacroCollectionTests/TestingMacroCollectionTests.swift +++ b/Tests/TestingMacroCollectionTests/TestingMacroCollectionTests.swift @@ -10,15 +10,16 @@ import SwiftSyntaxMacrosTestSupport import XCTest #if canImport(TestingMacroCollection) -import OzonTestingMacros + import OzonTestingMacros -let testMacros: [String: Macro.Type] = [ - "Mock": MockMacro.self, - "performanceMeasure": PerformanceMeasureMacro.self, - "AutoEquatable": AutoEquatableMacro.self, - "MockAccessor": MockAccessorMacro.self, - "AnyMockable": AnyMockableMacro.self, - "Arbitrary": ArbitraryMacro.self, - "FunctionBodyMock": FunctionBodyMockMacro.self, -] + nonisolated(unsafe) + let testMacros: [String: Macro.Type] = [ + "Mock": MockMacro.self, + "performanceMeasure": PerformanceMeasureMacro.self, + "AutoEquatable": AutoEquatableMacro.self, + "MockAccessor": MockAccessorMacro.self, + "AnyMockable": AnyMockableMacro.self, + "Arbitrary": ArbitraryMacro.self, + "FunctionBodyMock": FunctionBodyMockMacro.self, + ] #endif diff --git a/Versions.xcconfig b/Versions.xcconfig index 81aaa56..30801c4 100644 --- a/Versions.xcconfig +++ b/Versions.xcconfig @@ -1 +1 @@ -OZ_VERSION_NUMBER = 3.0.1 +OZ_VERSION_NUMBER = 3.1.0