Skip to content

Commit

Permalink
We do not need an initializer for optional properties
Browse files Browse the repository at this point in the history
  • Loading branch information
dfed committed Jan 5, 2024
1 parent e265aff commit 1b77393
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 21 deletions.
30 changes: 30 additions & 0 deletions Sources/SafeDICore/Extensions/PatternBindingSyntaxExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Distributed under the MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import SwiftSyntax

extension PatternBindingSyntax {
var isOptionalAndUninitialized: Bool {
guard let typeAnnotation else {
return initializer == nil
}
return typeAnnotation.type.typeDescription.isOptional
}
}
21 changes: 21 additions & 0 deletions Sources/SafeDICore/Models/TypeDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,27 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
lhs.asSource < rhs.asSource
}

var isOptional: Bool {
switch self {
case .any,
.array,
.attributed,
.closure,
.composition,
.dictionary,
.implicitlyUnwrappedOptional,
.metatype,
.nested,
.simple,
.some,
.tuple,
.unknown:
return false
case .optional:
return true
}
}

var isUnknown: Bool {
switch self {
case .any,
Expand Down
17 changes: 12 additions & 5 deletions Sources/SafeDICore/Visitors/InstantiableVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,16 @@ public final class InstantiableVisitor: SyntaxVisitor {
}
guard let dependencySource = dependencySources.first?.source else {
// This dependency is not part of the DI system.
// If this variable declaration is missing a binding, we need a custom initializer.
let patterns = node.bindings.filter { $0.initializer == nil && $0.accessorBlock == nil }.map(\.pattern)
uninitializedPropertyNames += patterns
// If this variable declaration is missing a binding and is non-optional, we need a custom initializer.
let patterns = node
.bindings
.filter {
$0.initializer == nil
&& $0.accessorBlock == nil
&& !$0.isOptionalAndUninitialized
}
.map(\.pattern)
uninitializedNonOptionalPropertyNames += patterns
.compactMap(IdentifierPatternSyntax.init)
.map(\.identifier.text)
+ patterns
Expand Down Expand Up @@ -298,7 +305,7 @@ public final class InstantiableVisitor: SyntaxVisitor {
public private(set) var instantiableType: TypeDescription?
public private(set) var additionalInstantiableTypes: [TypeDescription]?
public private(set) var diagnostics = [Diagnostic]()
public private(set) var uninitializedPropertyNames = [String]()
public private(set) var uninitializedNonOptionalPropertyNames = [String]()

public static let macroName = "Instantiable"
public static let instantiateMethodName = "instantiate"
Expand Down Expand Up @@ -429,7 +436,7 @@ public final class InstantiableVisitor: SyntaxVisitor {
}

private func initializerToGenerate() -> Initializer? {
guard uninitializedPropertyNames.isEmpty else {
guard uninitializedNonOptionalPropertyNames.isEmpty else {
// There's an uninitialized property, so we can't generate an initializer.
return nil
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/SafeDIMacros/Macros/InstantiableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public struct InstantiableMacro: MemberMacro {
.initializers
.contains(where: { $0.isValid(forFulfilling: visitor.dependencies) })
guard hasMemberwiseInitializerForInjectableProperties else {
if visitor.uninitializedPropertyNames.isEmpty {
if visitor.uninitializedNonOptionalPropertyNames.isEmpty {
var initializer = Initializer.generateRequiredInitializer(for: visitor.dependencies)
initializer.leadingTrivia = Trivia(stringLiteral: """
// A generated initializer that has one argument per SafeDI-injected property.
Expand All @@ -74,7 +74,7 @@ public struct InstantiableMacro: MemberMacro {
leadingTrivia: .newline,
decl: Initializer.generateRequiredInitializer(
for: visitor.dependencies,
andAdditionalPropertiesWithLabels: visitor.uninitializedPropertyNames
andAdditionalPropertiesWithLabels: visitor.uninitializedNonOptionalPropertyNames
),
trailingTrivia: .newline
),
Expand Down
59 changes: 45 additions & 14 deletions Tests/SafeDIMacrosTests/InstantiableMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,20 +142,20 @@ final class InstantiableMacroTests: XCTestCase {
@Instantiable
public struct ExampleService {
@Instantiated
let receivedA: ReceivedA
let instantiatedA: InstantiatedA
public init(receivedA: ReceivedA) {
self.receivedA = receivedA
public init(instantiatedA: InstantiatedA) {
self.instantiatedA = instantiatedA
}
}
"""
} expansion: {
"""
public struct ExampleService {
let receivedA: ReceivedA
let instantiatedA: InstantiatedA
public init(receivedA: ReceivedA) {
self.receivedA = receivedA
public init(instantiatedA: InstantiatedA) {
self.instantiatedA = instantiatedA
}
}
"""
Expand All @@ -168,21 +168,21 @@ final class InstantiableMacroTests: XCTestCase {
@Instantiable
public struct ExampleService {
@Instantiated
let receivedA: ReceivedA
let instantiatedA: InstantiatedA
}
"""
} expansion: {
"""
public struct ExampleService {
let receivedA: ReceivedA
let instantiatedA: InstantiatedA
// 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 only generated because you have not written this macro yourself.
// Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros.
public init(receivedA: ReceivedA) {
self.receivedA = receivedA
public init(instantiatedA: InstantiatedA) {
self.instantiatedA = instantiatedA
}
}
"""
Expand All @@ -195,15 +195,15 @@ final class InstantiableMacroTests: XCTestCase {
@Instantiable
public struct ExampleService {
@Instantiated
let receivedA: ReceivedA
let instantiatedA: InstantiatedA
let initializedProperty = 5
}
"""
} expansion: {
"""
public struct ExampleService {
let receivedA: ReceivedA
let instantiatedA: InstantiatedA
let initializedProperty = 5
Expand All @@ -212,8 +212,39 @@ final class InstantiableMacroTests: XCTestCase {
// As a result, this initializer can not be used within a #Preview macro closure.
// This initializer is only generated because you have not written this macro yourself.
// Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros.
public init(receivedA: ReceivedA) {
self.receivedA = receivedA
public init(instantiatedA: InstantiatedA) {
self.instantiatedA = instantiatedA
}
}
"""
}
}

func test_declaration_generatesRequiredInitializerWithDependenciesWhenPropertyIsOptional() {
assertMacro {
"""
@Instantiable
public struct ExampleService {
@Instantiated
let instantiatedA: InstantiatedA
var optionalProperty: Int?
}
"""
} expansion: {
"""
public struct ExampleService {
let instantiatedA: InstantiatedA
var optionalProperty: Int?
// 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 only generated because you have not written this macro yourself.
// Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros.
public init(instantiatedA: InstantiatedA) {
self.instantiatedA = instantiatedA
}
}
"""
Expand Down

0 comments on commit 1b77393

Please sign in to comment.