From 2535354acfb109a0c82e6a2e4d4db80b9cbf48e4 Mon Sep 17 00:00:00 2001 From: Goldilocks97 Date: Tue, 2 Jul 2024 16:01:32 +0300 Subject: [PATCH] SNP-1653 objc-bridge macro --- .../Router/NavigationStateMacro.swift | 2 +- .../Signals/CompletionHandlerMacro.swift | 12 +- .../Signals/MulticastMacro.swift | 2 +- .../Signals/ObjcBridgeMacro.swift | 165 ++++++++++++++++++ .../Signals/SignalsPlugin.swift | 1 + .../Macros/Signals/ObjcBridge.swift | 2 + .../Library/Errors/DeclarationError.swift | 5 +- .../Library/Errors/Support/Decls.swift | 2 +- .../Router/NavigationStateTests.swift | 2 +- .../Signals/CompletionHandlerTests.swift | 39 +++-- Tests/SurfMacros/Signals/MulticastTests.swift | 27 ++- .../SurfMacros/Signals/ObjcBridgeTests.swift | 65 +++++++ .../Support/AttachedTypeTests.swift | 40 +++++ .../Support/ProtocolableMacroTests.swift | 67 +------ .../Support/SupporingTestsBase.swift | 52 ++++++ 15 files changed, 391 insertions(+), 92 deletions(-) create mode 100644 Sources/SurfMacros/Implementation/Signals/ObjcBridgeMacro.swift create mode 100644 Sources/SurfMacros/Macros/Signals/ObjcBridge.swift create mode 100644 Tests/SurfMacros/Signals/ObjcBridgeTests.swift create mode 100644 Tests/SurfMacros/Support/AttachedTypeTests.swift rename Tests/SurfMacros/{Signals => }/Support/ProtocolableMacroTests.swift (80%) create mode 100644 Tests/SurfMacros/Support/SupporingTestsBase.swift diff --git a/Sources/SurfMacros/Implementation/Router/NavigationStateMacro.swift b/Sources/SurfMacros/Implementation/Router/NavigationStateMacro.swift index c8fefd6..62c89a6 100644 --- a/Sources/SurfMacros/Implementation/Router/NavigationStateMacro.swift +++ b/Sources/SurfMacros/Implementation/Router/NavigationStateMacro.swift @@ -58,7 +58,7 @@ private extension NavigationStateMacro { static func checkDeclarationType(declaration: some DeclGroupSyntax) throws { guard declaration.is(StructDeclSyntax.self) else { - throw DeclarationError.wrongAttaching(expected: .struct) + throw DeclarationError.wrongAttaching(expected: [.struct]) } } diff --git a/Sources/SurfMacros/Implementation/Signals/CompletionHandlerMacro.swift b/Sources/SurfMacros/Implementation/Signals/CompletionHandlerMacro.swift index ddbe907..6bc0c4d 100644 --- a/Sources/SurfMacros/Implementation/Signals/CompletionHandlerMacro.swift +++ b/Sources/SurfMacros/Implementation/Signals/CompletionHandlerMacro.swift @@ -28,7 +28,7 @@ public struct CompletionHandlerMacro: PeerMacro { in context: some MacroExpansionContext ) throws -> [DeclSyntax] { guard let protocolDecl = declaration.as(ProtocolDeclSyntax.self) else { - throw DeclarationError.wrongAttaching(expected: .protocol) + throw DeclarationError.wrongAttaching(expected: [.protocol]) } Names.protocol = protocolDecl.name.text try SignalsMacroGroupSupport.checkProtocolDeclaration(protocolDecl) @@ -44,11 +44,11 @@ public struct CompletionHandlerMacro: PeerMacro { private extension CompletionHandlerMacro { static func createHandlerClass(with protocolFuncDecls: [FunctionDeclSyntax]) -> ClassDeclSyntax { - let privateModifier = DeclModifierSyntax(name: .keyword(.private)) + let publicModifier = DeclModifierSyntax(name: .keyword(.public)) let protocolIdentifier = createProtocolIdentifier() let memberBlock = createMemberBlock(with: protocolFuncDecls) return .init( - modifiers: [privateModifier], + modifiers: [publicModifier], name: .identifier(Names.class), inheritanceClause: .init(inheritedTypes: [.init(type: protocolIdentifier)]), memberBlock: memberBlock @@ -66,7 +66,8 @@ private extension CompletionHandlerMacro { for funcDecl in protocolFuncDecls { SignalsMacroGroupSupport.createFuncDecl( from: funcDecl, - with: createFuncBody() + with: createFuncBody(), + modifiers: [.init(name: .keyword(.public))] ) } } @@ -88,9 +89,10 @@ private extension CompletionHandlerMacro { } static func createInit() -> InitializerDeclSyntax { + let publicModifier = DeclModifierSyntax(name: .keyword(.public)) let signature = createInitSignature() let body = createInitBody() - return .init(signature: signature, body: body) + return .init(modifiers: [publicModifier], signature: signature, body: body) } static func createInitSignature() -> FunctionSignatureSyntax { diff --git a/Sources/SurfMacros/Implementation/Signals/MulticastMacro.swift b/Sources/SurfMacros/Implementation/Signals/MulticastMacro.swift index 8f218b1..070c4fe 100644 --- a/Sources/SurfMacros/Implementation/Signals/MulticastMacro.swift +++ b/Sources/SurfMacros/Implementation/Signals/MulticastMacro.swift @@ -31,7 +31,7 @@ public struct MulticastMacro: PeerMacro { in context: some MacroExpansionContext ) throws -> [DeclSyntax] { guard let protocolDecl = declaration.as(ProtocolDeclSyntax.self) else { - throw DeclarationError.wrongAttaching(expected: .protocol) + throw DeclarationError.wrongAttaching(expected: [.protocol]) } try SignalsMacroGroupSupport.checkProtocolDeclaration(protocolDecl) Names.protocol = protocolDecl.name.text diff --git a/Sources/SurfMacros/Implementation/Signals/ObjcBridgeMacro.swift b/Sources/SurfMacros/Implementation/Signals/ObjcBridgeMacro.swift new file mode 100644 index 0000000..e5cf6e2 --- /dev/null +++ b/Sources/SurfMacros/Implementation/Signals/ObjcBridgeMacro.swift @@ -0,0 +1,165 @@ +import SwiftCompilerPlugin +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros +import SurfMacrosSupport + +public struct ObjcBridgeMacro: PeerMacro { + + // MARK: - Names + + private enum Names { + static let nsObject = "NSObject" + static let entity = "entity" + + static var declaration = "" + static var bridgeClass: String { + return declaration + "ObjcBridge" + } + } + + // MARK: - Macro + + public static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + try checkDeclaration(declaration) + let declarationName = try getDeclarationName(declaration) + Names.declaration = declarationName.text + let bridgeClass = createBridgeClass(with: declarationName) + return [.init(bridgeClass)] + } + +} + +// MARK: - Checks + +private extension ObjcBridgeMacro { + + static func checkDeclaration(_ declaration: some DeclSyntaxProtocol) throws { + let error = CustomError(description: "Generic types cannot be represented in objc.") + if let structDecl = declaration.as(StructDeclSyntax.self) { + if structDecl.genericParameterClause != nil { + throw error + } + } + if let classDecl = declaration.as(ClassDeclSyntax.self) { + if classDecl.genericParameterClause != nil { + throw error + } + } + if let enumDecl = declaration.as(EnumDeclSyntax.self) { + if enumDecl.genericParameterClause != nil { + throw error + } + } + } + +} + +// MARK: - Getters + + private extension ObjcBridgeMacro { + + static func getDeclarationName(_ declaration: some DeclSyntaxProtocol) throws -> TokenSyntax { + if let structDecl = declaration.as(StructDeclSyntax.self) { + return structDecl.name + } + if let classDecl = declaration.as(ClassDeclSyntax.self) { + return classDecl.name + } + if let protocolDecl = declaration.as(ProtocolDeclSyntax.self) { + return protocolDecl.name + } + if let enumDecl = declaration.as(EnumDeclSyntax.self) { + return enumDecl.name + } + throw DeclarationError.wrongAttaching(expected: [.class, .struct, .enum, .protocol]) + } + + } + +// MARK: - Creations + + private extension ObjcBridgeMacro { + + static func createBridgeClass(with name: TokenSyntax) -> ClassDeclSyntax { + let publicModifier = createPublicModifier() + let nsObjectInheritance = createNSObjectInheritance() + let memberBlock = createBridgeClassMemberBlock() + return .init( + modifiers: [publicModifier], + name: .identifier(Names.bridgeClass), + inheritanceClause: nsObjectInheritance, + memberBlock: memberBlock + ) + } + + static func createNSObjectInheritance() -> InheritanceClauseSyntax { + let nsObjectType = IdentifierTypeSyntax(name: .identifier(Names.nsObject)) + return .init(inheritedTypes: [.init(type: nsObjectType)]) + } + + static func createBridgeClassMemberBlock() -> MemberBlockSyntax { + let itemList = MemberBlockItemListSyntax { + createEntityProperty() + createInit() + } + return .init(members: itemList) + } + + static func createEntityProperty() -> VariableDeclSyntax { + let publicModifier = createPublicModifier() + + let pattern = IdentifierPatternSyntax(identifier: .identifier(Names.entity)) + let type = TypeAnnotationSyntax(type: IdentifierTypeSyntax(name: .identifier(Names.declaration))) + let patternBinding = PatternBindingSyntax(pattern: pattern, typeAnnotation: type) + + return .init( + modifiers: [publicModifier], + bindingSpecifier: .keyword(.let), + bindings: [patternBinding] + ) + } + + static func createInit() -> InitializerDeclSyntax { + let publicModifier = createPublicModifier() + let signature = createInitSignature() + let body = createInitBody() + return .init(modifiers: [publicModifier], signature: signature, body: body) + } + + static func createInitSignature() -> FunctionSignatureSyntax { + let entityParameter = createEntityParameter() + return .init(parameterClause: .init(parameters: [entityParameter])) + } + + static func createEntityParameter() -> FunctionParameterSyntax { + return .init( + firstName: .wildcardToken(), + secondName: .identifier(Names.entity), + type: IdentifierTypeSyntax(name: .identifier(Names.declaration)) + ) + } + + static func createInitBody() -> CodeBlockSyntax { + let selfEntity = MemberAccessExprSyntax( + base: DeclReferenceExprSyntax(baseName: .keyword(.`self`)), + declName: .init(baseName: .identifier(Names.entity)) + ) + let entityParameter = DeclReferenceExprSyntax(baseName: .identifier(Names.entity)) + let entityAssignment = InfixOperatorExprSyntax( + leftOperand: selfEntity, + operator: AssignmentExprSyntax(), + rightOperand: entityParameter + ) + return .init(statements: [.init(item: .expr(.init(entityAssignment)))]) + } + + static func createPublicModifier() -> DeclModifierSyntax { + return .init(name: .keyword(.public)) + } + + } diff --git a/Sources/SurfMacros/Implementation/Signals/SignalsPlugin.swift b/Sources/SurfMacros/Implementation/Signals/SignalsPlugin.swift index 3dda2d5..1071e11 100644 --- a/Sources/SurfMacros/Implementation/Signals/SignalsPlugin.swift +++ b/Sources/SurfMacros/Implementation/Signals/SignalsPlugin.swift @@ -6,6 +6,7 @@ import SwiftSyntaxMacros struct SignalsPlugin { static let providingMacros: [Macro.Type] = [ MulticastMacro.self, + ObjcBridgeMacro.self, CompletionHandlerMacro.self ] } diff --git a/Sources/SurfMacros/Macros/Signals/ObjcBridge.swift b/Sources/SurfMacros/Macros/Signals/ObjcBridge.swift new file mode 100644 index 0000000..559f9fd --- /dev/null +++ b/Sources/SurfMacros/Macros/Signals/ObjcBridge.swift @@ -0,0 +1,2 @@ +@attached(peer, names: suffixed(ObjcBridge)) +public macro ObjcBridge() = #externalMacro(module: "SurfMacroBody", type: "ObjcBridgeMacro") diff --git a/Sources/SurfMacros/Support/Library/Errors/DeclarationError.swift b/Sources/SurfMacros/Support/Library/Errors/DeclarationError.swift index b20ee7b..68f6248 100644 --- a/Sources/SurfMacros/Support/Library/Errors/DeclarationError.swift +++ b/Sources/SurfMacros/Support/Library/Errors/DeclarationError.swift @@ -2,11 +2,12 @@ import Foundation import SwiftSyntax public enum DeclarationError: Error, CustomStringConvertible { - case wrongAttaching(expected: Decls) + case wrongAttaching(expected: [Decls]) case missedModifier(decl: Decls, declName: String, expected: Modifiers) case missedInheritance(decl: Decls, declName: String, expected: String) case unexpectedAssociatedType case unexpectedVariable + case unexpectedParameterClause public var description: String { switch self { @@ -20,6 +21,8 @@ public enum DeclarationError: Error, CustomStringConvertible { return "There should not be any associated types" case .unexpectedVariable: return "There should not be any variables" + case .unexpectedParameterClause: + return "There should not be any parameter clause" } } } diff --git a/Sources/SurfMacros/Support/Library/Errors/Support/Decls.swift b/Sources/SurfMacros/Support/Library/Errors/Support/Decls.swift index 232d582..3928a10 100644 --- a/Sources/SurfMacros/Support/Library/Errors/Support/Decls.swift +++ b/Sources/SurfMacros/Support/Library/Errors/Support/Decls.swift @@ -1,6 +1,6 @@ import Foundation -public enum Decls: String, CustomStringConvertible { +public enum Decls: String, CustomStringConvertible, CaseIterable { case `protocol` case `class` case `struct` diff --git a/Tests/SurfMacros/Router/NavigationStateTests.swift b/Tests/SurfMacros/Router/NavigationStateTests.swift index d9ee3bb..0d88835 100644 --- a/Tests/SurfMacros/Router/NavigationStateTests.swift +++ b/Tests/SurfMacros/Router/NavigationStateTests.swift @@ -70,7 +70,7 @@ final class NavigationStateMacroTests: XCTestCase { """ } let diagnostic = DiagnosticSpec( - message: "Macro can be attached to struct only", + message: "Macro can be attached to [struct] only", line: 1, column: 1 ) diff --git a/Tests/SurfMacros/Signals/CompletionHandlerTests.swift b/Tests/SurfMacros/Signals/CompletionHandlerTests.swift index 2bad47a..bc19a59 100644 --- a/Tests/SurfMacros/Signals/CompletionHandlerTests.swift +++ b/Tests/SurfMacros/Signals/CompletionHandlerTests.swift @@ -8,16 +8,33 @@ import XCTest import SurfMacroBody import SurfMacrosSupport -private let macro = "CompletionHandler" -private let type = CompletionHandlerMacro.self -private let testMacros: [String: Macro.Type] = [macro: type] +private let testMacro = "CompletionHandler" +private let testType = CompletionHandlerMacro.self +private let testMacros: [String: Macro.Type] = [testMacro: testType] #endif final class CompletionHandlerMacroTests: XCTestCase { + func testWhenAttachedTypeIsNotProtocol() { + let runner = AttachedTypeTests(macro: testMacro, type: testType) + runner.testWhenWrongAttachedType( + originalSource: { + """ + @\(testMacro) + \($0) BatSignal {} + """ + }, + expandedSource: { + """ + \($0) BatSignal {} + """ + }, + allowedDecls: [.protocol] + ) + } + func testAllWrongProtocolFormats() { - let runner = ProtocolableMacroTests(macro: macro, type: type) - runner.testWhenAttachedTypeIsNotProtocol() + let runner = ProtocolableMacroTests(macro: testMacro, type: testType) runner.testWhenFuncHasAttribute() runner.testWhenFuncIsAsync() runner.testWhenFuncIsGenericWithWhereKeyword() @@ -32,7 +49,7 @@ final class CompletionHandlerMacroTests: XCTestCase { func testWithAllPossibleArgumentFormats() { assertMacroExpansion( """ - @\(macro) + @\(testMacro) protocol BatSignal { func call(robin: Robin) func call(for robin: Robin) @@ -46,18 +63,18 @@ final class CompletionHandlerMacroTests: XCTestCase { func call(_ robin: Robin) } - private class BatSignalHandler: BatSignal { + public class BatSignalHandler: BatSignal { private let completion: EmptyClosure? - init(completion: EmptyClosure? = nil) { + public init(completion: EmptyClosure? = nil) { self.completion = completion } - func call(robin: Robin) { + public func call(robin: Robin) { completion?() } - func call(for robin: Robin) { + public func call(for robin: Robin) { completion?() } - func call(_ robin: Robin) { + public func call(_ robin: Robin) { completion?() } } diff --git a/Tests/SurfMacros/Signals/MulticastTests.swift b/Tests/SurfMacros/Signals/MulticastTests.swift index 60c14e3..237c59f 100644 --- a/Tests/SurfMacros/Signals/MulticastTests.swift +++ b/Tests/SurfMacros/Signals/MulticastTests.swift @@ -8,16 +8,33 @@ import XCTest import SurfMacroBody import SurfMacrosSupport -private let macro = "Multicast" -private let type = MulticastMacro.self -private let testMacros: [String: Macro.Type] = [macro: type] +private let testMacro = "Multicast" +private let testType = MulticastMacro.self +private let testMacros: [String: Macro.Type] = [testMacro: testType] #endif final class MulticastMacroTests: XCTestCase { + func testWhenAttachedTypeIsNotProtocol() { + let runner = AttachedTypeTests(macro: testMacro, type: testType) + runner.testWhenWrongAttachedType( + originalSource: { + """ + @\(testMacro) + \($0) BatSignal {} + """ + }, + expandedSource: { + """ + \($0) BatSignal {} + """ + }, + allowedDecls: [.protocol] + ) + } + func testAllWrongProtocolFormats() { - let runner = ProtocolableMacroTests(macro: macro, type: type) - runner.testWhenAttachedTypeIsNotProtocol() + let runner = ProtocolableMacroTests(macro: testMacro, type: testType) runner.testWhenFuncHasAttribute() runner.testWhenFuncIsAsync() runner.testWhenFuncIsGenericWithWhereKeyword() diff --git a/Tests/SurfMacros/Signals/ObjcBridgeTests.swift b/Tests/SurfMacros/Signals/ObjcBridgeTests.swift new file mode 100644 index 0000000..d790a8f --- /dev/null +++ b/Tests/SurfMacros/Signals/ObjcBridgeTests.swift @@ -0,0 +1,65 @@ +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +#if canImport(SurfMacroBody) +import SurfMacroBody +let macro = "ObjcBridge" +let type = ObjcBridgeMacro.self +private let testMacros: [String: Macro.Type] = [macro: type] +#endif + +final class ObjcBridgeMacroTests: XCTestCase { + + func testAttachedType() { + let runner = AttachedTypeTests(macro: macro, type: type) + runner.testWhenWrongAttachedType( + originalSource: { + """ + @\(macro) + public \($0) SomeClass + """ + }, + expandedSource: { + """ + public \($0) SomeClass + """ + }, + allowedDecls: [.class, .enum, .protocol, .struct] + ) + } + + func testWhenThereIsGenericClause() { + let runner = SupporingTestsBase(macro: macro, type: type) + runner.launchTest( + """ + @ObjcBridge + public class SomeName {} + """, + expandedSource: """ + public class SomeName {} + """, + diagnosticMessage: "Generic types cannot be represented in objc." + ) + } + + func testProperUsage() { + assertMacroExpansion( + """ + @ObjcBridge + public class SomeName {} + """, + expandedSource: """ + public class SomeName {} + + public class SomeNameObjcBridge: NSObject { + public let entity: SomeName + public init(_ entity: SomeName) { + self.entity = entity + } + } + """, + macros: testMacros + ) + } +} diff --git a/Tests/SurfMacros/Support/AttachedTypeTests.swift b/Tests/SurfMacros/Support/AttachedTypeTests.swift new file mode 100644 index 0000000..748221a --- /dev/null +++ b/Tests/SurfMacros/Support/AttachedTypeTests.swift @@ -0,0 +1,40 @@ +// +// AttachedTypeTests.swift +// +// +// Created by pavlov on 02.07.2024. +// + +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import SurfMacrosSupport +import XCTest + +class AttachedTypeTests: SupporingTestsBase { + + // MARK: - Tests + + func testWhenWrongAttachedType( + originalSource: (String) -> String, + expandedSource: (String) -> String, + allowedDecls: [Decls], + file: StaticString = #file, + line: UInt = #line + ) { + let diagnosticMessage = "Macro can be attached to \(allowedDecls) only" + let wrongDecls = Decls.allCases.filter { !allowedDecls.contains($0) && $0 != .func } + + wrongDecls + .map { $0.rawValue } + .forEach { + launchTest( + originalSource($0), + expandedSource: expandedSource($0), + diagnosticMessage: diagnosticMessage, + file: file, + line: line + ) + } + } + +} diff --git a/Tests/SurfMacros/Signals/Support/ProtocolableMacroTests.swift b/Tests/SurfMacros/Support/ProtocolableMacroTests.swift similarity index 80% rename from Tests/SurfMacros/Signals/Support/ProtocolableMacroTests.swift rename to Tests/SurfMacros/Support/ProtocolableMacroTests.swift index 7ca8b70..82fcea9 100644 --- a/Tests/SurfMacros/Signals/Support/ProtocolableMacroTests.swift +++ b/Tests/SurfMacros/Support/ProtocolableMacroTests.swift @@ -10,46 +10,10 @@ import SwiftSyntaxMacrosTestSupport import SurfMacrosSupport import XCTest -final class ProtocolableMacroTests { - - private let macro: String - private let type: Macro.Type - - init(macro: String, type: Macro.Type) { - self.macro = macro - self.type = type - } +final class ProtocolableMacroTests: SupporingTestsBase { // MARK: - Tests - func testWhenAttachedTypeIsNotProtocol(file: StaticString = #file, line: UInt = #line) { - let originalSource: (String) -> String = { - """ - @\(self.macro) - \($0) BatSignal {} - """ - } - let expandedSource: (String) -> String = { - """ - \($0) BatSignal {} - """ - } - let diagnosticMessage = "Macro can be attached to protocol only" - let wrongDecls: [Decls] = [.class, .enum, .struct] - - wrongDecls - .map { $0.rawValue } - .forEach { - launchTest( - originalSource($0), - expandedSource: expandedSource($0), - diagnosticMessage: diagnosticMessage, - file: file, - line: line - ) - } - } - func testWhenThereIsAssociated(file: StaticString = #file, line: UInt = #line) { let originalSource = """ @\(self.macro) @@ -335,32 +299,3 @@ final class ProtocolableMacroTests { } } - -// MARK: - Private Methods - -private extension ProtocolableMacroTests { - - func launchTest( - _ originalSource: String, - expandedSource: String, - diagnosticMessage: String, - file: StaticString, - line: UInt - ) { - assertMacroExpansion( - originalSource, - expandedSource: expandedSource, - diagnostics: [ - .init( - message: diagnosticMessage, - line: 1, - column: 1 - ) - ], - macros: [macro: type], - file: file, - line: line - ) - } - -} diff --git a/Tests/SurfMacros/Support/SupporingTestsBase.swift b/Tests/SurfMacros/Support/SupporingTestsBase.swift new file mode 100644 index 0000000..0b5a73f --- /dev/null +++ b/Tests/SurfMacros/Support/SupporingTestsBase.swift @@ -0,0 +1,52 @@ +// +// SupporingTestsBase.swift +// +// +// Created by pavlov on 02.07.2024. +// + +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import SurfMacrosSupport +import XCTest + +class SupporingTestsBase { + + // MARK: - Properties + + let macro: String + let type: Macro.Type + + // MARK: - Init + + init(macro: String, type: Macro.Type) { + self.macro = macro + self.type = type + } + + // MARK: - Methods + + func launchTest( + _ originalSource: String, + expandedSource: String, + diagnosticMessage: String, + file: StaticString = #file, + line: UInt = #line + ) { + assertMacroExpansion( + originalSource, + expandedSource: expandedSource, + diagnostics: [ + .init( + message: diagnosticMessage, + line: 1, + column: 1 + ) + ], + macros: [macro: type], + file: file, + line: line + ) + } + +}