Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Plugins/Shared.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import PackagePlugin
// As of Xcode 15.0, Xcode command plugins have no way to read the package manifest, therefore we must hardcode the version number.
// It is okay for this number to be behind the most current release if the inputs and outputs to SafeDITool have not changed.
// Unlike SPM plugins, Xcode plugins can not determine the current version number, so we must hardcode it.
"1.5.1"
"1.5.2"
}

var safeDIOrigin: URL {
Expand Down
2 changes: 1 addition & 1 deletion SafeDI.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'SafeDI'
s.version = '1.5.1'
s.version = '1.5.2'
s.summary = 'Compile-time-safe dependency injection'
s.homepage = 'https://github.com/dfed/SafeDI'
s.license = 'MIT'
Expand Down
72 changes: 42 additions & 30 deletions Sources/SafeDICore/Generators/ScopeGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,35 @@ actor ScopeGenerator: CustomStringConvertible, Sendable {
}
}
)
onlyIfAvailableUnwrappedReceivedProperties = Set(
propertiesToGenerate.flatMap { [propertiesToDeclare, scopeData] propertyToGenerate in
propertyToGenerate.onlyIfAvailableUnwrappedReceivedProperties
.subtracting(propertiesToDeclare)
.subtracting(scopeData.forwardedProperties)
}
)
.union(
instantiable
.dependencies
.compactMap {
switch $0.source {
case .instantiated, .forwarded:
nil
case let .received(onlyIfAvailable):
if onlyIfAvailable {
$0.property.asUnwrappedProperty
} else {
nil
}
case let .aliased(fulfillingProperty, _, onlyIfAvailable):
if onlyIfAvailable {
fulfillingProperty.asUnwrappedProperty
} else {
nil
}
}
}
)
}

init(
Expand All @@ -95,6 +124,11 @@ actor ScopeGenerator: CustomStringConvertible, Sendable {
onlyIfAvailable: onlyIfAvailable
)
receivedProperties = [fulfillingProperty]
onlyIfAvailableUnwrappedReceivedProperties = if onlyIfAvailable {
[fulfillingProperty.asUnwrappedProperty]
} else {
[]
}
description = property.asSource
propertiesToGenerate = []
propertiesToDeclare = []
Expand Down Expand Up @@ -370,6 +404,8 @@ actor ScopeGenerator: CustomStringConvertible, Sendable {
private let scopeData: ScopeData
/// Properties that we require in order to satisfy our (and our children’s) dependencies.
private let receivedProperties: Set<Property>
/// Unwrapped versions of received properties from transitive `@Received(onlyIfAvailable: true)` dependencies.
private let onlyIfAvailableUnwrappedReceivedProperties: Set<Property>
/// Received properties that are optional and not created by a parent.
private let unavailableOptionalProperties: Set<Property>
/// Properties that will be generated as `let` constants.
Expand All @@ -378,32 +414,6 @@ actor ScopeGenerator: CustomStringConvertible, Sendable {
private let propertiesToDeclare: Set<Property>
private let property: Property?

nonisolated private var onlyIfAvailableUnwrappedReceivedProperties: Set<Property> {
switch scopeData {
case let .property(instantiable, _, _, _, _):
.init(instantiable.dependencies.compactMap {
switch $0.source {
case .instantiated, .forwarded:
nil
case let .received(onlyIfAvailable):
if onlyIfAvailable {
$0.property.asUnwrappedProperty
} else {
nil
}
case let .aliased(fulfillingProperty, _, onlyIfAvailable):
if onlyIfAvailable {
fulfillingProperty.asUnwrappedProperty
} else {
nil
}
}
})
case .root, .alias:
[]
}
}

private var unavailablePropertiesToGenerateCodeTask = [Set<Property>: Task<String, Error>]()

private var orderedPropertiesToGenerate: [ScopeGenerator] {
Expand All @@ -420,19 +430,21 @@ actor ScopeGenerator: CustomStringConvertible, Sendable {
else {
return
}
// Mark as fulfilled before recursing to prevent cycles.
propertyToUnfulfilledScopeMap[property] = nil
let scopeDependencies = propertyToUnfulfilledScopeMap
.keys
.intersection(scope.receivedProperties)
.union(scope.onlyIfAvailableUnwrappedReceivedProperties)
.intersection(
scope.receivedProperties
.union(scope.onlyIfAvailableUnwrappedReceivedProperties)
)
.compactMap { propertyToUnfulfilledScopeMap[$0] }
// Fulfill the scopes we depend upon.
for dependentScope in scopeDependencies {
fulfill(dependentScope)
}

// We can now be marked as fulfilled!
orderedPropertiesToGenerate.append(scope)
propertyToUnfulfilledScopeMap[property] = nil
}

for scope in propertiesToGenerate {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SafeDITool/SafeDITool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ struct SafeDITool: AsyncParsableCommand, Sendable {
// MARK: Internal

static var currentVersion: String {
"1.5.1"
"1.5.2"
}

func run() async throws {
Expand Down
71 changes: 71 additions & 0 deletions Tests/SafeDIToolTests/SafeDIToolCodeGenerationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5933,6 +5933,77 @@ struct SafeDIToolCodeGenerationTests: ~Copyable {
)
}

@Test
mutating func run_writesConvenienceExtensionOnRootOfTree_whenInstantiatorClosureTransitivelyCapturesVariableDeclaredLaterAlphabetically() async throws {
let output = try await executeSafeDIToolTest(
swiftFileContent: [
"""
@Instantiable(isRoot: true)
public final class Root {
public init(childVCBuilder: Instantiator<ChildVC>, service: Service) {
fatalError("SafeDI doesn't inspect the initializer body")
}

@Instantiated let childVCBuilder: Instantiator<ChildVC>
@Instantiated let service: Service
}
""",
"""
@Instantiable
public final class ChildVC {
public init(grandchildVCBuilder: Instantiator<GrandchildVC>) {
fatalError("SafeDI doesn't inspect the initializer body")
}

@Instantiated let grandchildVCBuilder: Instantiator<GrandchildVC>
}
""",
"""
@Instantiable
public final class GrandchildVC {
public init(service: Service?) {
fatalError("SafeDI doesn't inspect the initializer body")
}

@Received(onlyIfAvailable: true) let service: Service?
}
""",
"""
@Instantiable
public final class Service {
public init() {
fatalError("SafeDI doesn't inspect the initializer body")
}
}
""",
],
buildDependencyTreeOutput: true,
filesToDelete: &filesToDelete
)

#expect(try #require(output.dependencyTree) == """
// This file was generated by the SafeDIGenerateDependencyTree build tool plugin.
// Any modifications made to this file will be overwritten on subsequent builds.
// Please refrain from editing this file directly.

extension Root {
public convenience init() {
let service = Service()
func __safeDI_childVCBuilder() -> ChildVC {
func __safeDI_grandchildVCBuilder() -> GrandchildVC {
GrandchildVC(service: service)
}
let grandchildVCBuilder = Instantiator<GrandchildVC>(__safeDI_grandchildVCBuilder)
return ChildVC(grandchildVCBuilder: grandchildVCBuilder)
}
let childVCBuilder = Instantiator<ChildVC>(__safeDI_childVCBuilder)
self.init(childVCBuilder: childVCBuilder, service: service)
}
}
"""
)
}

// MARK: Private

private var filesToDelete = [URL]()
Expand Down
Loading