dagger: Processor error on missing type while traversing too far up component dependency chain.

Module ‘test-a’:

public final class A {
  @Inject A() {}
}

@Component
public interface ComponentA {
  A a();
}

Module ‘test-b’ which has implementation project(':test-a'):

public final class B {
  @Inject B(A a) {}
}

@Component(dependencies = ComponentA.class)
public interface ComponentB {
  B b();
}

Module ‘test-c’ which has implementation project(':test-b'):

public final class C {
  @Inject C(B b) {}
}

@Component(dependencies = ComponentB.class)
public interface ComponentC {
  C c();
}

fails with:

> Task :test-c:compileDebugJavaWithJavac FAILED
error: cannot access ComponentA
  class file for test.a.ComponentA not found
  Consult the following stack trace for details.
  com.sun.tools.javac.code.Symbol$CompletionFailure: class file for test.a.ComponentA not found
1 error

which makes sense, because ‘test-c’ isn’t meant to see ‘test-a’ as it’s an implementation detail of ‘test-b’, but why is Dagger in ‘test-c’ trying to do anything with ComponentA? Once it reaches ComponentB and sees the required B type exposed shouldn’t it stop?

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 58
  • Comments: 48

Commits related to this issue

Most upvoted comments

We fully understand the issue you’re seeing, that’s not the problem.

I think the best recommendation I have now is not to use implementation with dagger components in your component dependencies chain.

I’d like to bring this issue up again. I understand why it happens and why it isn’t purely Dagger’s fault. But the build tools (Gradle in this case) and Dagger don’t work well together. There are only two solutions at the moment for Gradle: Add the dependencies with api to the library module and fully expose them (not great for modularization) or add missing dependencies as compileOnly (or maybe even annotationProcessor / kapt) to the modules that need them, what isn’t good for modularization and isolation either.

I don’t have any hands-on experience with Blaze or Bazel, but I heard it can solve this issue with its finer granularity. But for many people those tools aren’t feasible.

How could Dagger avoid this issue? By turning off the cyclic dependency check?

Running into this again.

I think Dagger should support this case by giving up on validation when it reaches an annotation whose referenced types cannot be resolved. This means that the scope annotation of that referenced component dependency also almost certainly cannot be resolved in the current context as it’s been entirely encapsulated which means there can be no cycle. Even if there is a cycle, it’s hidden from the consumer and therefore irrelevant for what the check is enforcing. Correct semantics are still maintained.

This check is already best effort. There doesn’t seem to be a reason to make it fail in situations where the user is actually participating in the best practice of using implementation.

Here’s an updated workaround for Android.

Works for both apt and kapt and properly wires task dependnecies. Works for AGP 3.5 and Kotlin 1.3.61

fun Project.aptRuntime2CompileClasspath() = afterEvaluate {

  @Suppress("UNCHECKED_CAST")
  val variants: DomainObjectSet<BaseVariant> = when (val android = the<BaseExtension>()) {
    is AppExtension     -> android.applicationVariants
    is LibraryExtension -> android.libraryVariants
    else                -> error("Unrecognized android extension $android")
  } as DomainObjectSet<BaseVariant>
  
  for (variant in variants) {
    val compileJavaWithJavac = variant.javaCompileProvider
    val runtimeClasspath = variant.runtimeConfiguration
    /**
     * jar inside intermediates/runtime_library_classes,
     * which produced by running bundleLibRuntime${targetFlavor}
     */
    val runtimeClasspathJars = runtimeClasspath.copyRecursive().apply {
      val attributeArtifactType = AndroidArtifacts.ARTIFACT_TYPE
      val runtimeClasspathArtifact = "android-classes"
      attributes.attribute(attributeArtifactType, runtimeClasspathArtifact)
    }
    val runtimeClasspathJarsTasks: TaskDependency = runtimeClasspathJars.buildDependencies
    val runtimeClasspathJarsFiles = runtimeClasspathJars.fileCollection { true }
    //javac apt
    compileJavaWithJavac.configure {
      dependsOn(runtimeClasspathJarsTasks)
      /** classpath supplement */
      doFirst {
        classpath = classpath.plus(runtimeClasspathJarsFiles)
      }
    }
    //kotlin kapt
    val variantKaptTaskName = "kapt${variant.name.capitalize()}Kotlin"
    if (variantKaptTaskName !in tasks.names) continue
    tasks.named(variantKaptTaskName).configure {
      dependsOn(runtimeClasspathJarsTasks)
      /** classpath supplement */
      val kaptTask = this
      val kotlinCompileTask: AbstractCompile = javaClass.getField("kotlinCompileTask").run {
        isAccessible = true
        get(kaptTask)
      } as AbstractCompile
      kaptTask.doFirst {
        kotlinCompileTask.classpath = kotlinCompileTask.classpath.plus(runtimeClasspathJarsFiles)
      }
    }
  }
}

It adds module runtime classpath to annotation processor classpath.

Hi @ronshapiro , we are also modularizing our app with reducing build time as one of the main goals and I can imagine more developers will run into same situation. So hope you guys can figure out a solution. Thanks!

@ronshapiro I bumped into the same issue - trying to modularize our app and having a component in each module. We currently have the main app module’s AppComponent depending on a library’s component in datasource module and datasource by itself depends on another library module that also exposes a component. We get the same compilation error.

I tried to add scopes for each component, but that didn’t change anything.

I’ve recreated this setup in a sample app https://gitlab.com/codepond/dagger-modules

It would be great if you could have a look.

Thanks and Happy TuBiShvat! 😃

No problems so far.

Overhead and performance depend on your project setup. In theory: If you have only few root modules which generate @Component’s(this is the only place where aptRuntime2CompileClasspath should be applied), and a module tree with jvm/aar which you switched to implementation(because you were forced to use api for dagger), overall build time of the whole project should slightly increase because of compilation avoidance and less compilation dependencies for each module.

Question clearly states that the test-c isn’t meant to see test-a

But it does see it, because it’s exposed on B’s signatures (both constructor and annotations). You as a human might not consider the constructor part of the API (because it’s not really meant to be called by others), but a machine can’t know that. A type is either exposed or not. There is no “partly exposed”. You’d have to hide it behind an interface to solve that. This is the workaround that Ron already pointed out.

Looking at the original example from Jake:

public final class A {
  @Inject A() {}
}

@Component
public interface ComponentA {
  A a();
}
public final class B {
  @Inject B(A a) {}
}

@Component(dependencies = ComponentA.class)
public interface ComponentB {
  B b();
}

ComponentB references ComponentA with an annotation, which makes ComponentA part of its public signature. Even if it didn’t reference ComponentA, it is referencing B and B has A in its public constructor signature. So using an implementation dependency is not right in this case, it should be an api dependency. All the other examples in this thread have the same issue. See also the user guide chapter on how to recognize api and implementation usage of a dependency.

@ronshapiro already provided a workaround of having an interface for your component and only making that interface part of your API while keeping the implementation separate. I don’t see any other way around this. It’s a price you pay for having everything statically analysed and generated, which requires having everything on the public signatures instead of making it an implementation detail (like you would inside a Guice module).

ronshapiro: what do you think about an opt-in flag to disable this validation. Would you be willing to upstream such a change?

Currently this error completely blocks using dagger with our multi-module application.

I would have thought you’d see an ErrorType or something prior to this and that trying to resolve that type is what caused by exception.