Last active
March 20, 2026 09:35
-
-
Save kruperfone/8e95ecd7fffa71bdd23887ee058ab404 to your computer and use it in GitHub Desktop.
Spy Macro example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Docs | |
| https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/ | |
| https://docs.swift.org/swift-book/documentation/the-swift-programming-language/attributes/ | |
| https://developer.apple.com/documentation/swift/applying-macros | |
| Examples | |
| https://github.com/Matejkob/swift-spyable/tree/0d031775bcc90d6a39f43479d4fe55f7e6a7673f | |
| https://github.com/krzysztofzablocki/Swift-Macros | |
| https://github.com/apple/swift-syntax/blob/main/Examples/Sources/ExamplePlugin/Macros.swift#L92 | |
| Swift forum | |
| https://forums.swift.org/t/macros-accessing-the-parent-context-of-a-syntax-node-passed-to-a-macro/64443/19 | |
| https://forums.swift.org/t/examples-of-extensionmacro/66717/9 | |
| Other | |
| https://dev.to/francescoleoni98/how-to-create-swift-macros-with-xcode-15-3kll | |
| https://blog.leonifrancesco.com/articles/swift-macros | |
| https://www.avanderlee.com/swift/macros/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| @attached(peer, names: suffixed(Spy)) | |
| public macro spy() = #externalMacro(module: "SpyMacroMacros", type: "SpyMacro") | |
| */ | |
| import SwiftCompilerPlugin | |
| import SwiftSyntax | |
| import SwiftSyntaxBuilder | |
| import SwiftSyntaxMacros | |
| /// Implementation of the `stringify` macro, which takes an expression | |
| /// of any type and produces a tuple containing the value of that expression | |
| /// and the source code that produced the value. For example | |
| /// | |
| /// #stringify(x + y) | |
| /// | |
| /// will expand to | |
| /// | |
| /// (x + y, "x + y") | |
| public struct StringifyMacro: ExpressionMacro { | |
| public static func expansion( | |
| of node: some FreestandingMacroExpansionSyntax, | |
| in context: some MacroExpansionContext | |
| ) -> ExprSyntax { | |
| guard let argument = node.argumentList.first?.expression else { | |
| fatalError("compiler bug: the macro does not have any arguments") | |
| } | |
| return "(\(argument), \(literal: argument.description))" | |
| } | |
| } | |
| /** | |
| Will add a spy next to protocol declaration. | |
| Example: | |
| ``` | |
| @spy() | |
| protocol MyProtocol { | |
| var invokedParameter | |
| var parameter: Int { get set } | |
| func function(integer: Int, boolean: Bool) -> Double {} | |
| } | |
| ``` | |
| will expand to | |
| ``` | |
| class MyProtocolSpy: MyProtocol { | |
| var invokedParameterGetCount: Int = 0 | |
| var stubbedParameterGetResults: [Int] = [] | |
| var stubbedParameterGetResult: Int! | |
| var invokedParameterSetParameters: [Int] = [] | |
| var parameter: Int { | |
| get { | |
| invokedParameterGetCount += 1 | |
| return stubbedParameterGetResults.removeFirst() ?? stubbedParameterGetResult | |
| } | |
| set { | |
| invokedParameterSetParameters.append(newValue) | |
| } | |
| } | |
| var invokedFunctionParameters: [(integer: Int, boolean: Bool)] = [] | |
| var stubbedFunctionResults: [Double] = [] | |
| var stubbedFunctionResult: Double! | |
| func function(integer: Int, boolean: Bool) -> Double { | |
| invokedFunctionParameters.append((integer, boolean)) | |
| return stubbedFunctionResults.removeFirst() ?? stubbedFunctionResult | |
| } | |
| } | |
| ``` | |
| */ | |
| public struct SpyMacro: PeerMacro { | |
| public static func expansion( | |
| of node: SwiftSyntax.AttributeSyntax, | |
| providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, | |
| in context: some SwiftSyntaxMacros.MacroExpansionContext | |
| ) throws -> [SwiftSyntax.DeclSyntax] { | |
| guard let decl = declaration.as(ProtocolDeclSyntax.self) else { return [] } | |
| return [ | |
| DeclSyntax( | |
| ClassDeclSyntax( | |
| name: TokenSyntax.identifier(decl.name.text + "Spy"), | |
| inheritanceClause: InheritanceClauseSyntax { | |
| InheritedTypeSyntax( | |
| type: IdentifierTypeSyntax(name: decl.name) | |
| ) | |
| }, | |
| memberBlockBuilder: { | |
| for member in decl.memberBlock.members { | |
| if let function = member.decl.as(FunctionDeclSyntax.self) { | |
| handleFunction(function) | |
| } else if let variable = member.decl.as(VariableDeclSyntax.self) { | |
| for block in handleVariable(variable) { | |
| block | |
| } | |
| } | |
| } | |
| } | |
| ) | |
| ) | |
| ] | |
| } | |
| private static func handleVariable(_ input: VariableDeclSyntax) -> [MemberBlockItemListSyntax] { | |
| var identifiers: [IdentifierPatternSyntax] = [] | |
| var type: TypeSyntax! | |
| var hasSetter: Bool = false | |
| for binding in input.bindings { | |
| if let value = binding.pattern.as(IdentifierPatternSyntax.self) { | |
| identifiers.append(value.trimmed) | |
| } | |
| if let value = binding.typeAnnotation?.type.as(TypeSyntax.self)?.trimmed { | |
| type = value | |
| } | |
| for accessor in binding.accessorBlock?.accessors.as(AccessorDeclListSyntax.self) ?? [] { | |
| guard let accessor = accessor.as(AccessorDeclSyntax.self) else { continue } | |
| switch accessor.accessorSpecifier.tokenKind { | |
| case .keyword(.set): hasSetter = true | |
| default: continue | |
| } | |
| } | |
| } | |
| return identifiers.map { identifier in | |
| MemberBlockItemListSyntax { | |
| let capitalizedName: DeclSyntax = "\(raw: identifier.identifier.text.capitalized)" | |
| // MARK: Getter spies and stubs | |
| """ | |
| var invoked\(capitalizedName)GetCount: Int = 0 | |
| var stubbed\(capitalizedName)GetResults: [\(type)] = [] | |
| """ | |
| if type.isOptional { | |
| "var stubbed\(capitalizedName)GetResult: \(type)" | |
| } else { | |
| "var stubbed\(capitalizedName)GetResult: \(type)!" | |
| } | |
| // MARK: Setter stubs | |
| if hasSetter { | |
| "var invoked\(capitalizedName)SetParameters: [\(type)] = []" | |
| } | |
| // MARK: Variable declaration and getter | |
| """ | |
| var \(identifier): \(type) { | |
| get { | |
| invoked\(capitalizedName)GetCount += 1 | |
| return stubbed\(capitalizedName)GetResults.removeFirst() ?? stubbed\(capitalizedName)GetResult | |
| } | |
| """ | |
| // MARK: Setter spy | |
| if hasSetter { | |
| """ | |
| set { | |
| invoked\(capitalizedName)SetParameters.append(newValue) | |
| } | |
| """ | |
| } | |
| "\n }" | |
| } | |
| .with(\.leadingTrivia, .newlines(2)) | |
| } | |
| } | |
| private static func handleFunction(_ input: FunctionDeclSyntax) -> MemberBlockItemListSyntax { | |
| let capitalizedName: DeclSyntax = "\(raw: input.name.text.capitalized)" | |
| /// name for invoked parameters | |
| func name(for parameter: FunctionParameterListSyntax.Element) -> TokenSyntax { | |
| (parameter.secondName ?? parameter.firstName).trimmed | |
| } | |
| /// Type for invoked parameters | |
| func invokedType(for parameter: FunctionParameterListSyntax.Element) -> TypeSyntax { | |
| if let syntax = parameter.type.as(AttributedTypeSyntax.self) { | |
| return syntax.baseType | |
| } else { | |
| return parameter.type | |
| } | |
| } | |
| let parameters = input.signature.parameterClause.parameters.filter { | |
| // Filter out non-escaping closures because we can't append it to invoked parameters array | |
| guard let parameterSyntax = $0.as(FunctionParameterSyntax.self) else { return true } | |
| if parameterSyntax.type.is(IdentifierTypeSyntax.self) { | |
| return true | |
| } else if parameterSyntax.type.isOptional { | |
| // Optional closures are always escaping | |
| return true | |
| } else if parameterSyntax.type.is(FunctionTypeSyntax.self) { | |
| // Closure without @escaping | |
| return false | |
| } else if let type = parameterSyntax.type.as(AttributedTypeSyntax.self) { | |
| return type.attributes | |
| .contains { | |
| $0.as(AttributeSyntax.self)? | |
| .attributeName.as(IdentifierTypeSyntax.self)? | |
| .name | |
| .tokenKind == .identifier("escaping") | |
| } | |
| } | |
| return false | |
| } | |
| return MemberBlockItemListSyntax { | |
| // MARK: Invoked parameters | |
| switch parameters.count { | |
| case 0: | |
| "var invoked\(capitalizedName)Parameters: [(Void, Void)] = []" | |
| case 1: | |
| let parameter = parameters.first! | |
| "var invoked\(capitalizedName)Parameters: [(\(name(for: parameter)): \(invokedType(for: parameter)), Void)] = []" | |
| default: | |
| let params = parameters | |
| .map { "\(name(for: $0)): \(invokedType(for: $0))" } | |
| .joined(separator: ",") | |
| "var invoked\(capitalizedName)Parameters: [(\(raw: params))] = []" | |
| } | |
| // MARK: Stubbed result | |
| if let clause = input.signature.returnClause, clause.type.as(IdentifierTypeSyntax.self)?.name.text != "Void" { | |
| if input.signature.effectSpecifiers?.throwsSpecifier != nil { | |
| // Stub with Result when throws | |
| "var stubbed\(capitalizedName)Results: [Result<\(clause.type), Error>] = []" | |
| "var stubbed\(capitalizedName)Result: Result<\(clause.type), Error>!" | |
| } else { | |
| "var stubbed\(capitalizedName)Results: [\(clause.type)] = []" | |
| if clause.type.isOptional { | |
| // Skip unwrap modifier when already optional | |
| "var stubbed\(capitalizedName)Result: \(clause.type)" | |
| } else { | |
| "var stubbed\(capitalizedName)Result: \(clause.type)!" | |
| } | |
| } | |
| } else if input.signature.effectSpecifiers?.throwsSpecifier != nil { | |
| // Stub with Result when throws but not returns | |
| "var stubbed\(capitalizedName)Results: [Result<Void, Error>] = []" | |
| "var stubbed\(capitalizedName)Result: Result<Void, Error>!" | |
| } | |
| // MARK: Function declaration | |
| FunctionDeclSyntax( | |
| name: input.name, | |
| signature: input.signature | |
| ) { | |
| // MARK: Fill invoked | |
| switch parameters.count { | |
| case 0: | |
| "invoked\(capitalizedName)Parameters.append(((), ()))" | |
| case 1: | |
| let parameter = parameters.first! | |
| "invoked\(capitalizedName)Parameters.append((\(name(for: parameter)), ()))" | |
| default: | |
| let params = parameters | |
| .map { "\(name(for: $0))" } | |
| .joined(separator: ",") | |
| "invoked\(capitalizedName)Parameters.append((\(raw: params)))" | |
| } | |
| // MARK: Return | |
| if let clause = input.signature.returnClause, clause.type.as(IdentifierTypeSyntax.self)?.name.text != "Void" { | |
| if input.signature.effectSpecifiers?.throwsSpecifier != nil { | |
| // try to get result if throws | |
| "return try (stubbed\(capitalizedName)Results.removeFirst() ?? stubbed\(capitalizedName)Result).get()" | |
| } else { | |
| "return stubbed\(capitalizedName)Results.removeFirst() ?? stubbed\(capitalizedName)Result" | |
| } | |
| } else if input.signature.effectSpecifiers?.throwsSpecifier != nil { | |
| // try to get result if throws | |
| "return try (stubbed\(capitalizedName)Results.removeFirst() ?? stubbed\(capitalizedName)Result).get()" | |
| } | |
| } | |
| } | |
| .with(\.leadingTrivia, .newlines(2)) | |
| } | |
| } | |
| private extension TypeSyntax { | |
| var isOptional: Bool { | |
| self.is(OptionalTypeSyntax.self) || self.is(ImplicitlyUnwrappedOptionalTypeSyntax.self) | |
| } | |
| } | |
| @main | |
| struct SpyMacroPlugin: CompilerPlugin { | |
| let providingMacros: [Macro.Type] = [ | |
| StringifyMacro.self, | |
| SpyMacro.self | |
| ] | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import SwiftSyntaxMacros | |
| import SwiftSyntaxMacrosTestSupport | |
| import XCTest | |
| // Macro implementations build for the host, so the corresponding module is not available when cross-compiling. Cross-compiled tests may still make use of the macro itself in end-to-end tests. | |
| import SpyMacroMacros | |
| let testMacros: [String: Macro.Type] = [ | |
| "stringify": StringifyMacro.self, | |
| "spy": SpyMacro.self | |
| ] | |
| final class SpyMacroTests: XCTestCase { | |
| // MARK: - Variables | |
| func test_variable_whenHasGetter() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| var foobar: Type { get } | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| var foobar: Type { get } | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarGetCount: Int = 0 | |
| var stubbedFoobarGetResults: [Type] = [] | |
| var stubbedFoobarGetResult: Type! | |
| var foobar: Type { | |
| get { | |
| invokedFoobarGetCount += 1 | |
| return stubbedFoobarGetResults.removeFirst() ?? stubbedFoobarGetResult | |
| } | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_variable_whenHasGetter_andSetter() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| var foobar: Type { get set } | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| var foobar: Type { get set } | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarGetCount: Int = 0 | |
| var stubbedFoobarGetResults: [Type] = [] | |
| var stubbedFoobarGetResult: Type! | |
| var invokedFoobarSetParameters: [Type] = [] | |
| var foobar: Type { | |
| get { | |
| invokedFoobarGetCount += 1 | |
| return stubbedFoobarGetResults.removeFirst() ?? stubbedFoobarGetResult | |
| } | |
| set { | |
| invokedFoobarSetParameters.append(newValue) | |
| } | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_variable_whenHasGetter_andSetter_andOptional() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| var foobar: Type? { get set } | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| var foobar: Type? { get set } | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarGetCount: Int = 0 | |
| var stubbedFoobarGetResults: [Type?] = [] | |
| var stubbedFoobarGetResult: Type? | |
| var invokedFoobarSetParameters: [Type?] = [] | |
| var foobar: Type? { | |
| get { | |
| invokedFoobarGetCount += 1 | |
| return stubbedFoobarGetResults.removeFirst() ?? stubbedFoobarGetResult | |
| } | |
| set { | |
| invokedFoobarSetParameters.append(newValue) | |
| } | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_variable_whenHasGetter_andSetter_andOptionalForced() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| var foobar: Type! { get set } | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| var foobar: Type! { get set } | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarGetCount: Int = 0 | |
| var stubbedFoobarGetResults: [Type!] = [] | |
| var stubbedFoobarGetResult: Type! | |
| var invokedFoobarSetParameters: [Type!] = [] | |
| var foobar: Type! { | |
| get { | |
| invokedFoobarGetCount += 1 | |
| return stubbedFoobarGetResults.removeFirst() ?? stubbedFoobarGetResult | |
| } | |
| set { | |
| invokedFoobarSetParameters.append(newValue) | |
| } | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_variable_whenList() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| var foobar1, foobar2: Type { get } | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| var foobar1, foobar2: Type { get } | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobar1GetCount: Int = 0 | |
| var stubbedFoobar1GetResults: [Type] = [] | |
| var stubbedFoobar1GetResult: Type! | |
| var foobar1: Type { | |
| get { | |
| invokedFoobar1GetCount += 1 | |
| return stubbedFoobar1GetResults.removeFirst() ?? stubbedFoobar1GetResult | |
| } | |
| } | |
| var invokedFoobar2GetCount: Int = 0 | |
| var stubbedFoobar2GetResults: [Type] = [] | |
| var stubbedFoobar2GetResult: Type! | |
| var foobar2: Type { | |
| get { | |
| invokedFoobar2GetCount += 1 | |
| return stubbedFoobar2GetResults.removeFirst() ?? stubbedFoobar2GetResult | |
| } | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| // MARK: - Functions | |
| // MARK: Plain | |
| func test_function_whenHasNoArgumentsAndNoReturn() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar() | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar() | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(Void, Void)] = [] | |
| func foobar() { | |
| invokedFoobarParameters.append(((), ())) | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| // MARK: Returns | |
| func test_function_whenReturnsVoid() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar() -> Void | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar() -> Void | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(Void, Void)] = [] | |
| func foobar() -> Void { | |
| invokedFoobarParameters.append(((), ())) | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_function_whenReturnsSomething() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar() -> ReturnType | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar() -> ReturnType | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(Void, Void)] = [] | |
| var stubbedFoobarResults: [ReturnType] = [] | |
| var stubbedFoobarResult: ReturnType! | |
| func foobar() -> ReturnType { | |
| invokedFoobarParameters.append(((), ())) | |
| return stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_function_whenReturnsOptional() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar() -> ReturnType? | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar() -> ReturnType? | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(Void, Void)] = [] | |
| var stubbedFoobarResults: [ReturnType?] = [] | |
| var stubbedFoobarResult: ReturnType? | |
| func foobar() -> ReturnType? { | |
| invokedFoobarParameters.append(((), ())) | |
| return stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_function_whenReturnsOptionalForced() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar() -> ReturnType! | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar() -> ReturnType! | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(Void, Void)] = [] | |
| var stubbedFoobarResults: [ReturnType!] = [] | |
| var stubbedFoobarResult: ReturnType! | |
| func foobar() -> ReturnType! { | |
| invokedFoobarParameters.append(((), ())) | |
| return stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_function_whenReturnsClosure() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar() -> ((InputType1, InputType2) -> ReturnType) | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar() -> ((InputType1, InputType2) -> ReturnType) | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(Void, Void)] = [] | |
| var stubbedFoobarResults: [((InputType1, InputType2) -> ReturnType)] = [] | |
| var stubbedFoobarResult: ((InputType1, InputType2) -> ReturnType)! | |
| func foobar() -> ((InputType1, InputType2) -> ReturnType) { | |
| invokedFoobarParameters.append(((), ())) | |
| return stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_function_whenReturnsAnyPublisher() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar() -> AnyPublisher<SuccessType, FailureType> | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar() -> AnyPublisher<SuccessType, FailureType> | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(Void, Void)] = [] | |
| var stubbedFoobarResults: [AnyPublisher<SuccessType, FailureType>] = [] | |
| var stubbedFoobarResult: AnyPublisher<SuccessType, FailureType>! | |
| func foobar() -> AnyPublisher<SuccessType, FailureType> { | |
| invokedFoobarParameters.append(((), ())) | |
| return stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_function_whenReturnsSubject() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar() -> PassthroughSubject<SuccessType, FailureType> | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar() -> PassthroughSubject<SuccessType, FailureType> | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(Void, Void)] = [] | |
| var stubbedFoobarResults: [PassthroughSubject<SuccessType, FailureType>] = [] | |
| var stubbedFoobarResult: PassthroughSubject<SuccessType, FailureType>! | |
| func foobar() -> PassthroughSubject<SuccessType, FailureType> { | |
| invokedFoobarParameters.append(((), ())) | |
| return stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| // MARK: Arguments | |
| func test_function_whenHasArgument() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar(arg: ArgType) | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar(arg: ArgType) | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(arg: ArgType, Void)] = [] | |
| func foobar(arg: ArgType) { | |
| invokedFoobarParameters.append((arg, ())) | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_function_whenHasOptionalArgument() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar(arg: ArgType?) | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar(arg: ArgType?) | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(arg: ArgType?, Void)] = [] | |
| func foobar(arg: ArgType?) { | |
| invokedFoobarParameters.append((arg, ())) | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_function_whenHasOptionalForcedArgument() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar(arg: ArgType!) | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar(arg: ArgType!) | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(arg: ArgType!, Void)] = [] | |
| func foobar(arg: ArgType!) { | |
| invokedFoobarParameters.append((arg, ())) | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_function_whenHasArgumentWithSecondName() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar(with arg: ArgType) | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar(with arg: ArgType) | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(arg: ArgType, Void)] = [] | |
| func foobar(with arg: ArgType) { | |
| invokedFoobarParameters.append((arg, ())) | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_function_whenHasArguments() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar(arg1: ArgType1, arg2: ArgType2) | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar(arg1: ArgType1, arg2: ArgType2) | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(arg1: ArgType1, arg2: ArgType2)] = [] | |
| func foobar(arg1: ArgType1, arg2: ArgType2) { | |
| invokedFoobarParameters.append((arg1, arg2)) | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| // Closure arguments | |
| func test_function_whenHasEscapingClosureArgument() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar(closure: @escaping () -> Void) | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar(closure: @escaping () -> Void) | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(closure: () -> Void, Void)] = [] | |
| func foobar(closure: @escaping () -> Void) { | |
| invokedFoobarParameters.append((closure, ())) | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_function_whenHasNotEscapingClosureArgument() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar(closure: () -> Void) | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar(closure: () -> Void) | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(Void, Void)] = [] | |
| func foobar(closure: () -> Void) { | |
| invokedFoobarParameters.append(((), ())) | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_function_whenHasOptionalClosureArgument() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar(closure: (() -> Void)?) | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar(closure: (() -> Void)?) | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(closure: (() -> Void)?, Void)] = [] | |
| func foobar(closure: (() -> Void)?) { | |
| invokedFoobarParameters.append((closure, ())) | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| // MARK: Throws | |
| func test_function_whenThrowsAndReturnsNothing() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar() throws | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar() throws | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(Void, Void)] = [] | |
| var stubbedFoobarResults: [Result<Void, Error>] = [] | |
| var stubbedFoobarResult: Result<Void, Error>! | |
| func foobar() throws { | |
| invokedFoobarParameters.append(((), ())) | |
| return try (stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult).get() | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_function_whenThrowsAndReturnsSomething() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar() throws -> ReturnType | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar() throws -> ReturnType | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(Void, Void)] = [] | |
| var stubbedFoobarResults: [Result<ReturnType, Error>] = [] | |
| var stubbedFoobarResult: Result<ReturnType, Error>! | |
| func foobar() throws -> ReturnType { | |
| invokedFoobarParameters.append(((), ())) | |
| return try (stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult).get() | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_function_whenThrowsAndReturnsOptional() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar() throws -> ReturnType? | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar() throws -> ReturnType? | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(Void, Void)] = [] | |
| var stubbedFoobarResults: [Result<ReturnType?, Error>] = [] | |
| var stubbedFoobarResult: Result<ReturnType?, Error>! | |
| func foobar() throws -> ReturnType? { | |
| invokedFoobarParameters.append(((), ())) | |
| return try (stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult).get() | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_function_whenThrowsAndReturnsOptionalForced() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar() throws -> ReturnType! | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar() throws -> ReturnType! | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(Void, Void)] = [] | |
| var stubbedFoobarResults: [Result<ReturnType!, Error>] = [] | |
| var stubbedFoobarResult: Result<ReturnType!, Error>! | |
| func foobar() throws -> ReturnType! { | |
| invokedFoobarParameters.append(((), ())) | |
| return try (stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult).get() | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| // MARK: Async | |
| func test_function_whenAsync() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar() async | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar() async | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(Void, Void)] = [] | |
| func foobar() async { | |
| invokedFoobarParameters.append(((), ())) | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| func test_function_whenAsyncThrows() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar() async throws | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar() async throws | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(Void, Void)] = [] | |
| var stubbedFoobarResults: [Result<Void, Error>] = [] | |
| var stubbedFoobarResult: Result<Void, Error>! | |
| func foobar() async throws { | |
| invokedFoobarParameters.append(((), ())) | |
| return try (stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult).get() | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| // MARK: Everything | |
| func test_function_whenComplex() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol TestProtocol { | |
| func foobar( | |
| with arg1: Arg1, | |
| arg2: Arg2?, | |
| closure: @escaping (cArg1: CArg1, cArg2: CArg2) -> ClosureReturnType | |
| ) async throws -> ReturnType | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol TestProtocol { | |
| func foobar( | |
| with arg1: Arg1, | |
| arg2: Arg2?, | |
| closure: @escaping (cArg1: CArg1, cArg2: CArg2) -> ClosureReturnType | |
| ) async throws -> ReturnType | |
| } | |
| class TestProtocolSpy: TestProtocol { | |
| var invokedFoobarParameters: [(arg1: Arg1, arg2: Arg2?, closure: (cArg1: CArg1, cArg2: CArg2) -> ClosureReturnType)] = [] | |
| var stubbedFoobarResults: [Result<ReturnType, Error>] = [] | |
| var stubbedFoobarResult: Result<ReturnType, Error>! | |
| func foobar( | |
| with arg1: Arg1, | |
| arg2: Arg2?, | |
| closure: @escaping (cArg1: CArg1, cArg2: CArg2) -> ClosureReturnType | |
| ) async throws -> ReturnType { | |
| invokedFoobarParameters.append((arg1, arg2, closure)) | |
| return try (stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult).get() | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| // MARK: - Var and Func | |
| func test_variableAndFunction() { | |
| assertMacroExpansion( | |
| """ | |
| @spy() | |
| protocol MyProtocol { | |
| var parameter: Int { get set } | |
| func function(integer: Int, boolean: Bool) -> Double | |
| } | |
| """, | |
| expandedSource: """ | |
| protocol MyProtocol { | |
| var parameter: Int { get set } | |
| func function(integer: Int, boolean: Bool) -> Double | |
| } | |
| class MyProtocolSpy: MyProtocol { | |
| var invokedParameterGetCount: Int = 0 | |
| var stubbedParameterGetResults: [Int] = [] | |
| var stubbedParameterGetResult: Int! | |
| var invokedParameterSetParameters: [Int] = [] | |
| var parameter: Int { | |
| get { | |
| invokedParameterGetCount += 1 | |
| return stubbedParameterGetResults.removeFirst() ?? stubbedParameterGetResult | |
| } | |
| set { | |
| invokedParameterSetParameters.append(newValue) | |
| } | |
| } | |
| var invokedFunctionParameters: [(integer: Int, boolean: Bool)] = [] | |
| var stubbedFunctionResults: [Double] = [] | |
| var stubbedFunctionResult: Double! | |
| func function(integer: Int, boolean: Bool) -> Double { | |
| invokedFunctionParameters.append((integer, boolean)) | |
| return stubbedFunctionResults.removeFirst() ?? stubbedFunctionResult | |
| } | |
| } | |
| """, | |
| macros: testMacros | |
| ) | |
| } | |
| } | |
| protocol TestProtocol { | |
| var variableGetSet: Int { get set } | |
| var variableGet: Int { get } | |
| func function() | |
| func functionWithArguments() | |
| func functionWithArgumentsAndReturnValue() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment