-
-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve + consolidate cycle detection #100
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -284,7 +284,7 @@ actor ScopeGenerator: CustomStringConvertible, Sendable { | |
} | ||
|
||
func generateDOT() async throws -> String { | ||
let orderedPropertiesToGenerate = try orderedPropertiesToGenerate | ||
let orderedPropertiesToGenerate = orderedPropertiesToGenerate | ||
let instantiatedProperties = orderedPropertiesToGenerate.map(\.scopeData.asDOTNode) | ||
var childDOTs = [String]() | ||
for orderedPropertyToGenerate in orderedPropertiesToGenerate { | ||
|
@@ -351,58 +351,43 @@ actor ScopeGenerator: CustomStringConvertible, Sendable { | |
private var generateCodeTask: Task<String, Error>? | ||
|
||
private var orderedPropertiesToGenerate: [ScopeGenerator] { | ||
get throws { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we no longer need to |
||
var orderedPropertiesToGenerate = [ScopeGenerator]() | ||
var propertyToUnfulfilledScopeMap = propertiesToGenerate | ||
.reduce(into: OrderedDictionary<Property, ScopeGenerator>()) { partialResult, scope in | ||
if let property = scope.property { | ||
partialResult[property] = scope | ||
} | ||
} | ||
func fulfill(_ scope: ScopeGenerator, stack: OrderedSet<Property> = []) throws { | ||
guard | ||
let property = scope.property, | ||
propertyToUnfulfilledScopeMap[property] != nil | ||
else { | ||
return | ||
} | ||
guard !stack.contains(property) else { | ||
if property.propertyType.isConstant { | ||
throw GenerationError.dependencyCycleDetected( | ||
stack.drop(while: { $0 != property }) + [property], | ||
scope: self | ||
) | ||
} else { | ||
return | ||
} | ||
var orderedPropertiesToGenerate = [ScopeGenerator]() | ||
var propertyToUnfulfilledScopeMap = propertiesToGenerate | ||
.reduce(into: OrderedDictionary<Property, ScopeGenerator>()) { partialResult, scope in | ||
if let property = scope.property { | ||
partialResult[property] = scope | ||
} | ||
|
||
let scopeDependencies = propertyToUnfulfilledScopeMap | ||
.keys | ||
.intersection(scope.requiredReceivedProperties) | ||
.compactMap { propertyToUnfulfilledScopeMap[$0] } | ||
// Fulfill the scopes we depend upon. | ||
for dependentScope in scopeDependencies { | ||
var stack = stack | ||
stack.append(property) | ||
try fulfill(dependentScope, stack: stack) | ||
} | ||
// We can now be marked as fulfilled! | ||
orderedPropertiesToGenerate.append(scope) | ||
propertyToUnfulfilledScopeMap[property] = nil | ||
} | ||
|
||
for scope in propertiesToGenerate { | ||
try fulfill(scope) | ||
func fulfill(_ scope: ScopeGenerator) { | ||
guard | ||
let property = scope.property, | ||
propertyToUnfulfilledScopeMap[property] != nil | ||
else { | ||
return | ||
} | ||
let scopeDependencies = propertyToUnfulfilledScopeMap | ||
.keys | ||
.intersection(scope.requiredReceivedProperties) | ||
.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 | ||
} | ||
|
||
return orderedPropertiesToGenerate | ||
for scope in propertiesToGenerate { | ||
fulfill(scope) | ||
} | ||
|
||
return orderedPropertiesToGenerate | ||
} | ||
|
||
private func generateProperties(leadingMemberWhitespace: String) async throws -> [String] { | ||
var generatedProperties = [String]() | ||
for childGenerator in try orderedPropertiesToGenerate { | ||
for childGenerator in orderedPropertiesToGenerate { | ||
try await generatedProperties.append( | ||
childGenerator | ||
.generateCode(leadingWhitespace: leadingMemberWhitespace) | ||
|
@@ -421,17 +406,11 @@ actor ScopeGenerator: CustomStringConvertible, Sendable { | |
|
||
private enum GenerationError: Error, CustomStringConvertible { | ||
case erasedInstantiatorGenericDoesNotMatch(property: Property, instantiable: Instantiable) | ||
case dependencyCycleDetected([Property], scope: ScopeGenerator) | ||
|
||
var description: String { | ||
switch self { | ||
case let .erasedInstantiatorGenericDoesNotMatch(property, instantiable): | ||
"Property `\(property.asSource)` on \(instantiable.concreteInstantiable.asSource) incorrectly configured. Property should instead be of type `\(Dependency.erasedInstantiatorType)<\(instantiable.concreteInstantiable.asSource).ForwardedProperties, \(property.typeDescription.asInstantiatedType.asSource)>`." | ||
case let .dependencyCycleDetected(properties, scope): | ||
""" | ||
Dependency cycle detected on \(scope)! | ||
\(properties.map(\.asSource).joined(separator: " -> ")) | ||
""" | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -301,8 +301,8 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { | |
func test_run_onCodeWithReceivedPropertyThatRefersToCurrentInstantiable_throwsError() async throws { | ||
await assertThrowsError( | ||
""" | ||
Dependency cycle detected! | ||
AuthService -> AuthService | ||
Dependency received in same chain it is instantiated! | ||
@Instantiated authService: AuthService -> @Received authService: AuthService | ||
""" | ||
) { | ||
try await executeSafeDIToolTest( | ||
|
@@ -468,8 +468,8 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { | |
func test_run_onCodeWhereAliasedReceivedPropertyRefersToCurrentInstantiable_throwsError() async throws { | ||
await assertThrowsError( | ||
""" | ||
Dependency cycle detected! | ||
AuthService -> AuthService | ||
Dependency received in same chain it is instantiated! | ||
@Instantiated authService: AuthService -> @Received authService: AuthService | ||
""" | ||
) { | ||
try await executeSafeDIToolTest( | ||
|
@@ -911,8 +911,8 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { | |
func test_run_onCodeWithCircularReceivedDependencies_throwsError() async { | ||
await assertThrowsError( | ||
""" | ||
Dependency cycle detected on Root! | ||
a: A -> b: B -> c: C -> a: A | ||
Dependency received in same chain it is instantiated! | ||
@Instantiated a: A -> @Received b: B -> @Received c: C -> @Received a: A | ||
""" | ||
) { | ||
try await executeSafeDIToolTest( | ||
|
@@ -960,8 +960,59 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { | |
func test_run_onCodeWithCircularReceivedRenamedDependencies_throwsError() async { | ||
await assertThrowsError( | ||
""" | ||
Dependency cycle detected on A! | ||
b: B -> c: C -> renamedB: B -> b: B | ||
Dependency received in same chain it is instantiated! | ||
@Instantiated a: A -> @Received renamedB: B -> @Received c: C -> @Received a: A | ||
Comment on lines
+963
to
+964
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this test better mirrors the above test. I liked the parallelism |
||
""" | ||
) { | ||
try await executeSafeDIToolTest( | ||
swiftFileContent: [ | ||
""" | ||
@Instantiable | ||
public struct Root { | ||
@Instantiated | ||
private let a: A | ||
@Instantiated | ||
private let b: B | ||
@Received(fulfilledByDependencyNamed: "b", ofType: B.self) | ||
private let renamedB: B | ||
@Instantiated | ||
private let c: C | ||
} | ||
""", | ||
""" | ||
@Instantiable | ||
public struct A { | ||
@Received | ||
private let renamedB: B | ||
} | ||
""", | ||
""" | ||
@Instantiable | ||
public struct B { | ||
@Received | ||
private let c: C | ||
} | ||
""", | ||
""" | ||
@Instantiable | ||
public struct C { | ||
@Received | ||
private let a: A | ||
} | ||
""", | ||
], | ||
buildDependencyTreeOutput: true, | ||
filesToDelete: &filesToDelete | ||
) | ||
} | ||
} | ||
|
||
@MainActor | ||
func test_run_onCodeWithMultipleCircularReceivedRenamedDependencies_throwsError() async { | ||
await assertThrowsError( | ||
""" | ||
Dependency received in same chain it is instantiated! | ||
@Instantiated c: C -> @Received renamedB: B -> @Received c: C | ||
""" | ||
) { | ||
try await executeSafeDIToolTest( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
brought this line back from #95. Now that we're doing our cycle checking before we're doing our "initialized in chain" checking, we can make this more semantically correct