diff --git a/Package.swift b/Package.swift index b5488b4..e1d7e97 100644 --- a/Package.swift +++ b/Package.swift @@ -7,44 +7,44 @@ import PackageDescription let name = "AutoRegisterable" let package = Package( - name: name, - platforms: [.macOS(.v12), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], - products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. - .library( - name: name, - targets: [name] - ) - ], - dependencies: [ - .package(url: "https://github.com/num42/swift-macrotester.git", from: "1.0.1"), - .package(url: "https://github.com/apple/swift-syntax.git", from: "510.0.1") - ], - targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. - // Macro implementation that performs the source transformation of a macro. - .macro( - name: "\(name)Macros", - dependencies: [ - .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), - .product(name: "SwiftCompilerPlugin", package: "swift-syntax") - ] - ), - // Library that exposes a macro as part of its API, which is used in client programs. - .target( - name: name, - dependencies: [.target(name: "\(name)Macros")] - ), - // A test target used to develop the macro implementation. - .testTarget( - name: "\(name)Tests", - dependencies: [ - .product(name: "MacroTester", package: "swift-macrotester"), - .target(name: "\(name)Macros"), - .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax") - ], - resources: [.copy("Resources")] - ) - ] + name: name, + platforms: [.macOS(.v12), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: name, + targets: [name] + ), + ], + dependencies: [ + .package(url: "https://github.com/num42/swift-macrotester.git", from: "1.0.1"), + .package(url: "https://github.com/apple/swift-syntax.git", from: "510.0.1"), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + // Macro implementation that performs the source transformation of a macro. + .macro( + name: "\(name)Macros", + dependencies: [ + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), + ] + ), + // Library that exposes a macro as part of its API, which is used in client programs. + .target( + name: name, + dependencies: [.target(name: "\(name)Macros")] + ), + // A test target used to develop the macro implementation. + .testTarget( + name: "\(name)Tests", + dependencies: [ + .product(name: "MacroTester", package: "swift-macrotester"), + .target(name: "\(name)Macros"), + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + ], + resources: [.copy("Resources")] + ), + ] ) diff --git a/Sources/AutoRegisterable/AutoRegisterable.swift b/Sources/AutoRegisterable/AutoRegisterable.swift index 91bebe9..857491d 100644 --- a/Sources/AutoRegisterable/AutoRegisterable.swift +++ b/Sources/AutoRegisterable/AutoRegisterable.swift @@ -1,5 +1,5 @@ @attached(member, names: arbitrary) public macro AutoRegisterable() = #externalMacro( - module: "AutoRegisterableMacros", - type: "AutoRegisterableMacro" + module: "AutoRegisterableMacros", + type: "AutoRegisterableMacro" ) diff --git a/Sources/AutoRegisterableMacros/AutoRegisterableMacro.swift b/Sources/AutoRegisterableMacros/AutoRegisterableMacro.swift index 468e44c..1e64008 100644 --- a/Sources/AutoRegisterableMacros/AutoRegisterableMacro.swift +++ b/Sources/AutoRegisterableMacros/AutoRegisterableMacro.swift @@ -4,103 +4,105 @@ import SwiftSyntaxBuilder import SwiftSyntaxMacros public struct AutoRegisterableMacro: MemberMacro { - public static func expansion( - of node: SwiftSyntax.AttributeSyntax, - providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax, - in context: some SwiftSyntaxMacros.MacroExpansionContext - ) throws -> [SwiftSyntax.DeclSyntax] { - let className = declaration.as(ClassDeclSyntax.self)?.name.description ?? declaration.as(StructDeclSyntax.self)!.name.description + public enum MacroError: Error, CustomStringConvertible { + case requiresStructOrClass + case requiresDependencies - let members = ( - declaration - .as(ClassDeclSyntax.self)? - .memberBlock - ?? declaration.as(StructDeclSyntax.self)! - .memberBlock - ) - .members - - let initializers = members.compactMap { $0.decl.as(InitializerDeclSyntax.self) } - - let parametersArray = initializers.map { - $0.signature.parameterClause.parameters - .map { (name: $0.firstName.text, type: $0.type.description) } + public var description: String { + switch self { + case .requiresStructOrClass: + "#AutoRegisterable requires a struct or class" + case .requiresDependencies: + "#AutoRegisterable requires a property using a type called \"Dependencies\"" + } + } } - let dependencyMembers = members - .compactMap { - $0.as(MemberBlockItemSyntax.self)? - .decl.as(StructDeclSyntax.self) - } - .first { - $0.name.text == "Dependencies" - }! - .memberBlock.members - - let patternBindings = dependencyMembers.compactMap { $0.as(MemberBlockItemSyntax.self)? - .decl.as(VariableDeclSyntax.self)? - .bindings - .compactMap { $0.as(PatternBindingSyntax.self)} - } - - - -// let parametersString = dependencyNames -// -// .map { "\($0.name): \($0.type)? = nil" }.joined(separator: ",\n ") -// }.joined(separator: ",\n ") + public static func expansion( + of _: SwiftSyntax.AttributeSyntax, + providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax, + in _: some SwiftSyntaxMacros.MacroExpansionContext + ) throws -> [SwiftSyntax.DeclSyntax] { + guard let objectName = declaration.as(ClassDeclSyntax.self)?.name.description + ?? declaration.as(StructDeclSyntax.self)?.name.description + else { + throw MacroError.requiresStructOrClass + } - let parametersString = - patternBindings.compactMap { $0.compactMap { (String($0.pattern.description), String($0.typeAnnotation!.type.description)) } } - .reduce([], +) - .map { - $0.appending(": \($1)? = nil") - } - .joined(separator: ",\n") - .indentedBy(" ") - - let dependencyNames = patternBindings.compactMap { $0.compactMap { String($0.pattern.description) } } - .reduce([], +) - - let dependenciesString = - dependencyNames.map { - $0.appending(": \($0) ?? (try! container.resolve())") - } - .joined(separator: ",\n") - .indentedBy(" ") + guard let members = ( + declaration + .as(ClassDeclSyntax.self)? + .memberBlock + ?? declaration.as(StructDeclSyntax.self)? + .memberBlock + )? + .members + else { + throw MacroError.requiresStructOrClass + } - return [ - DeclSyntax( - extendedGraphemeClusterLiteral: """ - public static func register( - in container: DependencyContainer, - scope: ComponentScope = .shared, - as type: TargetType.Type = \(className).self, - \(parametersString) - ) { - container.register(scope) { - \(className)( - dependencies: \(className).Dependencies( - \(dependenciesString) - ) - ) as! TargetType - } + guard let dependencyMembers = members + .compactMap({ $0.decl.as(StructDeclSyntax.self) }) + .first(where: { $0.name.text == "Dependencies" }) + else { + throw MacroError.requiresDependencies } - """ - ) - ] - } + + let patternBindings = dependencyMembers + .memberBlock.members + .compactMap { + $0.decl.as(VariableDeclSyntax.self)? + .bindings + .compactMap { $0 } + } + + let parametersString = + patternBindings.compactMap { $0.compactMap { (String($0.pattern.description), String($0.typeAnnotation!.type.description)) } } + .reduce([], +) + .map { $0.appending(": \($1)? = nil") } + .joined(separator: ",\n") + .indentedBy(" ") + + let dependencyNames = patternBindings.compactMap { $0.compactMap { String($0.pattern.description) } } + .reduce([], +) + + let dependenciesString = + dependencyNames.map { $0.appending(": \($0) ?? (try! container.resolve())") } + .joined(separator: ",\n") + .indentedBy(" ") + + return [ + DeclSyntax( + extendedGraphemeClusterLiteral: """ + public static func register( + in container: DependencyContainer, + scope: ComponentScope = .shared, + as type: TargetType.Type = \(objectName).self\(dependencyNames.isEmpty ? "" : ",") + \(parametersString) + ) { + container.register(scope) { + \(objectName)( + dependencies: \(objectName).Dependencies( + \(dependenciesString) + ) + ) as! TargetType + } + } + """ + ), + ] + } } @main struct AutoRegisterablePlugin: CompilerPlugin { - let providingMacros: [Macro.Type] = [ - AutoRegisterableMacro.self - ] + let providingMacros: [Macro.Type] = [ + AutoRegisterableMacro.self, + ] } extension String { - func indentedBy(_ indentation: String) -> String { - split(separator: "\n").joined(separator: "\n" + indentation) - } + func indentedBy(_ indentation: String) -> String { + split(separator: "\n").joined(separator: "\n" + indentation) + } } diff --git a/Tests/AutoRegisterableTests/AutoRegisterableDiagnosticsTests.swift b/Tests/AutoRegisterableTests/AutoRegisterableDiagnosticsTests.swift new file mode 100644 index 0000000..55c0308 --- /dev/null +++ b/Tests/AutoRegisterableTests/AutoRegisterableDiagnosticsTests.swift @@ -0,0 +1,54 @@ +import MacroTester +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +#if canImport(AutoRegisterableMacros) + import AutoRegisterableMacros + + final class AutoRegisterableDiagnosticsTests: XCTestCase { + let testMacros: [String: Macro.Type] = [ + "AutoRegisterable": AutoRegisterableMacro.self, + ] + + func testEnumThrowsError() throws { + assertMacroExpansion( + """ + @AutoRegisterable + enum AnEnum {} + """, + expandedSource: """ + enum AnEnum {} + """, + diagnostics: [ + .init( + message: AutoRegisterableMacro.MacroError.requiresStructOrClass.description, + line: 1, + column: 1 + ), + ], + macros: testMacros + ) + } + + func testStructHasNoDependencies() throws { + assertMacroExpansion( + """ + @AutoRegisterable + struct AStruct {} + """, + expandedSource: """ + struct AStruct {} + """, + diagnostics: [ + .init( + message: AutoRegisterableMacro.MacroError.requiresDependencies.description, + line: 1, + column: 1 + ), + ], + macros: testMacros + ) + } + } +#endif diff --git a/Tests/AutoRegisterableTests/AutoRegisterableTests.swift b/Tests/AutoRegisterableTests/AutoRegisterableTests.swift index cc837d3..fd9dc13 100644 --- a/Tests/AutoRegisterableTests/AutoRegisterableTests.swift +++ b/Tests/AutoRegisterableTests/AutoRegisterableTests.swift @@ -4,20 +4,23 @@ import SwiftSyntaxMacrosTestSupport import XCTest #if canImport(AutoRegisterableMacros) - import AutoRegisterableMacros + import AutoRegisterableMacros - let testMacros: [String: Macro.Type] = [ - "AutoRegisterable": AutoRegisterableMacro.self - ] + final class AutoRegisterableTests: XCTestCase { + let testMacros: [String: Macro.Type] = [ + "AutoRegisterable": AutoRegisterableMacro.self, + ] -final class AutoRegisterableTests: XCTestCase { - func testAutoRegisterableInAppService() throws { - testMacro(macros: testMacros) - } + func testAutoRegisterableInAppService() throws { + testMacro(macros: testMacros) + } - func testAutoRegisterableInFMSServiceProduction() throws { - testMacro(macros: testMacros) - } -} -#endif + func testAutoRegisterableInFMSServiceProduction() throws { + testMacro(macros: testMacros) + } + func testStructWithoutDependencyEntries() throws { + testMacro(macros: testMacros) + } + } +#endif diff --git a/Tests/AutoRegisterableTests/Resources/testStructWithoutDependencyEntries/Input.swift.test b/Tests/AutoRegisterableTests/Resources/testStructWithoutDependencyEntries/Input.swift.test new file mode 100644 index 0000000..ea8ed9c --- /dev/null +++ b/Tests/AutoRegisterableTests/Resources/testStructWithoutDependencyEntries/Input.swift.test @@ -0,0 +1,31 @@ +import Dip +import FMSKit +import GRDB +import Model +import RxSwift +import UIBase +import UIKit.UIApplication + +@AutoRegisterable +public struct AppService { + struct Dependencies {} + + init(dependencies: Dependencies) { + self.dependencies = dependencies + } + + func scheduleSync() { + dependencies.scheduledSync.schedule() + } + + func triggerSync(completion: (() -> Void)? = nil) { + dependencies.scheduledSync.trigger(completion: completion) + } + + func cancelSync() { + dependencies.scheduledSync.cancel() + } + + private let disposeBag = DisposeBag() + private let dependencies: Dependencies +} diff --git a/Tests/AutoRegisterableTests/Resources/testStructWithoutDependencyEntries/Output.swift.test b/Tests/AutoRegisterableTests/Resources/testStructWithoutDependencyEntries/Output.swift.test new file mode 100644 index 0000000..282a521 --- /dev/null +++ b/Tests/AutoRegisterableTests/Resources/testStructWithoutDependencyEntries/Output.swift.test @@ -0,0 +1,44 @@ +import Dip +import FMSKit +import GRDB +import Model +import RxSwift +import UIBase +import UIKit.UIApplication +public struct AppService { + struct Dependencies {} + + init(dependencies: Dependencies) { + self.dependencies = dependencies + } + + func scheduleSync() { + dependencies.scheduledSync.schedule() + } + + func triggerSync(completion: (() -> Void)? = nil) { + dependencies.scheduledSync.trigger(completion: completion) + } + + func cancelSync() { + dependencies.scheduledSync.cancel() + } + + private let disposeBag = DisposeBag() + private let dependencies: Dependencies + + public static func register( + in container: DependencyContainer, + scope: ComponentScope = .shared, + as type: TargetType.Type = AppService .self + + ) { + container.register(scope) { + AppService ( + dependencies: AppService .Dependencies( + + ) + ) as! TargetType + } + } +}