Skip to content

Commit

Permalink
Merge pull request #1 from num42/feature/add_diagnostics_and_allow_em…
Browse files Browse the repository at this point in the history
…pty_dependency_structs

Feature/add diagnostics and allow empty dependency structs
  • Loading branch information
Lutzifer authored Apr 22, 2024
2 parents 15e5141 + dafaab5 commit 84a95cf
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 141 deletions.
80 changes: 40 additions & 40 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
),
]
)
4 changes: 2 additions & 2 deletions Sources/AutoRegisterable/AutoRegisterable.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@attached(member, names: arbitrary)
public macro AutoRegisterable() = #externalMacro(
module: "AutoRegisterableMacros",
type: "AutoRegisterableMacro"
module: "AutoRegisterableMacros",
type: "AutoRegisterableMacro"
)
174 changes: 88 additions & 86 deletions Sources/AutoRegisterableMacros/AutoRegisterableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<TargetType>(
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<TargetType>(
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)
}
}
54 changes: 54 additions & 0 deletions Tests/AutoRegisterableTests/AutoRegisterableDiagnosticsTests.swift
Original file line number Diff line number Diff line change
@@ -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
29 changes: 16 additions & 13 deletions Tests/AutoRegisterableTests/AutoRegisterableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit 84a95cf

Please sign in to comment.