如何在Swift ExtensionMacro中获取协议的详细定义?
解决ExtensionMacro中获取协议合规细节的问题
要在ExtensionMacro里从DeclGroupSyntax(比如你的FooSpy测试类)中提取目标协议的合规要求,核心是通过SwiftSyntax的语义查找能力定位协议声明,再解析其成员。以下是具体实现步骤和代码示例:
核心步骤
- 提取类遵守的协议:从DeclGroupSyntax的
inheritanceClause中筛选出目标协议(比如Foo)。 - 查找协议的完整声明:利用MacroExpansionContext的语义查找能力,获取协议的ProtocolDeclSyntax实例。
- 解析协议成员要求:遍历协议的成员块,提取方法、属性等需要实现的内容。
- 生成Spy实现代码:基于提取的成员,生成包含XCTest期望和预设返回值的扩展成员。
完整代码示例
1. 宏的定义与扩展实现
import SwiftSyntax import SwiftSyntaxMacros import XCTest public struct TestableMacro: ExtensionMacro { // 开启语义上下文支持,才能查找协议声明 public static let requiresContext = true public static func expansion( of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, providingExtensionsOf type: some TypeSyntaxProtocol, conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext ) throws -> [ExtensionDeclSyntax] { // 1. 从测试类的继承列表中找到Foo协议 guard let inheritanceClause = declaration.inheritanceClause, let fooType = inheritanceClause.inheritedTypes.first(where: { $0.type.as(IdentifierTypeSyntax.self)?.name.text == "Foo" })?.type else { throw MacroError.testClassDoesNotConformToFoo } // 2. 通过语义上下文查找Foo协议的完整声明 guard let fooProtocolDecl = context.findSymbol(for: fooType)?.declaration.as(ProtocolDeclSyntax.self) else { throw MacroError.cannotLocateFooProtocol } // 3. 遍历协议成员,生成Spy实现 var extensionMembers: [DeclSyntax] = [] for member in fooProtocolDecl.memberBlock.members { if let funcDecl = member.decl.as(FunctionDeclSyntax.self) { extensionMembers.append(DeclSyntax(generateSpyFunction(for: funcDecl))) } else if let varDecl = member.decl.as(VariableDeclSyntax.self) { extensionMembers.append(DeclSyntax(generateSpyVariable(for: varDecl))) } } // 4. 生成最终的扩展 let extensionDecl = ExtensionDeclSyntax( extendedType: type, memberBlock: MemberBlockSyntax(members: MemberBlockItemListSyntax(extensionMembers)) ) return [extensionDecl] } // 生成带XCTest期望的方法实现 private static func generateSpyFunction(for funcDecl: FunctionDeclSyntax) -> FunctionDeclSyntax { let funcName = funcDecl.name.text let returnType = funcDecl.signature.returnClause?.type // 为每个方法添加对应的期望属性 let expectationProperty = VariableDeclSyntax( bindingKeyword: .keyword(.let), bindings: [ PatternBindingSyntax( pattern: IdentifierPatternSyntax(identifier: .identifier("\(funcName)Expectation")), initializer: InitializerClauseSyntax(value: ExprSyntax("XCTestExpectation(description: \"\(funcName) invocation\")")) ) ] ) // 方法体:标记调用、满足期望、返回预设值 var statements: [CodeBlockItemSyntax] = [ CodeBlockItemSyntax(item: .expr(ExprSyntax("XCTAssert(true, \"\(funcName) was called\")"))), CodeBlockItemSyntax(item: .expr(ExprSyntax("\(funcName)Expectation.fulfill()"))) ] if let returnType = returnType { statements.append(CodeBlockItemSyntax(item: .expr(ExprSyntax("return \(defaultValue(for: returnType))")))) } let body = FunctionBodySyntax(statements: CodeBlockItemListSyntax(statements)) return FunctionDeclSyntax( funcKeyword: funcDecl.funcKeyword, name: funcDecl.name, signature: funcDecl.signature, body: body ) } // 生成带预设值的属性实现 private static func generateSpyVariable(for varDecl: VariableDeclSyntax) -> VariableDeclSyntax { guard let varName = varDecl.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text, let type = varDecl.typeAnnotation?.type else { return varDecl } return VariableDeclSyntax( bindingKeyword: .keyword(.var), bindings: [ PatternBindingSyntax( pattern: IdentifierPatternSyntax(identifier: .identifier(varName)), typeAnnotation: TypeAnnotationSyntax(type: type), initializer: InitializerClauseSyntax(value: ExprSyntax(defaultValue(for: type))) ) ] ) } // 根据类型生成默认返回值 private static func defaultValue(for type: TypeSyntax) -> ExprSyntax { switch type.as(IdentifierTypeSyntax.self)?.name.text { case "Void": return ExprSyntax("()") case "Int": return ExprSyntax("0") case "String": return ExprSyntax("\"\"") case "Bool": return ExprSyntax("false") default: return ExprSyntax("nil") } } } enum MacroError: Error { case testClassDoesNotConformToFoo case cannotLocateFooProtocol }
2. 宏的注册
在MacroPlugin中注册该宏:
import SwiftSyntaxMacros public struct TestableMacroPlugin: MacroPlugin { public static let macros: [String: Macro.Type] = [ "Testable": TestableMacro.self ] }
关键注意事项
- 开启语义上下文:必须设置
requiresContext = true,否则无法通过context.findSymbol查找协议声明。 - 测试Target环境:确保宏在测试Target中展开,测试Target已导入XCTest,避免生产代码引入测试依赖。
- 成员类型适配:示例仅处理了基本方法和属性,可根据需求扩展处理关联类型、下标等协议成员。
内容的提问来源于stack exchange,提问作者CW0007007
相关产品推荐
相关产品推荐

