diff --git a/Sources/Spyable/Spyable.swift b/Sources/Spyable/Spyable.swift index c5feb99..d0648d8 100644 --- a/Sources/Spyable/Spyable.swift +++ b/Sources/Spyable/Spyable.swift @@ -134,7 +134,11 @@ /// - The generated spy class name is suffixed with `Spy` (e.g., `ServiceProtocolSpy`). /// @attached(peer, names: suffixed(Spy)) -public macro Spyable(behindPreprocessorFlag: String? = nil, accessLevel: SpyAccessLevel? = nil) = +public macro Spyable( + behindPreprocessorFlag: String? = nil, + accessLevel: SpyAccessLevel? = nil, + inheritedTypes: String? = nil +) = #externalMacro( module: "SpyableMacro", type: "SpyableMacro" diff --git a/Sources/SpyableMacro/Extractors/Extractor.swift b/Sources/SpyableMacro/Extractors/Extractor.swift index 48aeefb..441b045 100644 --- a/Sources/SpyableMacro/Extractors/Extractor.swift +++ b/Sources/SpyableMacro/Extractors/Extractor.swift @@ -145,6 +145,46 @@ struct Extractor { func extractAccessLevel(from protocolDeclSyntax: ProtocolDeclSyntax) -> DeclModifierSyntax? { protocolDeclSyntax.modifiers.first(where: \.name.isAccessLevelSupportedInProtocol) } + + func extractInheritedTypes( + from attribute: AttributeSyntax, + in context: some MacroExpansionContext + ) -> String? { + guard case let .argumentList(argumentList) = attribute.arguments else { + // No arguments are present in the attribute. + return nil + } + + let inheritedTypesArgument = argumentList.first { argument in + argument.label?.text == "inheritedTypes" + } + + guard let inheritedTypesArgument else { + // The `inheritedTypes` argument is missing. + return nil + } + + let segments = inheritedTypesArgument.expression + .as(StringLiteralExprSyntax.self)? + .segments + + guard let segments, + segments.count == 1, + case let .stringSegment(literalSegment)? = segments.first + else { + // The `inheritedTypes` argument's value is not a static string literal. + context.diagnose( + Diagnostic( + node: attribute, + message: SpyableDiagnostic.behindPreprocessorFlagArgumentRequiresStaticStringLiteral, + highlights: [Syntax(inheritedTypesArgument.expression)] + ) + ) + return nil + } + + return literalSegment.content.text + } } extension TokenSyntax { diff --git a/Tests/SpyableMacroTests/Macro/UT_SpyableMacro.swift b/Tests/SpyableMacroTests/Macro/UT_SpyableMacro.swift index 60f87de..246d7c1 100644 --- a/Tests/SpyableMacroTests/Macro/UT_SpyableMacro.swift +++ b/Tests/SpyableMacroTests/Macro/UT_SpyableMacro.swift @@ -555,4 +555,38 @@ final class UT_SpyableMacro: XCTestCase { macros: sut ) } + + func testMacroWithParameterToMakeGeneratedSpyConformToAnotherProtocol() { + let baseProtocolDeclaration = """ + protocol BaseProtocol { + func baseMethod() + } + + class BaseClass: BaseProtocol { + func baseMethod() {} + } + + """ + + let protocolDeclaration = "protocol MyProtocol: BaseProtocol {}" + + assertMacroExpansion( + """ + \(baseProtocolDeclaration) + @Spyable(inheritedTypes: "BaseClass") + \(protocolDeclaration) + """, + expandedSource: """ + \(baseProtocolDeclaration) + + \(protocolDeclaration) + + class MyProtocolSpy: MyProtocol, @unchecked Sendable, BaseClass { + init() { + } + } + """, + macros: sut + ) + } }