Skip to content

Commit

Permalink
Generate required initializer via FixIt instead of code-generation (#104
Browse files Browse the repository at this point in the history
)
  • Loading branch information
dfed committed Jul 23, 2024
1 parent fbf0df1 commit 4506d05
Show file tree
Hide file tree
Showing 16 changed files with 829 additions and 431 deletions.
5 changes: 3 additions & 2 deletions Examples/ExamplePackageIntegration/Package.resolved
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"originHash" : "4828c1967a8becd3449664c8ced8013fc851b1b749c97f36a6b6e17c68977693",
"pins" : [
{
"identity" : "jjliso8601dateformatter",
Expand Down Expand Up @@ -30,7 +31,7 @@
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"location" : "https://github.com/swiftlang/swift-syntax.git",
"state" : {
"revision" : "fa8f95c2d536d6620cc2f504ebe8a6167c9fc2dd",
"version" : "510.0.1"
Expand All @@ -55,5 +56,5 @@
}
}
],
"version" : 2
"version" : 3
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ import SharedModule

@Instantiable
public final class ChildA: Instantiable {
public init(shared: SharedThing, grandchildA: GrandchildA) {
self.shared = shared
self.grandchildA = grandchildA
}

@Received let shared: SharedThing

@Instantiated let grandchildA: GrandchildA
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ import SharedModule

@Instantiable
public final class ChildB: Instantiable {
public init(shared: SharedThing, grandchildB: GrandchildB) {
self.shared = shared
self.grandchildB = grandchildB
}

@Received let shared: SharedThing

@Instantiated let grandchildB: GrandchildB
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ import SharedModule

@Instantiable
public final class ChildC: Instantiable {
public init(shared: SharedThing, grandchildC: GrandchildC) {
self.shared = shared
self.grandchildC = grandchildC
}

@Received let shared: SharedThing

@Instantiated let grandchildC: GrandchildC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,9 @@ import SharedModule

@Instantiable
public final class GrandchildA: Instantiable {
public init(shared: SharedThing) {
self.shared = shared
}

@Received let shared: SharedThing
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,9 @@ import SharedModule

@Instantiable
public actor GrandchildB: Instantiable {
public init(shared: SharedThing) {
self.shared = shared
}

@Received let shared: SharedThing
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,9 @@ import SharedModule

@Instantiable
public struct GrandchildC: Instantiable {
public init(shared: SharedThing) {
self.shared = shared
}

@Received let shared: SharedThing
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ import SharedModule

@Instantiable
public final class Root: Instantiable {
public init(childA: ChildA, childB: ChildB, childC: ChildC, shared: SharedThing, userDefaults: UserDefaults) {
self.childA = childA
self.childB = childB
self.childC = childC
self.shared = shared
self.userDefaults = userDefaults
}

static let shared = Root()

@Instantiated let childA: ChildA
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@
import SafeDI

@Instantiable
public final class SharedThing: Instantiable {}
public final class SharedThing: Instantiable {
public init() {}
}
35 changes: 32 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,7 @@ import SafeDI

@Instantiable
public final class UserService: Instantiable {

/// Public, memberwise initializer that takes each injected property.
/// This initializer can be auto-generated by SafeDI if it isn‘t manually written.
public init(authService: AuthService, securePersistentStorage: SecurePersistentStorage) {
self.authService = authService
self.securePersistentStorage = securePersistentStorage
Expand Down Expand Up @@ -171,6 +169,11 @@ Here we have a `LoggedInContentView` whose forwarded `user` property is received
```swift
@Instantiable
public struct LoggedInContentView: View, Instantiable {
public init(user: User, profileViewBuilder: ErasedInstantiator<(), AnyView>) {
self.user = user
self.profileViewBuilder = profileViewBuilder
}

public var body: some View {
... // Instantiates and displays a ProfileView when a button is pressed.
}
Expand All @@ -182,6 +185,10 @@ public struct LoggedInContentView: View, Instantiable {

@Instantiable
public struct ProfileView: View, Instantiable {
public init(updateUserService: UpdateUserService) {
self.updateUserService = updateUserService
}

public var body: some View {
... // Allows for updating user information.
}
Expand Down Expand Up @@ -238,6 +245,11 @@ import SwiftUI

@Instantiable
public struct LoggedInView: View, Instantiable {
public init(userManager: UserManager, profileViewBuilder: Instantiator<ProfileView>) {
self.userManager = userManager
self.profileViewBuilder = profileViewBuilder
}

public var body: some View {
... // A logged in user experience
}
Expand All @@ -249,6 +261,11 @@ public struct LoggedInView: View, Instantiable {

@Instantiable
public struct ProfileView: View, Instantiable {
public init(userVendor: UserVendor, editProfileViewBuilder: Instantiator<EditProfileView>) {
self.userVendor = userVendor
self.editProfileViewBuilder = editProfileViewBuilder
}

public var body: some View {
... // A profile viewing experience
}
Expand All @@ -260,6 +277,10 @@ public struct ProfileView: View, Instantiable {

@Instantiable
public struct EditProfileView: View, Instantiable {
public init(userVendor: UserVendor) {
self.userVendor = userVendor
}

public var body: some View {
... // A profile editing experience
}
Expand All @@ -279,6 +300,10 @@ The [`Instantiator`](Sources/SafeDI/DelayedInstantiation/Instantiator.swift) typ
```swift
@Instantiable
public struct MyApp: App, Instantiable {
public init(contentViewInstantiator: Instantiator<ContentView>) {
self.contentViewInstantiator = contentViewInstantiator
}

public var body: some Scene {
WindowGroup {
// Returns a new instance of a `ContentView`.
Expand Down Expand Up @@ -308,6 +333,10 @@ import SwiftUI

@Instantiable
public struct ParentView: View, Instantiable {
public init(childViewBuilder: ErasedInstantiator<(), AnyView>) {
self.childViewBuilder = childViewBuilder
}

public var body: some View {
VStack {
Text("Child View")
Expand Down Expand Up @@ -382,7 +411,7 @@ To install the SafeDI framework into your package with [Swift Package Manager](h

```swift
dependencies: [
.package(url: "https://github.com/dfed/SafeDI", from: "0.7.0"),
.package(url: "https://github.com/dfed/SafeDI", from: "0.8.0"),
]
```

Expand Down
17 changes: 13 additions & 4 deletions Sources/SafeDICore/Errors/FixableInstantiableError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ public enum FixableInstantiableError: DiagnosticError {
case dependencyHasTooManyAttributes
case dependencyHasInitializer
case missingPublicOrOpenAttribute
case missingRequiredInitializer(hasInjectableProperties: Bool)
case missingRequiredInitializer(MissingInitializer)

public enum MissingInitializer {
case hasOnlyInjectableProperties
case hasInjectableAndNotInjectableProperties
case hasNoInjectableProperties
}

public var description: String {
switch self {
Expand All @@ -55,10 +61,13 @@ public enum FixableInstantiableError: DiagnosticError {
"Dependency must not have hand-written initializer"
case .missingPublicOrOpenAttribute:
"@\(InstantiableVisitor.macroName)-decorated type must be `public` or `open`"
case let .missingRequiredInitializer(hasInjectableProperties):
if hasInjectableProperties {
case let .missingRequiredInitializer(missingInitializer):
switch missingInitializer {
case .hasOnlyInjectableProperties:
"@\(InstantiableVisitor.macroName)-decorated type must have a `public` or `open` initializer with a parameter for each @\(Dependency.Source.instantiatedRawValue), @\(Dependency.Source.receivedRawValue), or @\(Dependency.Source.forwardedRawValue)-decorated property."
case .hasInjectableAndNotInjectableProperties:
"@\(InstantiableVisitor.macroName)-decorated type must have a `public` or `open` initializer with a parameter for each @\(Dependency.Source.instantiatedRawValue), @\(Dependency.Source.receivedRawValue), or @\(Dependency.Source.forwardedRawValue)-decorated property. Parameters in this initializer that do not correspond to a decorated property must have default values."
} else {
case .hasNoInjectableProperties:
"@\(InstantiableVisitor.macroName)-decorated type with no @\(Dependency.Source.instantiatedRawValue), @\(Dependency.Source.receivedRawValue), or @\(Dependency.Source.forwardedRawValue)-decorated properties must have a `public` or `open` initializer that either takes no parameters or has a default value for each parameter."
}
}
Expand Down
5 changes: 4 additions & 1 deletion Sources/SafeDICore/Models/Initializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,10 @@ public struct Initializer: Codable, Hashable, Sendable {
leadingTrivia: .space,
trailingTrivia: .space
),
rightOperand: DeclReferenceExprSyntax(baseName: TokenSyntax.identifier(dependency.property.label))
rightOperand: DeclReferenceExprSyntax(
baseName: TokenSyntax.identifier(dependency.property.label),
trailingTrivia: dependency == dependencies.last ? .newline : nil
)
)))
)
}
Expand Down
5 changes: 4 additions & 1 deletion Sources/SafeDICore/Models/Property.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ public struct Property: Codable, Hashable, Comparable, Sendable {
))
if let attributes {
for attribute in attributes {
AttributeSyntax(attributeName: IdentifierTypeSyntax(name: .identifier(attribute)))
AttributeSyntax(
attributeName: IdentifierTypeSyntax(name: .identifier(attribute)),
trailingTrivia: .space
)
}
}
},
Expand Down
7 changes: 3 additions & 4 deletions Sources/SafeDICore/Visitors/InstantiableVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ public final class InstantiableVisitor: SyntaxVisitor {

public override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
guard
declarationType.isTypeDefinition
&& node.modifiers.staticModifier == nil
declarationType.isTypeDefinition,
node.modifiers.staticModifier == nil
else {
return .skipChildren
}
// Check attributes and extract dependency source.
let dependencySources = node.attributes.dependencySources
guard dependencySources.isEmpty || dependencySources.count == 1 else {
if dependencySources.count > 1 {
diagnostics.append(Diagnostic(
node: node.attributes,
error: FixableInstantiableError.dependencyHasTooManyAttributes,
Expand All @@ -51,7 +51,6 @@ public final class InstantiableVisitor: SyntaxVisitor {
),
]
))
return .skipChildren
}
guard let dependencySource = dependencySources.first?.source else {
// This dependency is not part of the DI system.
Expand Down
39 changes: 25 additions & 14 deletions Sources/SafeDIMacros/Macros/InstantiableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,29 @@ public struct InstantiableMacro: MemberMacro {
.contains(where: { $0.isValid(forFulfilling: visitor.dependencies) })
guard hasMemberwiseInitializerForInjectableProperties else {
if visitor.uninitializedNonOptionalPropertyNames.isEmpty {
var initializer = Initializer.generateRequiredInitializer(
for: visitor.dependencies,
declarationType: concreteDeclaration.declType
var membersWithInitializer = declaration.memberBlock.members
membersWithInitializer.insert(
MemberBlockItemSyntax(
leadingTrivia: .newline,
decl: Initializer.generateRequiredInitializer(
for: visitor.dependencies,
declarationType: concreteDeclaration.declType
),
trailingTrivia: .newline
),
at: membersWithInitializer.startIndex
)
initializer.leadingTrivia = Trivia(stringLiteral: """
// A generated initializer that has one argument per SafeDI-injected property.
// Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros.
// As a result, this initializer can not be used within a #Preview macro closure.
// This initializer is generated only because you have not written an appropriate initializer yourself.
// Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros.
""")
return [DeclSyntax(initializer)]
+ generateForwardedProperties(from: forwardedProperties)
context.diagnose(Diagnostic(
node: Syntax(declaration.memberBlock),
error: FixableInstantiableError.missingRequiredInitializer(.hasOnlyInjectableProperties),
changes: [
.replace(
oldNode: Syntax(declaration.memberBlock.members),
newNode: Syntax(membersWithInitializer)
),
]
))
return []
} else {
var membersWithInitializer = declaration.memberBlock.members
membersWithInitializer.insert(
Expand All @@ -137,7 +146,9 @@ public struct InstantiableMacro: MemberMacro {
// TODO: Create separate fixit if just `public` or `open` are missing.
context.diagnose(Diagnostic(
node: Syntax(declaration.memberBlock),
error: FixableInstantiableError.missingRequiredInitializer(hasInjectableProperties: !visitor.dependencies.isEmpty),
error: FixableInstantiableError.missingRequiredInitializer(
visitor.dependencies.isEmpty ? .hasNoInjectableProperties : .hasInjectableAndNotInjectableProperties
),
changes: [
.replace(
oldNode: Syntax(declaration.memberBlock.members),
Expand Down
Loading

0 comments on commit 4506d05

Please sign in to comment.