/* * Copyright (C) 2018 The Dagger Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package dagger.spi; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.testing.compile.Compilation; import com.google.testing.compile.JavaFileObjects; import dagger.internal.codegen.ComponentProcessor; import javax.tools.JavaFileObject; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public final class SpiPluginTest { @Test public void moduleBinding() { JavaFileObject module = JavaFileObjects.forSourceLines( "test.TestModule", "package test;", "", "import dagger.Module;", "import dagger.Provides;", "", "@Module", "interface TestModule {", " @Provides", " static int provideInt() {", " return 0;", " }", "}"); Compilation compilation = javac() .withProcessors(new ComponentProcessor()) .withOptions( "-Aerror_on_binding=java.lang.Integer", "-Adagger.fullBindingGraphValidation=ERROR", "-Adagger.pluginsVisitFullBindingGraphs=ENABLED") .compile(module); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining( message("[FailingPlugin] Bad Binding: @Provides int test.TestModule.provideInt()")) .inFile(module) .onLineContaining("interface TestModule"); } @Test public void dependencyTraceAtBinding() { JavaFileObject foo = JavaFileObjects.forSourceLines( "test.Foo", "package test;", "", "import javax.inject.Inject;", "", "class Foo {", " @Inject Foo() {}", "}"); JavaFileObject component = JavaFileObjects.forSourceLines( "test.TestComponent", "package test;", "", "import dagger.Component;", "", "@Component", "interface TestComponent {", " Foo foo();", "}"); Compilation compilation = javac() .withProcessors(new ComponentProcessor()) .withOptions("-Aerror_on_binding=test.Foo") .compile(component, foo); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining( message( "[FailingPlugin] Bad Binding: @Inject test.Foo()", " test.Foo is requested at", " test.TestComponent.foo()")) .inFile(component) .onLineContaining("interface TestComponent"); } @Test public void dependencyTraceAtDependencyRequest() { JavaFileObject foo = JavaFileObjects.forSourceLines( "test.Foo", "package test;", "", "import javax.inject.Inject;", "", "class Foo {", " @Inject Foo(Duplicated inFooDep) {}", "}"); JavaFileObject duplicated = JavaFileObjects.forSourceLines( "test.Duplicated", "package test;", "", "import javax.inject.Inject;", "", "class Duplicated {", " @Inject Duplicated() {}", "}"); JavaFileObject entryPoint = JavaFileObjects.forSourceLines( "test.EntryPoint", "package test;", "", "import javax.inject.Inject;", "", "class EntryPoint {", " @Inject EntryPoint(Foo foo, Duplicated dup1, Duplicated dup2) {}", "}"); JavaFileObject chain1 = JavaFileObjects.forSourceLines( "test.Chain1", "package test;", "", "import javax.inject.Inject;", "", "class Chain1 {", " @Inject Chain1(Chain2 chain) {}", "}"); JavaFileObject chain2 = JavaFileObjects.forSourceLines( "test.Chain2", "package test;", "", "import javax.inject.Inject;", "", "class Chain2 {", " @Inject Chain2(Chain3 chain) {}", "}"); JavaFileObject chain3 = JavaFileObjects.forSourceLines( "test.Chain3", "package test;", "", "import javax.inject.Inject;", "", "class Chain3 {", " @Inject Chain3(Foo foo) {}", "}"); JavaFileObject component = JavaFileObjects.forSourceLines( "test.TestComponent", "package test;", "", "import dagger.Component;", "", "@Component", "interface TestComponent {", " EntryPoint entryPoint();", " Chain1 chain();", "}"); CompilationFactory compilationFactory = new CompilationFactory(component, foo, duplicated, entryPoint, chain1, chain2, chain3); assertThat(compilationFactory.compilationWithErrorOnDependency("entryPoint")) .hadErrorContaining( message( "[FailingPlugin] Bad Dependency: test.TestComponent.entryPoint() (entry point)", " test.EntryPoint is requested at", " test.TestComponent.entryPoint()")) .inFile(component) .onLineContaining("interface TestComponent"); assertThat(compilationFactory.compilationWithErrorOnDependency("dup1")) .hadErrorContaining( message( "[FailingPlugin] Bad Dependency: test.EntryPoint(…, dup1, …)", " test.Duplicated is injected at", " test.EntryPoint(…, dup1, …)", " test.EntryPoint is requested at", " test.TestComponent.entryPoint()")) .inFile(component) .onLineContaining("interface TestComponent"); assertThat(compilationFactory.compilationWithErrorOnDependency("dup2")) .hadErrorContaining( message( "[FailingPlugin] Bad Dependency: test.EntryPoint(…, dup2)", " test.Duplicated is injected at", " test.EntryPoint(…, dup2)", " test.EntryPoint is requested at", " test.TestComponent.entryPoint()")) .inFile(component) .onLineContaining("interface TestComponent"); Compilation inFooDepCompilation = compilationFactory.compilationWithErrorOnDependency("inFooDep"); assertThat(inFooDepCompilation) .hadErrorContaining( message( "[FailingPlugin] Bad Dependency: test.Foo(inFooDep)", " test.Duplicated is injected at", " test.Foo(inFooDep)", " test.Foo is injected at", " test.EntryPoint(foo, …)", " test.EntryPoint is requested at", " test.TestComponent.entryPoint()", "The following other entry points also depend on it:", " test.TestComponent.chain()")) .inFile(component) .onLineContaining("interface TestComponent"); } @Test public void dependencyTraceAtDependencyRequest_subcomponents() { JavaFileObject foo = JavaFileObjects.forSourceLines( "test.Foo", "package test;", "", "import javax.inject.Inject;", "", "class Foo {", " @Inject Foo() {}", "}"); JavaFileObject entryPoint = JavaFileObjects.forSourceLines( "test.EntryPoint", "package test;", "", "import javax.inject.Inject;", "", "class EntryPoint {", " @Inject EntryPoint(Foo foo) {}", "}"); JavaFileObject component = JavaFileObjects.forSourceLines( "test.TestComponent", "package test;", "", "import dagger.Component;", "", "@Component", "interface TestComponent {", " TestSubcomponent sub();", "}"); JavaFileObject subcomponent = JavaFileObjects.forSourceLines( "test.TestSubcomponent", "package test;", "", "import dagger.Subcomponent;", "", "@Subcomponent", "interface TestSubcomponent {", " EntryPoint childEntryPoint();", "}"); CompilationFactory compilationFactory = new CompilationFactory(component, subcomponent, foo, entryPoint); assertThat(compilationFactory.compilationWithErrorOnDependency("childEntryPoint")) .hadErrorContaining( message( "[FailingPlugin] Bad Dependency: " + "test.TestSubcomponent.childEntryPoint() (entry point)", " test.EntryPoint is requested at", " test.TestSubcomponent.childEntryPoint()" + " [test.TestComponent → test.TestSubcomponent]")) .inFile(component) .onLineContaining("interface TestComponent"); assertThat(compilationFactory.compilationWithErrorOnDependency("foo")) .hadErrorContaining( // TODO(ronshapiro): Maybe make the component path resemble a stack trace: // test.TestSubcomponent is a child of // test.TestComponent // TODO(dpb): Or invert the order: Child → Parent message( "[FailingPlugin] Bad Dependency: test.EntryPoint(foo)", " test.Foo is injected at", " test.EntryPoint(foo)", " test.EntryPoint is requested at", " test.TestSubcomponent.childEntryPoint() " + "[test.TestComponent → test.TestSubcomponent]")) .inFile(component) .onLineContaining("interface TestComponent"); } @Test public void errorOnComponent() { JavaFileObject component = JavaFileObjects.forSourceLines( "test.TestComponent", "package test;", "", "import dagger.Component;", "", "@Component", "interface TestComponent {}"); Compilation compilation = javac() .withProcessors(new ComponentProcessor()) .withOptions("-Aerror_on_component") .compile(component); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining("[FailingPlugin] Bad Component: test.TestComponent") .inFile(component) .onLineContaining("interface TestComponent"); } @Test public void errorOnSubcomponent() { JavaFileObject subcomponent = JavaFileObjects.forSourceLines( "test.TestSubcomponent", "package test;", "", "import dagger.Subcomponent;", "", "@Subcomponent", "interface TestSubcomponent {}"); JavaFileObject component = JavaFileObjects.forSourceLines( "test.TestComponent", "package test;", "", "import dagger.Component;", "", "@Component", "interface TestComponent {", " TestSubcomponent subcomponent();", "}"); Compilation compilation = javac() .withProcessors(new ComponentProcessor()) .withOptions("-Aerror_on_subcomponents") .compile(component, subcomponent); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContaining( "[FailingPlugin] Bad Subcomponent: test.TestComponent → test.TestSubcomponent " + "[test.TestComponent → test.TestSubcomponent]") .inFile(component) .onLineContaining("interface TestComponent"); } // SpiDiagnosticReporter uses a shortest path algorithm to determine a dependency trace to a // binding. Without modifications, this would produce a strange error if a shorter path exists // from one entrypoint, through a @Module.subcomponents builder binding edge, and to the binding // usage within the subcomponent. Therefore, when scanning for the shortest path, we only consider // BindingNodes so we don't cross component boundaries. This test exhibits this case. @Test public void shortestPathToBindingExistsThroughSubcomponentBuilder() { JavaFileObject chain1 = JavaFileObjects.forSourceLines( "test.Chain1", "package test;", "", "import javax.inject.Inject;", "", "class Chain1 {", " @Inject Chain1(Chain2 chain) {}", "}"); JavaFileObject chain2 = JavaFileObjects.forSourceLines( "test.Chain2", "package test;", "", "import javax.inject.Inject;", "", "class Chain2 {", " @Inject Chain2(Chain3 chain) {}", "}"); JavaFileObject chain3 = JavaFileObjects.forSourceLines( "test.Chain3", "package test;", "", "import javax.inject.Inject;", "", "class Chain3 {", " @Inject Chain3(ExposedOnSubcomponent exposedOnSubcomponent) {}", "}"); JavaFileObject exposedOnSubcomponent = JavaFileObjects.forSourceLines( "test.ExposedOnSubcomponent", "package test;", "", "import javax.inject.Inject;", "", "class ExposedOnSubcomponent {", " @Inject ExposedOnSubcomponent() {}", "}"); JavaFileObject subcomponent = JavaFileObjects.forSourceLines( "test.TestSubcomponent", "package test;", "", "import dagger.Subcomponent;", "", "@Subcomponent", "interface TestSubcomponent {", " ExposedOnSubcomponent exposedOnSubcomponent();", "", " @Subcomponent.Builder", " interface Builder {", " TestSubcomponent build();", " }", "}"); JavaFileObject subcomponentModule = JavaFileObjects.forSourceLines( "test.SubcomponentModule", "package test;", "", "import dagger.Module;", "", "@Module(subcomponents = TestSubcomponent.class)", "interface SubcomponentModule {}"); JavaFileObject component = JavaFileObjects.forSourceLines( "test.TestComponent", "package test;", "", "import dagger.Component;", "import javax.inject.Singleton;", "", "@Singleton", "@Component(modules = SubcomponentModule.class)", "interface TestComponent {", " Chain1 chain();", " TestSubcomponent.Builder subcomponent();", "}"); Compilation compilation = javac() .withProcessors(new ComponentProcessor()) .withOptions("-Aerror_on_binding=test.ExposedOnSubcomponent") .compile( component, subcomponent, chain1, chain2, chain3, exposedOnSubcomponent, subcomponentModule); assertThat(compilation) .hadErrorContaining( message( "[FailingPlugin] Bad Binding: @Inject test.ExposedOnSubcomponent()", " test.ExposedOnSubcomponent is injected at", " test.Chain3(exposedOnSubcomponent)", " test.Chain3 is injected at", " test.Chain2(chain)", " test.Chain2 is injected at", " test.Chain1(chain)", " test.Chain1 is requested at", " test.TestComponent.chain()", "The following other entry points also depend on it:", " test.TestSubcomponent.exposedOnSubcomponent() " + "[test.TestComponent → test.TestSubcomponent]")) .inFile(component) .onLineContaining("interface TestComponent"); } // This works around an issue in the opensource compile testing where only one diagnostic is // recorded per line. When multiple validation items resolve to the same entry point, we can // only see the first. This helper class makes it easier to compile all of the files in the test // multiple times with different options to single out each error private static class CompilationFactory { private final ImmutableList javaFileObjects; CompilationFactory(JavaFileObject... javaFileObjects) { this.javaFileObjects = ImmutableList.copyOf(javaFileObjects); } private Compilation compilationWithErrorOnDependency(String dependencySimpleName) { return javac() .withProcessors(new ComponentProcessor()) .withOptions("-Aerror_on_dependency=" + dependencySimpleName) .compile(javaFileObjects); } } private static String message(String... lines) { return Joiner.on("\n ").join(lines); } }