diff --git a/Package.swift b/Package.swift index 16f4908..4a12760 100644 --- a/Package.swift +++ b/Package.swift @@ -25,8 +25,7 @@ let package = Package( .macro( name: "MacrosImplementation", dependencies: [ - "MacrosHelper", - .product(name: "SwiftSyntax", package: "swift-syntax"), + "SwiftSyntaxHelper", .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), .product(name: "SwiftDiagnostics", package: "swift-syntax"), @@ -35,8 +34,11 @@ let package = Package( ), .target( - name: "MacrosHelper", - path: "Sources/Macros/Helper" + name: "SwiftSyntaxHelper", + dependencies: [ + .product(name: "SwiftSyntax", package: "swift-syntax"), + ], + path: "Sources/Macros/SyntaxHelper" ), .target( @@ -60,6 +62,7 @@ let package = Package( .testTarget( name: "Bibbi-MacroTests", dependencies: [ + "SwiftSyntaxHelper", "MacrosImplementation", .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), ], diff --git a/README.md b/README.md index d487a57..6ada9dc 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,22 @@ ### Macros -- [v] [@Codable]() -- [v] [@CodableKey(_:)]() -- [v] [@Deprecated]() -- [v] [@DependencyValue(for:)]() -- [v] [@DependencyValues]() -- [v] [@Wrapper]() +- [x] ~~[#URL]()~~ _(Deprecated)_ + +- [x] [@Codable]() +- [x] [@CodableKey(_:)]() +- [x] ~~[@Deprecated]()~~ _(Deprecated)_ +- [x] ~~[@DependencyValue(for:)]()~~ _(Deprecated)_ +- [x] [@DependencyOrganizer]() +- [x] [@Reactor]() +- [x] [@Wrapper]() +- [x] (@WrapperView) + +### Libraries + +- [x] [DIContainer]() +- [x] [Dependencies]() +- [x] [ReactorKit]() ## Requirements @@ -22,4 +32,4 @@ | 버전 | 내용 | | :----: | :------------------: | -| - | - | +| v0.1.0 | `@Codable`, `@Wrapper` 매크로 구현 등 | diff --git a/Sources/Macros/Implementation/Complex/WrapperMacro.swift b/Sources/Macros/Implementation/Complex/WrapperMacro.swift index e7a5467..bb464f6 100644 --- a/Sources/Macros/Implementation/Complex/WrapperMacro.swift +++ b/Sources/Macros/Implementation/Complex/WrapperMacro.swift @@ -39,27 +39,29 @@ extension WrapperMacro: MemberMacro { throw MacroError.message("매크로 선언으로 전달된 Generic 타입이 유효하지 않습니다.") } + let scope = declaration.getAccessControl() + return [ """ - public typealias R = \(firstGenericType) + \(scope)typealias R = \(firstGenericType) """, """ - public typealias V = \(lastGenericType) + \(scope)typealias V = \(lastGenericType) """, """ - public func makeViewController() -> V { + \(scope)func makeViewController() -> V { return \(lastGenericType)(reactor: makeReactor()) } """, """ - public var viewController: V { + \(scope)var viewController: V { return makeViewController() } """, """ - public var reactor: R { + \(scope)var reactor: R { return makeReactor() } """ diff --git a/Sources/Macros/Implementation/Complex/WrapperViewMacro.swift b/Sources/Macros/Implementation/Complex/WrapperViewMacro.swift new file mode 100644 index 0000000..d26ef40 --- /dev/null +++ b/Sources/Macros/Implementation/Complex/WrapperViewMacro.swift @@ -0,0 +1,88 @@ +// +// File.swift +// +// +// Created by 김건우 on 6/3/24. +// + +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct WrapperViewMacro { } + +extension WrapperViewMacro: MemberMacro { + + public static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclGroupSyntax, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + guard + let _ = declaration.as(ClassDeclSyntax.self) + else { + throw MacroError.message("이 매크로는 Class에만 적용할 수 있습니다.") + } + + guard + let genericArguments = node.attributeName + .as(IdentifierTypeSyntax.self)? + .genericArgumentClause? + .arguments, + let firstGenericType = genericArguments + .first? + .argument, + let lastGenericType = genericArguments + .last? + .argument + else { + throw MacroError.message("매크로 선언으로 전달된 Generic 타입이 유효하지 않습니다.") + } + + let scope = declaration.getAccessControl() + + return [ + """ + \(scope)typealias R = \(firstGenericType) + """, + """ + \(scope)typealias V = \(lastGenericType) + """, + + """ + \(scope)func makeView() -> V { + return \(lastGenericType)(reactor: makeReactor()) + } + """, + + """ + \(scope)var view: V { + return makeView() + } + """, + """ + \(scope)var reactor: R { + return makeReactor() + } + """ + ] + } + +} + +extension WrapperViewMacro: ExtensionMacro { + + public static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [ExtensionDeclSyntax] { + let baseWrapperExtension = try ExtensionDeclSyntax("extension \(type.trimmed): BaseWrapper { }") + + return [baseWrapperExtension] + } + +} + diff --git a/Sources/Macros/Implementation/Extension/ReactorMacro.swift b/Sources/Macros/Implementation/Extension/ReactorMacro.swift new file mode 100644 index 0000000..4183f0f --- /dev/null +++ b/Sources/Macros/Implementation/Extension/ReactorMacro.swift @@ -0,0 +1,26 @@ +// +// File.swift +// +// +// Created by 김건우 on 6/5/24. +// + +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct ReactorMacro: ExtensionMacro { + + public static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [ExtensionDeclSyntax] { + let reactorExtension = try ExtensionDeclSyntax("extension \(type.trimmed): Reactor { }") + + return [reactorExtension] + } + +} diff --git a/Sources/Macros/Implementation/MemberAttribute/DependencyValuesMacro.swift b/Sources/Macros/Implementation/MemberAttribute/DependencyOrganizerMacro.swift similarity index 88% rename from Sources/Macros/Implementation/MemberAttribute/DependencyValuesMacro.swift rename to Sources/Macros/Implementation/MemberAttribute/DependencyOrganizerMacro.swift index 86cd99a..55d07d0 100644 --- a/Sources/Macros/Implementation/MemberAttribute/DependencyValuesMacro.swift +++ b/Sources/Macros/Implementation/MemberAttribute/DependencyOrganizerMacro.swift @@ -5,12 +5,12 @@ // Created by 김건우 on 6/1/24. // -import MacrosHelper import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros +import SwiftSyntaxHelper -public struct DependencyValuesMacro: MemberAttributeMacro { +public struct DependencyOrganizerMacro: MemberAttributeMacro { public static func expansion( of node: AttributeSyntax, @@ -36,7 +36,7 @@ public struct DependencyValuesMacro: MemberAttributeMacro { } if !varDecl.attributes.isAttributeApplied("DependencyValue") { - let capitalizedIdentifier = identifier.capitalizeFirstLetter() + let capitalizedIdentifier = identifier.makeFirstLetterLowercase() return [ """ diff --git a/Sources/Macros/Implementation/Plugin.swift b/Sources/Macros/Implementation/Plugin.swift index 4314028..779ceca 100644 --- a/Sources/Macros/Implementation/Plugin.swift +++ b/Sources/Macros/Implementation/Plugin.swift @@ -15,8 +15,10 @@ struct Bibbi_MacroPlugin: CompilerPlugin { CodableKeyMacro.self, CodableMacro.self, WrapperMacro.self, + WrapperViewMacro.self, URLMacro.self, - DependencyValuesMacro.self, + ReactorMacro.self, + DependencyOrganizerMacro.self, DeprecatedMacro.self ] } diff --git a/Sources/Macros/Interface/AccessorMacrosInterface.swift b/Sources/Macros/Interface/AccessorMacrosInterface.swift index a2bd482..8027fbc 100644 --- a/Sources/Macros/Interface/AccessorMacrosInterface.swift +++ b/Sources/Macros/Interface/AccessorMacrosInterface.swift @@ -32,6 +32,7 @@ import Dependencies /// /// Author: - 김소월 /// +@available(*, deprecated) @attached(accessor) public macro DependencyValue(for: any DependencyKey.Type) = #externalMacro( module: "MacrosImplementation", diff --git a/Sources/Macros/Interface/ComplexMacorsInterfae.swift b/Sources/Macros/Interface/ComplexMacorsInterfae.swift index 3359539..4fb3103 100644 --- a/Sources/Macros/Interface/ComplexMacorsInterfae.swift +++ b/Sources/Macros/Interface/ComplexMacorsInterfae.swift @@ -119,6 +119,7 @@ public macro CodableKey(name: String) = #externalMacro( /// func makeReactor() -> R /// } /// ``` +/// 이 매크로는 첨부된 선언의 접근 제어자에 따라 전개되는 멤버의 접근 제어자가 바뀝니다. 예를 들어, 첨부 Class가 public 접근 제어자를 가진다면 전개되는 멤버도 public 접근 제어자를 가집니다. /// /// - Warning: 이 매크로는 Class에만 적용할 수 있습니다. /// @@ -141,3 +142,65 @@ public protocol BaseWrapper { +// MARK: - Wrapper View Complex + + +/// 타입이 BaseWrapper 프로토콜을 준수하게 합니다. +/// +/// 이 매크로를 적용한 타입에 BaseWrapper 프로토콜을 준수하게 하고 make() 메서드 및 view, reactor 계산 프로퍼티를 추가합니다. +/// +/// 첫 번째 Generic 타입은 Reactor 프로토콜을 준수하는 타입이어야 하고, 두 번째 Generic 타입은 ReactorKit.View 프로토콜을 준수하는 타입이어야 합니다. +/// +/// 아래는 전개되기 전과 후의 코드를 보여줍니다. +/// ```swift +/// @Wrapper +/// public class HomeViewControllerWrapper { +/// public func makeReactor() -> R { +/// return HomeReactor() +/// } +/// +/// // Begin expansion of "@WrapperView" +/// public typealias R = RankingReactor +/// public typealias V = RankingView +/// +/// public func makeView() -> V { +/// return RankingView(reactor: makeReactor()) +/// } +/// +/// public var view: V { +/// return makeView() +/// } +/// +/// public var reactor: R { +/// return makeReactor() +/// } +/// // End expansion of "@WrapperView" +/// } +/// // Begin expansion of "@WrapperView" +/// extension HomeViewControllerWrapper: BaseWrapper { } +/// // End expansion of "@WrapperView" +/// ``` +/// +/// - 참고: BaseWrapper 프로토콜은 아래와 같이 선언되어 있습니다. +/// ```swift +/// public protocol BaseWrapper { +/// associatedtype R: Reactor +/// associatedtype V: ReactorKit.View +/// +/// func makeReactor() -> R +/// } +/// ``` +/// 이 매크로는 첨부된 선언의 접근 제어자에 따라 전개되는 멤버의 접근 제어자가 바뀝니다. 예를 들어, 첨부 Class가 public 접근 제어자를 가진다면 전개되는 멤버도 public 접근 제어자를 가집니다. +/// +/// - Warning: 이 매크로는 Class에만 적용할 수 있습니다. +/// +/// - Author: 김소월 +/// +@attached(member, names: arbitrary) +@attached(extension, conformances: BaseWrapper) +public macro WrapperView() = #externalMacro( + module: "MacrosImplementation", + type: "WrapperViewMacro" +) + + diff --git a/Sources/Macros/Interface/ExpressionMacrosInterface.swift b/Sources/Macros/Interface/ExpressionMacrosInterface.swift index 7899817..9d20126 100644 --- a/Sources/Macros/Interface/ExpressionMacrosInterface.swift +++ b/Sources/Macros/Interface/ExpressionMacrosInterface.swift @@ -26,6 +26,7 @@ import Foundation /// - Note: 매크로가 전개되기 전에 전개 단계에서 매개변수로 넘겨진 문자열 리터럴이 유효한지 확인하기에 런타임 경고가 발생할 여지를 차단합니다. /// - Author: 김소월 /// +@available(*, deprecated) @freestanding(expression) public macro URL(_ s: String) -> URL = #externalMacro( module: "MacrosImplementation", diff --git a/Sources/Macros/Interface/ExtensionMacrosInterface.swift b/Sources/Macros/Interface/ExtensionMacrosInterface.swift new file mode 100644 index 0000000..8a30334 --- /dev/null +++ b/Sources/Macros/Interface/ExtensionMacrosInterface.swift @@ -0,0 +1,18 @@ +// +// File.swift +// +// +// Created by 김건우 on 6/5/24. +// + +import ReactorKit + +// MARK: - Reactor Extension + +/// 타입이 Reactor 프로토콜을 준수하게 합니다. +/// - Author: 김소월 +@attached(extension, conformances: Reactor) +public macro Reactor() = #externalMacro( + module: "MacrosImplementation", + type: "ReactorMacro" +) diff --git a/Sources/Macros/Interface/MemberAttributeMacrosInterface.swift b/Sources/Macros/Interface/MemberAttributeMacrosInterface.swift index 7d7f03d..407478a 100644 --- a/Sources/Macros/Interface/MemberAttributeMacrosInterface.swift +++ b/Sources/Macros/Interface/MemberAttributeMacrosInterface.swift @@ -20,7 +20,7 @@ public macro Deprecated() = #externalMacro( ) -// MARK: - Dependency Values Member Attribute +// MARK: - Dependency Organizer MemberAttribute /// @DependencyValue 매크로를 프로퍼티에 전개합니다. @@ -50,8 +50,9 @@ public macro Deprecated() = #externalMacro( /// /// - Warning: 이 매크로는 Extension에만 적용할 수 있습니다. /// +@available(*, deprecated) @attached(memberAttribute) -public macro DependencyVales() = #externalMacro( +public macro DependencyOrganizer() = #externalMacro( module: "MacrosImplementation", - type: "DependencyValuesMacro" + type: "DependencyOrganizerMacro" ) diff --git a/Sources/Macros/Playground/AccessorMacrosPlayground.swift b/Sources/Macros/Playground/AccessorMacrosPlayground.swift index 4bbf880..55cdbc3 100644 --- a/Sources/Macros/Playground/AccessorMacrosPlayground.swift +++ b/Sources/Macros/Playground/AccessorMacrosPlayground.swift @@ -42,16 +42,16 @@ class MemberRepository: MemberRepositoryProtocol { struct MeRepositoryKey: DependencyKey { static let liveValue: MeRepositoryProtocol = MeRepository() } -struct MemberRepositoryKey: DependencyKey { +struct MemberRepoKey: DependencyKey { static let liveValue: MemberRepositoryProtocol = MemberRepository() } -// DependencyValues -@DependencyVales +// DependencyOrganizer +@DependencyOrganizer extension DependencyValues { var meRepository: MeRepositoryProtocol - @DependencyValue(for: MemberRepositoryKey.self) - var memberRepository: MemberRepositoryProtocol + @DependencyValue(for: MemberRepoKey.self) + var memberRepo: MemberRepositoryProtocol } @@ -65,7 +65,7 @@ func runAccessorMacrosPlayground() { let me = meRepository.request() print("Requested Data from MeRepository: \(me)") - @Dependency(\.memberRepository) var memberRepository + @Dependency(\.memberRepo) var memberRepository let members = memberRepository.request() print("Requested Data from MemberRepository: \(members)") diff --git a/Sources/Macros/Playground/ComplexMacrosPlayground.swift b/Sources/Macros/Playground/ComplexMacrosPlayground.swift index ffeae79..3d0fa95 100644 --- a/Sources/Macros/Playground/ComplexMacrosPlayground.swift +++ b/Sources/Macros/Playground/ComplexMacrosPlayground.swift @@ -11,7 +11,7 @@ import MacrosInterface // MARK: - Codable Complex @Codable -public struct MemberDTO { +struct MemberDTO { var name: String @CodableKey(name: "day_of_birth") var dayOfBirth: String var age: Int @@ -25,49 +25,102 @@ public struct MemberDTO { import UIKit import ReactorKit -public class SomeReactor: Reactor { - public typealias Action = NoAction +class HomeReactor { + typealias Action = NoAction - public struct State { } + struct State { } - public var initialState: State = State() + var initialState: State = State() } -public class SomeViewController: UIViewController, ReactorKit.View { - public typealias Reactor = SomeReactor - public var disposeBag = DisposeBag() +extension HomeReactor: Reactor { } + +extension HomeReactor: Identifiable { + public var id: UUID { UUID() } +} + +extension HomeReactor: Equatable { + static func == ( + lhs: HomeReactor, + rhs: HomeReactor + ) -> Bool { + return lhs.id == rhs.id + } +} + +class HomeViewController: UIViewController, ReactorKit.View { + typealias Reactor = HomeReactor + var disposeBag = DisposeBag() - public convenience init(reactor: Reactor) { + convenience init(reactor: Reactor) { self.init() self.reactor = reactor } - public override func viewDidLoad() { + override func viewDidLoad() { super.viewDidLoad() } - public func bind(reactor: SomeReactor) { } + func bind(reactor: HomeReactor) { } } -@Wrapper -public class SomeViewControlllerWrapper { +@Wrapper +class HomeViewControllerWrapper { - public func makeReactor() -> R { - return SomeReactor() + func makeReactor() -> R { + return HomeReactor() } } func runWrapperMacro() { - let someViewController = SomeViewControlllerWrapper().viewController - print("SomeViewController: \(someViewController)") + let vc = HomeViewControllerWrapper().viewController + print("HomeViewController: \(vc)") } #endif + +// MARK: - Wrapper View Complex + +#if canImport(UIKit) + +import UIKit + +class HomeView: UIView, ReactorKit.View { + typealias Reactor = HomeReactor + var disposeBag = DisposeBag() + + convenience init(reactor: Reactor) { + self.init() + self.reactor = reactor + } + + func bind(reactor: HomeReactor) { } +} + +@WrapperView +class HomeViewWrapper { + + func makeReactor() -> R { + return HomeReactor() + } + +} + +func runWrapperViewMacro() { + let v = HomeViewWrapper().view + print("HomeView: \(v)") +} + + + +#endif + + func runComplexMacorsPlayground() { let jsonString = """ @@ -86,6 +139,7 @@ func runComplexMacorsPlayground() { #if canImport(UIKit) runWrapperMacro() + runWrapperViewMacro() #endif diff --git a/Sources/Macros/Playground/ExtensionMacrosPlayground.swift b/Sources/Macros/Playground/ExtensionMacrosPlayground.swift new file mode 100644 index 0000000..41273c6 --- /dev/null +++ b/Sources/Macros/Playground/ExtensionMacrosPlayground.swift @@ -0,0 +1,28 @@ +// +// File.swift +// +// +// Created by 김건우 on 6/5/24. +// + +import ReactorKit +import MacrosInterface + +// MARK: - Reactor Extension + +@Reactor +public class CommentReactor { + public typealias Action = NoAction + public struct State { } + public var initialState = State() +} + + + + +func runExtensionMacrosPlayground() { + + let reactor = CommentReactor() + print("CommentReactor: \(reactor)") + +} diff --git a/Sources/Macros/Playground/main.swift b/Sources/Macros/Playground/main.swift index 09ea0e6..6fd914e 100644 --- a/Sources/Macros/Playground/main.swift +++ b/Sources/Macros/Playground/main.swift @@ -22,6 +22,11 @@ runComplexMacorsPlayground() runExressionMacrosPlayground() +// MARK: - Extension Macros + +runExtensionMacrosPlayground() + + // MARK: - MemberAttribute Macros runMemberAttributeMacrosPlayground() diff --git a/Sources/Macros/Helper/Extensions/AttributeListDecl+Ext.swift b/Sources/Macros/SyntaxHelper/Extensions/AttributeListDecl+Ext.swift similarity index 100% rename from Sources/Macros/Helper/Extensions/AttributeListDecl+Ext.swift rename to Sources/Macros/SyntaxHelper/Extensions/AttributeListDecl+Ext.swift diff --git a/Sources/Macros/SyntaxHelper/Extensions/DeclGroupSyntax+Ext.swift b/Sources/Macros/SyntaxHelper/Extensions/DeclGroupSyntax+Ext.swift new file mode 100644 index 0000000..3c4f86d --- /dev/null +++ b/Sources/Macros/SyntaxHelper/Extensions/DeclGroupSyntax+Ext.swift @@ -0,0 +1,32 @@ +// +// File.swift +// +// +// Created by 김건우 on 6/2/24. +// + +import SwiftSyntax + +public extension DeclGroupSyntax { + + // 문서 작성 + + func getAccessControl() -> DeclModifierSyntax { + modifiers + .first(where: { modifier in + let tokenKind = modifier.name.tokenKind + return tokenKind == .keyword(.open) || + tokenKind == .keyword(.public) || + tokenKind == .keyword(.internal) || + tokenKind == .keyword(.fileprivate) || + tokenKind == .keyword(.private) || + tokenKind == .keyword(.package) + }) + // 접근 제어자를 찾을 수 없다면 internal 접근 제어자 반환 + ?? DeclModifierSyntax( + name: .keyword(.internal), + trailingTrivia: [.spaces(1)] + ) + } + +} diff --git a/Sources/Macros/Helper/Extensions/String+Ext.swift b/Sources/Macros/SyntaxHelper/Extensions/String+Ext.swift similarity index 86% rename from Sources/Macros/Helper/Extensions/String+Ext.swift rename to Sources/Macros/SyntaxHelper/Extensions/String+Ext.swift index 43451e2..8d3d0df 100644 --- a/Sources/Macros/Helper/Extensions/String+Ext.swift +++ b/Sources/Macros/SyntaxHelper/Extensions/String+Ext.swift @@ -14,7 +14,7 @@ public extension String { /// - Returns: String /// - Author: 김소월 /// - func capitalizeFirstLetter() -> String { + func makeFirstLetterLowercase() -> String { return prefix(1).uppercased() + dropFirst() } diff --git a/Sources/Macros/Helper/Extensions/VariableDecl+Ext.swift b/Sources/Macros/SyntaxHelper/Extensions/VariableDecl+Ext.swift similarity index 100% rename from Sources/Macros/Helper/Extensions/VariableDecl+Ext.swift rename to Sources/Macros/SyntaxHelper/Extensions/VariableDecl+Ext.swift diff --git a/Tests/Macros/Accessor/DepdencyValueMacroTests.swift b/Tests/Macros/Accessor/DepdencyValueMacroTests.swift index fcd9790..8dcc839 100644 --- a/Tests/Macros/Accessor/DepdencyValueMacroTests.swift +++ b/Tests/Macros/Accessor/DepdencyValueMacroTests.swift @@ -11,7 +11,7 @@ import MacrosImplementation import XCTest fileprivate let testMacros: [String: Macro.Type] = [ - "DependencyValue": DependecyValueMacro.self + "DependencyValue": DependencyValueMacro.self ] final class DepdencyValueMacroTests: XCTestCase { diff --git a/Tests/Macros/Extension/ReactorMacroTests.swift b/Tests/Macros/Extension/ReactorMacroTests.swift new file mode 100644 index 0000000..a8f9443 --- /dev/null +++ b/Tests/Macros/Extension/ReactorMacroTests.swift @@ -0,0 +1,39 @@ +// +// File.swift +// +// +// Created by 김건우 on 6/5/24. +// + +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import MacrosImplementation +import XCTest +import SyntaxHelper + +fileprivate let testMacros: [String: Macro.Type] = [ + "Reactor": ReactorMacro.self +] + +final class ReactorMacroTests: XCTestCase { + + func testReactorMacro() throws { + + assertMacroExpansion( + """ + @Reactor + class testReactor { } + """, + expandedSource: + """ + class testReactor { } + + extension testReactor: Reactor { + } + """, + macros: testMacros + ) + + } + +}