Skip to content

Commit

Permalink
Macros
Browse files Browse the repository at this point in the history
Proposal of using protocols in Injects
  • Loading branch information
ddanielczyk authored Dec 20, 2023
2 parents 715a1ed + 648dc37 commit ef03e98
Show file tree
Hide file tree
Showing 9 changed files with 286 additions and 7 deletions.
2 changes: 1 addition & 1 deletion InjectGrail.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

Pod::Spec.new do |s|
s.name = 'InjectGrail'
s.version = '0.2.6'
s.version = '0.2.7'
s.summary = 'Holy Grail of Swift Injection frameworks for iOS and MacOs.'
s.description = <<-DESC
Tired of injection framework that puts everything in one big bag of dependecy resolvers? This framework might be good for you.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book

/// A macro that produces both a value and a string containing the
/// source code that generated the value. For example,
///
/// #stringify(x + y)
///
/// produces a tuple `(x + y, "x + y")`.

@attached(member, names: named(`injector`), named(init(injector:)))
public macro Needs<T>(noConstructor: Bool = false, noLet: Bool = false) = #externalMacro(module: "InjectGrailMacrosMacros", type: "NeedsMacro")

@attached(member, names: named(`injector`), named(init(injector:)))
public macro NeedsInjector() = #externalMacro(module: "InjectGrailMacrosMacros", type: "NeedsInjectorMacro")

@attached(member)
public macro Injects<each T>() = #externalMacro(module: "InjectGrailMacrosMacros", type: "InjectsMacro")
48 changes: 48 additions & 0 deletions InjectGrailMacros/Sources/InjectGrailMacrosClient/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import InjectGrailMacros

protocol TestProtocol {
}
struct TestProtocolImpl: TestProtocol {

}

@Needs<TestProtocol>
class Test {

}

@Needs<TestProtocol>(noConstructor: true)
class TestNoConstructor {
init(injector: TestProtocolImpl) {
self.injector = injector
}
}


@Needs<TestProtocol>
class TestNoLet {

}

@Needs<TestProtocol>
@Injects<TestNoConstructor,
TestNoLet,
TestNoLet,
TestNoLet,
TestNoLet,
TestNoLet,
TestNoLet,
TestNoLet,
TestNoLet,TestNoLet,TestNoLet,TestNoLet,TestNoLet>
class TestNothing {
let injector: TestProtocolImpl

init(injector: TestProtocolImpl) {
self.injector = injector
}
}

let _ = Test(injector: TestProtocolImpl())
let _ = TestNoConstructor(injector: TestProtocolImpl())
let _ = TestNoLet(injector: TestProtocolImpl())
let _ = TestNothing(injector: TestProtocolImpl())
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// File.swift
//
//
// Created by Łukasz Kwoska on 15/11/2023.
//

import Foundation

public struct InjectGrailError: Error {
public let message: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

public struct NeedsMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard declaration.as(ClassDeclSyntax.self) != nil else {
throw InjectGrailError(message: "@Needs only works on class declaration")
}

guard
let generics = node.attributeName.as(IdentifierTypeSyntax.self)?.genericArgumentClause?.arguments.first?.argument,
let protocolType = generics.as(IdentifierTypeSyntax.self)?.name.text
else {
throw InjectGrailError(message: "@Needs requires Injector protocol")
}

let noLet = declaration.memberBlock.members.first(where: { $0.decl.as(VariableDeclSyntax.self)?.bindings.first(where: { "\($0.pattern)" == "injector" }) != nil}) != nil
let noConstructor = declaration.memberBlock.members.first(where: { $0.decl.as(InitializerDeclSyntax.self) != nil})?.decl.as(InitializerDeclSyntax.self)!.signature.parameterClause.parameters.first?.firstName.text == "injector"


return [
noLet ? nil : DeclSyntax(stringLiteral: "let injector: \(protocolType)Impl"),
noConstructor ? nil : DeclSyntax(stringLiteral: "init(injector: \(protocolType)Impl){ self.injector = injector}")
].compactMap({$0})
}
}

public struct NeedsInjectorMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let classDeclaration = declaration.as(ClassDeclSyntax.self) else {
throw InjectGrailError(message: "@NeedsInjector only works on class declaration")
}

let className = classDeclaration.name.text
let injectorProtocolType = className.replacingOccurrences(of: "Impl", with: "Injector")

let noLet = declaration.memberBlock.members.first(where: { $0.decl.as(VariableDeclSyntax.self)?.bindings.first(where: { "\($0.pattern)" == "injector" }) != nil}) != nil
let noConstructor = declaration.memberBlock.members.first(where: { $0.decl.as(InitializerDeclSyntax.self) != nil})?.decl.as(InitializerDeclSyntax.self)!.signature.parameterClause.parameters.first?.firstName.text == "injector"

return [
noLet ? nil : DeclSyntax(stringLiteral: "let injector: \(injectorProtocolType)Impl"),
noConstructor ? nil : DeclSyntax(stringLiteral: "init(injector: \(injectorProtocolType)Impl){ self.injector = injector}")
].compactMap({$0})
}
}

public struct InjectsMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard declaration.as(ClassDeclSyntax.self) != nil else {
throw InjectGrailError(message: "@Injects only works on class declaration")
}

guard
node.attributeName.as(IdentifierTypeSyntax.self)?.genericArgumentClause?.arguments.allSatisfy({ argument -> Bool in
argument.as(GenericArgumentSyntax.self)?.argument.as(IdentifierTypeSyntax.self) != nil
}) ?? false
else {
throw InjectGrailError(message: "@Injects requires Injected classes")
}


return []
}
}

@main
struct InjectGrailMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
NeedsMacro.self,
NeedsInjectorMacro.self,
InjectsMacro.self
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest

// Macro implementations build for the host, so the corresponding module is not available when cross-compiling. Cross-compiled tests may still make use of the macro itself in end-to-end tests.
#if canImport(InjectGrailMacrosMacros)
import InjectGrailMacrosMacros

let testMacros: [String: Macro.Type] = [
"@Needs": NeedsMacro.self,
]
#endif

final class InjectGrailMacrosTests: XCTestCase {
func testMacro() throws {
#if canImport(InjectGrailMacrosMacros)
assertMacroExpansion(
"""
protocol TestProtocol {
}
@Needs<TestProtocol>
class Test {
}
""",
expandedSource: """
protocol TestProtocol {
}
@Needs<TestProtocol>
class Test {
let injector:TestProtocolImpl
init(injector: TestProtocolImpl) {
self.injector = injector
}
}
""",
macros: testMacros
)
#else
throw XCTSkip("macros are only supported when running tests for the host platform")
#endif
}

func testMacroWithStringLiteral() throws {
#if canImport(InjectGrailMacrosMacros)
assertMacroExpansion(
#"""
protocol TestProtocol {
}
@Needs(TestProtocol)
protocol Test {
}
"""#,
expandedSource: #"""
"""#,
macros: testMacros
)
#else
throw XCTSkip("macros are only supported when running tests for the host platform")
#endif
}
}
14 changes: 14 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036",
"version" : "509.0.2"
}
}
],
"version" : 2
}
34 changes: 31 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,45 @@
// swift-tools-version:5.7
// swift-tools-version:5.9

import PackageDescription
import CompilerPluginSupport

let package = Package(
name: "InjectGrail",
platforms: [
.iOS(.v14)
.iOS(.v14),
.macOS(.v10_15)
],
products: [
.library(name: "InjectGrail", targets: ["InjectGrail"])
.library(name: "InjectGrail", targets: ["InjectGrail"]),
.library(name: "InjectGrailMacros", targets: ["InjectGrailMacros"]),
],
dependencies: [
// Depend on the Swift 5.9 release of SwiftSyntax
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
],
targets: [
.target(name: "InjectGrail", path: "InjectGrail/Classes"),
.target(
name: "InjectGrailMacros",
dependencies: ["InjectGrailMacrosMacros"],
path: "InjectGrailMacros/Sources/InjectGrailMacros"
),
.macro(
name: "InjectGrailMacrosMacros",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
],
path: "InjectGrailMacros/Sources/InjectGrailMacrosMacros"
),
.testTarget(
name: "InjectGrailMacrosTests",
dependencies: [
"InjectGrailMacrosMacros",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
],
path: "InjectGrailMacros/Tests/InjectGrailMacrosTests"
)
],
swiftLanguageVersions: [.v5]
)
14 changes: 11 additions & 3 deletions Templates/Inject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,15 @@ func resolveDependencyTree(injectorProperties: [String: [Property]], injectsToI
}

func extractNeededName(_ type: Type) -> String? {
guard
guard
let attribute = type.attributes["Needs"]?.first
else { return nil }
else {
if type.attributes["NeedsInjector"]?.first != nil {
return type.name.replacingOccurrences(of: "Impl", with: "Injector")
} else {
return nil
}
}

let name = String(describing: attribute)

Expand All @@ -248,6 +254,8 @@ func extractInjects(_ type: Type, _ injectablesToInjectors: [String: String], _
let trimmed = $0.trimmingCharacters(in: .whitespacesAndNewlines)
if let injectedInjector = injectablesToInjectors[trimmed] {
return ["Injects\(injectedInjector)"]
} else if let injectedInjector = injectablesToInjectors[trimmed + "Impl"] {
return ["Injects\(injectedInjector)"]
} else if let injectedInjectors = protocolsToInjectables[trimmed]?.compactMap({ injectablesToInjectors[$0] })
.map({"Injects\($0)"}) {
return injectedInjectors
Expand All @@ -267,7 +275,7 @@ public func protocolName(_ injectable: String, data: InjectData) -> String {
}

public func calculateInjectData() -> InjectData {
let injectables = types.all.filter({ $0.inheritedTypes.contains("Injectable") || $0.attributes["Needs"] != nil})
let injectables = types.all.filter({ $0.inheritedTypes.contains("Injectable") || $0.attributes["Needs"] != nil || $0.attributes["NeedsInjector"] != nil })
let needed = Set(types.all.flatMap { extractNeededName($0) })

let injectors = types.protocols.filter({$0.inheritedTypes.contains("Injector") || needed.contains($0.name) })
Expand Down

0 comments on commit ef03e98

Please sign in to comment.