Skip to content

Commit

Permalink
Merge branch 'main-1.4.1'
Browse files Browse the repository at this point in the history
# Conflicts:
#	api/common/src/main/kotlin/loadImplementation.kt
  • Loading branch information
Fedor Ihnatkevich committed Nov 2, 2023
2 parents 897a6da + 4c39ebd commit bafdd8c
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ internal class DefaultImplementationLoader : ImplementationLoader {
require(builderClass.isAnnotationPresent(Component.Builder::class.java)) {
"$builderClass is not a builder for a Yatagan component"
}
val componentClass = requireNotNull(builderClass.enclosingClass) {
val componentClassName = builderClass.name.substringBeforeLast("$")
require(componentClassName != builderClass.name) {
"No enclosing component class found for $builderClass"
}
val componentClass = builderClass.classLoader.loadClass(componentClassName)
val yataganComponentClass = loadImplementationClass(componentClass)
val builder = builderClass.cast(yataganComponentClass.getDeclaredMethod("builder").invoke(null))
Result.success(builder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,49 +16,51 @@

package com.yandex.yatagan.core.graph.impl

import com.yandex.yatagan.base.ifOrElseNull
import com.yandex.yatagan.base.traverseDepthFirstWithPath
import com.yandex.yatagan.core.graph.BindingGraph
import com.yandex.yatagan.core.graph.bindings.AliasBinding
import com.yandex.yatagan.core.graph.bindings.BaseBinding
import com.yandex.yatagan.core.graph.bindings.Binding
import com.yandex.yatagan.core.graph.component1
import com.yandex.yatagan.core.graph.component2
import com.yandex.yatagan.core.model.DependencyKind
import com.yandex.yatagan.core.model.NodeDependency
import com.yandex.yatagan.core.model.component1
import com.yandex.yatagan.core.model.component2
import com.yandex.yatagan.core.model.isEager
import com.yandex.yatagan.validation.Validator
import com.yandex.yatagan.validation.format.Strings
import com.yandex.yatagan.validation.format.reportError
import kotlin.collections.component1
import kotlin.collections.component2

internal fun validateNoLoops(graph: BindingGraph, validator: Validator) {
traverseDepthFirstWithPath(
roots = buildList<BaseBinding> {
traverseDepthFirstWithPath<Pair<DependencyKind, BaseBinding>>(
roots = buildList {
graph.entryPoints.forEach { (_, dependency) ->
add(graph.resolveBindingRaw(dependency.node))
add(dependency.kind to graph.resolveBindingRaw(dependency.node))
}
graph.memberInjectors.forEach {
it.membersToInject.forEach { (_, dependency) ->
add(graph.resolveBindingRaw(dependency.node))
add(dependency.kind to graph.resolveBindingRaw(dependency.node))
}
}
},
childrenOf = { binding ->
childrenOf = { (_, binding) ->
class DependenciesVisitor : BaseBinding.Visitor<List<NodeDependency>> {
override fun visitOther(other: BaseBinding) = throw AssertionError()
override fun visitAlias(alias: AliasBinding) = listOf(alias.source)
override fun visitBinding(binding: Binding) =
binding.dependencies + binding.nonStaticConditionProviders
}
binding.accept(DependenciesVisitor()).mapNotNull { (node, kind) ->
ifOrElseNull(kind.isEager) { binding.owner.resolveBindingRaw(node) }
}.asIterable()
binding.accept(DependenciesVisitor()).map { (node, kind) ->
kind to binding.owner.resolveBindingRaw(node)
}
},
onLoop = { bindingLoop ->
validator.reportError(Strings.Errors.dependencyLoop(chain = bindingLoop.toList()))
// Check for lazy edges - if they are present, this is a "benign" loop, don't report it.
if (bindingLoop.all { (kind, _) -> kind.isEager }) {
val chain = bindingLoop.mapTo(arrayListOf()) { (_, binding) -> binding }
validator.reportError(Strings.Errors.dependencyLoop(chain = chain))
}
}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ fun generate(
}
}
run {
// Break the loops
fun Binding.withoutDependencies(nodes: Collection<LogicalNode>): Binding {
fun replaceWithLazy(): Map<LogicalNode, DependencyKind> {
return buildMap {
Expand All @@ -225,41 +226,55 @@ fun generate(
}
}

// Break the loops
// Nodes currently being visited (entered)
val markedGray = hashSetOf<LogicalNode>()
// Already visited nodes (entered and exited)
val markedBlack = hashSetOf<LogicalNode>()
// Local eager-edge connectivity sub-graph
val stack = arrayListOf<LogicalNode>()
// Pending nodes behind non-eager edges.
val pendingQueue = arrayListOf<LogicalNode>()

stack += component.entryPoints.keys
pendingQueue += component.entryPoints.keys
for (memberInjector in component.memberInjectors) {
stack += memberInjector.members
pendingQueue += memberInjector.members
}

while (stack.isNotEmpty()) {
when (val node = stack.last()) {
in markedBlack -> {
stack.removeLast()
}
in markedGray -> {
stack.removeLast()
markedBlack += node
markedGray -= node
}
else -> {
markedGray += node
var binding = componentTree.resolveBinding(node)
// Check if this binding introduces a loop into the graph
val formsLoopBy = binding.dependencies.mapNotNull { (depNode, depKind) ->
depNode.takeIf { depKind == DependencyKind.Direct && depNode in markedGray }
while (pendingQueue.isNotEmpty()) {
check(markedGray.isEmpty())

stack += pendingQueue
pendingQueue.clear()

while (stack.isNotEmpty()) {
when (val node = stack.last()) {
in markedBlack -> {
stack.removeLast()
}
if (formsLoopBy.isNotEmpty()) {
// Need to break the loop
binding = binding.withoutDependencies(formsLoopBy)
componentTree.updateBinding(node, binding)

in markedGray -> {
stack.removeLast()
markedBlack += node
markedGray -= node
}
for ((depNode, depKind) in binding.dependencies) {
if (depKind == DependencyKind.Direct) {
stack += depNode

else -> {
markedGray += node
var binding = componentTree.resolveBinding(node)
// Check if this binding introduces a loop into the graph
val formsLoopBy = binding.dependencies.mapNotNull { (depNode, depKind) ->
depNode.takeIf { depKind == DependencyKind.Direct && depNode in markedGray }
}
if (formsLoopBy.isNotEmpty()) {
// Need to break the loop
binding = binding.withoutDependencies(formsLoopBy)
componentTree.updateBinding(node, binding)
}
for ((depNode, depKind) in binding.dependencies) {
when (depKind) {
DependencyKind.Direct -> stack += depNode
else -> pendingQueue += depNode
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,45 @@ class GraphLoopsFailureTest(
compileRunAndValidate()
}

@Test
fun `dependency loop behind Provider`() {
givenKotlinSource("test.TestCase", """
import com.yandex.yatagan.*
import javax.inject.*
class ClassA @Inject constructor(b: ClassB)
class ClassB @Inject constructor(a: ClassC)
class ClassC @Inject constructor(a: Optional<ClassD>)
class ClassD @Inject constructor(a: ClassA)
class Entry @Inject constructor(a: Provider<ClassA>)
class MyApi
@Module class MyModule {
@Provides @Named("X") fun provideX(@Named("Y") d: MyApi): MyApi = MyApi()
@Provides @Named("Y") fun provideY(@Named("Z") d: MyApi): MyApi = MyApi()
@Provides @Named("Z") fun provideZ(@Named("X") d1: MyApi,
@Named("Y") d2: MyApi): MyApi = MyApi()
@Provides @Named("E") fun provideE(@Named("Y") d1: Lazy<MyApi>,
@Named("X") d2: Lazy<MyApi>): MyApi = MyApi()
}
@Component
interface RootComponent {
val c: Entry
fun sub(): SubComponent
}
@Component(isRoot = false, modules = [MyModule::class])
interface SubComponent {
val c: Entry
@get:Named("E") val z: MyApi
}
""".trimIndent())

compileRunAndValidate()
}

@Test
fun `dependency loop with alias`() {
givenKotlinSource("test.TestCase", """
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
error: Binding dependency loop detected:
(*) `@javax.inject.Named(value="Y") test.MyApi` provided by `provision test.MyModule::provideY(.. 1 dependency)` depends on <-
`@javax.inject.Named(value="Z") test.MyApi` provided by `provision test.MyModule::provideZ(.. 2 dependencies)` depends on <-
`@javax.inject.Named(value="X") test.MyApi` provided by `provision test.MyModule::provideX(.. 1 dependency)` depends on <- (*)
Encountered:
in graph for root-component test.RootComponent
here: graph for component test.SubComponent
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: Binding dependency loop detected:
(*) `@javax.inject.Named(value="Y") test.MyApi` provided by `provision test.MyModule::provideY(.. 1 dependency)` depends on <-
`@javax.inject.Named(value="Z") test.MyApi` provided by `provision test.MyModule::provideZ(.. 2 dependencies)` depends on <- (*)
Encountered:
in graph for root-component test.RootComponent
here: graph for component test.SubComponent
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: Binding dependency loop detected:
(*) `test.ClassB` provided by `inject-constructor test.ClassB(.. 1 dependency)` depends on <-
`test.ClassC` provided by `inject-constructor test.ClassC(.. 1 dependency)` depends on <-
`test.ClassD` provided by `inject-constructor test.ClassD(.. 1 dependency)` depends on <-
`test.ClassA` provided by `inject-constructor test.ClassA(.. 1 dependency)` depends on <- (*)
Encountered:
here: graph for root-component test.RootComponent
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Encountered:
in graph for root-component test.RootComponent
here: graph for component test.SubComponent
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Loading

0 comments on commit bafdd8c

Please sign in to comment.