-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added an Injectable Protocol and Macro for simpler Injecting (#10)
- Loading branch information
Showing
14 changed files
with
465 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import Foundation | ||
|
||
/// This protocol is used to create a detached injectable component without needing a dependency module. | ||
/// This is most likely used with the `@Injectable` macro, which will create the inject function and define it for you | ||
public protocol Injectable { | ||
static func inject() throws -> Self | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import Foundation | ||
|
||
// These are the definition of the two macros, as explained here https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/#Macro-Declarations | ||
|
||
/// The `@Injectable` macro is used to conform to `Injectable` and add a memberwise init and static default method | ||
@attached(extension, conformances: Injectable) | ||
@attached(member, names: named(inject), named(init)) | ||
public macro Injectable() = #externalMacro(module: "WhoopDIKitMacros", type: "InjectableMacro") | ||
|
||
/// The `@InjectableName` macro is used as a marker for the `@Injectable` protocol to add a `WhoopDI.inject(name)` in the inject method | ||
@attached(peer) | ||
public macro InjectableName(name: String) = #externalMacro(module: "WhoopDIKitMacros", type: "InjectableNameMacro") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import SwiftSyntax | ||
import SwiftSyntaxBuilder | ||
import SwiftSyntaxMacros | ||
import SwiftSyntaxMacroExpansion | ||
|
||
struct InjectableMacro: ExtensionMacro, MemberMacro { | ||
/// Adds the `inject` and `init` function that we use for the `Injectable` protocol | ||
static func expansion(of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] { | ||
// We only want to work for classes, structs, and actors | ||
guard [SwiftSyntax.SyntaxKind.classDecl, .structDecl, .actorDecl].contains(declaration.kind) else { | ||
throw MacroExpansionErrorMessage("@Injectable needs to be declared on a concrete type, not a protocol") | ||
} | ||
|
||
let allVariables = declaration.allMemberVariables | ||
|
||
// Create the initializer args in the form `name: type = default` | ||
let initializerArgs: String = allVariables.map { variable in | ||
"\(variable.name): \(variable.type)\(variable.defaultExpression.map { " = \($0)" } ?? "")" | ||
}.joined(separator: ", ") | ||
|
||
// Creates the intitializer body | ||
let initializerStoring: String = allVariables.map { variable in | ||
"self.\(variable.name) = \(variable.name)" | ||
}.joined(separator: "\n") | ||
|
||
// Creates the whoopdi calls in the `inject` func | ||
let injectingVariables: String = allVariables.map { variable in | ||
"\(variable.name): WhoopDI.inject(\(variable.injectedName.map { "\"\($0)\"" } ?? "nil"))" | ||
}.joined(separator: ", ") | ||
|
||
let accessLevel = self.accessLevel(declaration: declaration) ?? "internal" | ||
|
||
return [ | ||
/// Adds the static inject function, such as: | ||
/// public static func inject() -> Self { | ||
/// Self.init(myValue: WhoopDI.inject(nil)) | ||
/// } | ||
""" | ||
\(raw: accessLevel) static func inject() -> Self { | ||
Self.init(\(raw: injectingVariables)) | ||
} | ||
""", | ||
/// Adds the memberwise init, such as: | ||
/// public init(myValue: String) { | ||
/// self.myValue = myValue | ||
/// } | ||
""" | ||
\(raw: accessLevel) init(\(raw: initializerArgs)) { | ||
\(raw: initializerStoring) | ||
} | ||
""" | ||
] | ||
} | ||
|
||
static func expansion(of node: SwiftSyntax.AttributeSyntax, | ||
attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, | ||
providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, | ||
conformingTo protocols: [SwiftSyntax.TypeSyntax], | ||
in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.ExtensionDeclSyntax] { | ||
guard [SwiftSyntax.SyntaxKind.classDecl, .structDecl, .actorDecl].contains(declaration.kind) else { | ||
throw MacroExpansionErrorMessage("@Injectable needs to be declared on a concrete type, not a protocol") | ||
} | ||
// Creates the extension to be Injectable (needs to be separate from member macro because member macros can't add protocols) | ||
guard let identified = declaration.asProtocol(NamedDeclSyntax.self) else { return [] } | ||
let name = identified.name | ||
let extensionSyntax: ExtensionDeclSyntax = try ExtensionDeclSyntax("extension \(name): Injectable { }") | ||
|
||
return [ | ||
extensionSyntax | ||
] | ||
} | ||
|
||
// Gets the access level fo the top level type | ||
private static func accessLevel(declaration: some DeclGroupSyntax) -> String? { | ||
switch declaration { | ||
case let decl as StructDeclSyntax: | ||
return decl.modifiers.accessModifier | ||
case let decl as ClassDeclSyntax: | ||
return decl.modifiers.accessModifier | ||
case let decl as ActorDeclSyntax: | ||
return decl.modifiers.accessModifier | ||
default: | ||
fatalError() | ||
} | ||
} | ||
} | ||
|
||
extension AttributeSyntax.Arguments { | ||
// Get the first string literal in the argument list to the macro | ||
var labeledContent: String? { | ||
switch self { | ||
case let .argumentList(strList): | ||
strList.compactMap { str in | ||
str.expression.as(StringLiteralExprSyntax.self)?.segments.compactMap { (segment) -> String? in | ||
return switch segment { | ||
case .stringSegment(let segment): segment.content.text | ||
default: nil | ||
} | ||
}.joined() | ||
}.first | ||
default: | ||
nil | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import SwiftSyntax | ||
import SwiftSyntaxMacros | ||
|
||
/// This macro is just to have a marker, it does not actually do anything without the `@Injectable` macro | ||
struct InjectableNameMacro: PeerMacro { | ||
static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] { | ||
[] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import SwiftCompilerPlugin | ||
import SwiftSyntaxMacros | ||
|
||
@main | ||
struct WhoopDIKitPlugin: CompilerPlugin { | ||
let providingMacros: [Macro.Type] = [ | ||
InjectableMacro.self, | ||
InjectableNameMacro.self | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import SwiftSyntax | ||
|
||
enum AccessControlType: String { | ||
case `public` | ||
case `private` | ||
case `internal` | ||
case `fileprivate` | ||
} | ||
|
||
extension DeclModifierListSyntax { | ||
var accessModifier: String? { | ||
return compactMap { modifier in | ||
AccessControlType(rawValue: modifier.name.text)?.rawValue | ||
}.first | ||
} | ||
} |
70 changes: 70 additions & 0 deletions
70
Sources/WhoopDIKitMacros/Support/VariableDeclSyntax+Injectable.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import SwiftSyntax | ||
import SwiftSyntaxBuilder | ||
import SwiftSyntaxMacros | ||
import SwiftSyntaxMacroExpansion | ||
|
||
extension VariableDeclSyntax { | ||
/// Determine whether this variable has the syntax of a stored property. | ||
/// | ||
/// This syntactic check cannot account for semantic adjustments due to, | ||
/// e.g., accessor macros or property wrappers. | ||
/// taken from https://github.com/apple/swift-syntax/blob/main/Examples/Sources/MacroExamples/Implementation/MemberAttribute/WrapStoredPropertiesMacro.swift | ||
var isStoredProperty: Bool { | ||
if bindings.count != 1 { | ||
return false | ||
} | ||
|
||
let binding = bindings.first! | ||
switch binding.accessorBlock?.accessors { | ||
case .none: | ||
return true | ||
|
||
case .accessors(let accessors): | ||
for accessor in accessors { | ||
switch accessor.accessorSpecifier.tokenKind { | ||
case .keyword(.willSet), .keyword(.didSet): | ||
// Observers can occur on a stored property. | ||
break | ||
|
||
default: | ||
// Other accessors make it a computed property. | ||
return false | ||
} | ||
} | ||
|
||
return true | ||
|
||
case .getter: | ||
return false | ||
} | ||
} | ||
|
||
// Check if the token is a let and if there is a value in the initializer | ||
var isLetWithValue: Bool { | ||
self.bindingSpecifier.tokenKind == .keyword(.let) && bindings.first?.initializer != nil | ||
} | ||
|
||
// Check if the modifiers have lazy or static, in which case we wouldn't add it to the init | ||
var isStaticOrLazy: Bool { | ||
self.modifiers.contains { syntax in | ||
syntax.name.tokenKind == .keyword(.static) || syntax.name.tokenKind == .keyword(.lazy) | ||
} | ||
} | ||
|
||
var variableName: String? { | ||
self.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text | ||
} | ||
|
||
var typeName: TypeSyntax? { | ||
guard let annotationType = self.bindings.first?.typeAnnotation?.type.trimmed else { return nil } | ||
if (annotationType.is(FunctionTypeSyntax.self)) { | ||
return "@escaping \(annotationType)" | ||
} else { | ||
return annotationType | ||
} | ||
} | ||
|
||
var isInstanceAssignableVariable: Bool { | ||
return !isStaticOrLazy && !isLetWithValue && isStoredProperty | ||
} | ||
} |
50 changes: 50 additions & 0 deletions
50
Sources/WhoopDIKitMacros/Support/VariableDeclaration.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import SwiftSyntax | ||
import SwiftSyntaxBuilder | ||
import SwiftSyntaxMacros | ||
import SwiftSyntaxMacroExpansion | ||
|
||
struct VariableDeclaration { | ||
let name: String | ||
let type: TypeSyntax | ||
let defaultExpression: ExprSyntax? | ||
let injectedName: String? | ||
} | ||
|
||
extension DeclGroupSyntax { | ||
var allMemberVariables: [VariableDeclaration] { | ||
let allMembers = self.memberBlock.members | ||
// Go through all members and return valid variable declarations when needed | ||
return allMembers.compactMap { (memberBlock) -> VariableDeclaration? in | ||
// Only do this for stored properties that are not `let` with a value (since those are constant) | ||
guard let declSyntax = memberBlock.decl.as(VariableDeclSyntax.self), | ||
let propertyName = declSyntax.variableName, | ||
let typeName = declSyntax.typeName | ||
else { return nil } | ||
guard declSyntax.isInstanceAssignableVariable else { return nil } | ||
|
||
// If the code has `InjectableName` on it, get the name to use | ||
let injectedName = injectableName(variableSyntax: declSyntax) | ||
|
||
/// Use the equality expression in the initializer as the default value (since that is how the memberwise init works) | ||
/// Example: | ||
/// var myValue: Int = 100 | ||
/// Becomes | ||
/// init(..., myValue: Int = 100) | ||
let equalityExpression = declSyntax.bindings.first?.initializer?.value | ||
return VariableDeclaration(name: propertyName, type: typeName, defaultExpression: equalityExpression, injectedName: injectedName) | ||
} | ||
} | ||
|
||
private func injectableName(variableSyntax: VariableDeclSyntax) -> String? { | ||
variableSyntax.attributes.compactMap { (attribute) -> String? in | ||
switch attribute { | ||
case .attribute(let syntax): | ||
// Check for `InjectableName` and then get the name from it | ||
guard let name = syntax.attributeName.as(IdentifierTypeSyntax.self)?.name.text, | ||
name == "InjectableName" else { return nil } | ||
return syntax.arguments?.labeledContent | ||
default: return nil | ||
} | ||
}.first | ||
} | ||
} |
Oops, something went wrong.