/* * 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.internal.codegen; import static com.google.testing.compile.CompilationSubject.assertThat; import static dagger.internal.codegen.Compilers.compilerWithOptions; import static dagger.internal.codegen.TestUtils.endsWithMessage; import androidx.room.compiler.processing.util.Source; import com.google.common.collect.ImmutableList; import com.google.testing.compile.Compilation; import dagger.testing.compile.CompilerTests; import java.util.regex.Pattern; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; @RunWith(Parameterized.class) public class DependencyCycleValidationTest { @Parameters(name = "{0}") public static ImmutableList parameters() { return CompilerMode.TEST_PARAMETERS; } private final CompilerMode compilerMode; public DependencyCycleValidationTest(CompilerMode compilerMode) { this.compilerMode = compilerMode; } private static final Source SIMPLE_CYCLIC_DEPENDENCY = CompilerTests.javaSource( "test.Outer", "package test;", "", "import dagger.Binds;", "import dagger.Component;", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Inject;", "", "final class Outer {", " static class A {", " @Inject A(C cParam) {}", " }", "", " static class B {", " @Inject B(A aParam) {}", " }", "", " static class C {", " @Inject C(B bParam) {}", " }", "", " @Module", " interface MModule {", " @Binds Object object(C c);", " }", "", " @Component", " interface CComponent {", " C getC();", " }", "}"); @Test public void cyclicDependency() { CompilerTests.daggerCompiler(SIMPLE_CYCLIC_DEPENDENCY) .withProcessingOptions(compilerMode.processorOptions()) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( String.join( "\n", "Found a dependency cycle:", " Outer.C is injected at", " Outer.A(cParam)", " Outer.A is injected at", " Outer.B(aParam)", " Outer.B is injected at", " Outer.C(bParam)", " Outer.C is injected at", " Outer.A(cParam)", " ...", "", "The cycle is requested via:", " Outer.C is requested at", " Outer.CComponent.getC()")) .onSource(SIMPLE_CYCLIC_DEPENDENCY) .onLineContaining("interface CComponent"); }); } // TODO(b/243720787): Requires CompilationResultSubject#hasErrorContainingMatch() @Test public void cyclicDependencyWithModuleBindingValidation() { // Cycle errors should not show a dependency trace to an entry point when doing full binding // graph validation. So ensure that the message doesn't end with "test.Outer.C is requested at // test.Outer.CComponent.getC()", as the previous test's message does. Pattern moduleBindingValidationError = endsWithMessage( "Found a dependency cycle:", " Outer.C is injected at", " Outer.A(cParam)", " Outer.A is injected at", " Outer.B(aParam)", " Outer.B is injected at", " Outer.C(bParam)", " Outer.C is injected at", " Outer.A(cParam)", " ...", "", "======================", "Full classname legend:", "======================", "Outer: test.Outer", "========================", "End of classname legend:", "========================"); Compilation compilation = compilerWithOptions("-Adagger.fullBindingGraphValidation=ERROR") .compile(SIMPLE_CYCLIC_DEPENDENCY.toJFO()); assertThat(compilation).failed(); assertThat(compilation) .hadErrorContainingMatch(moduleBindingValidationError) .inFile(SIMPLE_CYCLIC_DEPENDENCY.toJFO()) .onLineContaining("interface MModule"); assertThat(compilation) .hadErrorContainingMatch(moduleBindingValidationError) .inFile(SIMPLE_CYCLIC_DEPENDENCY.toJFO()) .onLineContaining("interface CComponent"); assertThat(compilation).hadErrorCount(2); } @Test public void cyclicDependencyNotIncludingEntryPoint() { Source component = CompilerTests.javaSource( "test.Outer", "package test;", "", "import dagger.Component;", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Inject;", "", "final class Outer {", " static class A {", " @Inject A(C cParam) {}", " }", "", " static class B {", " @Inject B(A aParam) {}", " }", "", " static class C {", " @Inject C(B bParam) {}", " }", "", " static class D {", " @Inject D(C cParam) {}", " }", "", " @Component", " interface DComponent {", " D getD();", " }", "}"); CompilerTests.daggerCompiler(component) .withProcessingOptions(compilerMode.processorOptions()) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( String.join( "\n", "Found a dependency cycle:", " Outer.C is injected at", " Outer.A(cParam)", " Outer.A is injected at", " Outer.B(aParam)", " Outer.B is injected at", " Outer.C(bParam)", " Outer.C is injected at", " Outer.A(cParam)", " ...", "", "The cycle is requested via:", " Outer.C is injected at", " Outer.D(cParam)", " Outer.D is requested at", " Outer.DComponent.getD()")) .onSource(component) .onLineContaining("interface DComponent"); }); } @Test public void cyclicDependencyNotBrokenByMapBinding() { Source component = CompilerTests.javaSource( "test.Outer", "package test;", "", "import dagger.Component;", "import dagger.Module;", "import dagger.Provides;", "import dagger.multibindings.IntoMap;", "import dagger.multibindings.StringKey;", "import java.util.Map;", "import javax.inject.Inject;", "", "final class Outer {", " static class A {", " @Inject A(Map cMap) {}", " }", "", " static class B {", " @Inject B(A aParam) {}", " }", "", " static class C {", " @Inject C(B bParam) {}", " }", "", " @Component(modules = CModule.class)", " interface CComponent {", " C getC();", " }", "", " @Module", " static class CModule {", " @Provides @IntoMap", " @StringKey(\"C\")", " static C c(C c) {", " return c;", " }", " }", "}"); CompilerTests.daggerCompiler(component) .withProcessingOptions(compilerMode.processorOptions()) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( String.join( "\n", "Found a dependency cycle:", " Outer.C is injected at", " Outer.CModule.c(c)", " Map is injected at", " Outer.A(cMap)", " Outer.A is injected at", " Outer.B(aParam)", " Outer.B is injected at", " Outer.C(bParam)", " Outer.C is injected at", " Outer.CModule.c(c)", " ...", "", "The cycle is requested via:", " Outer.C is requested at", " Outer.CComponent.getC()")) .onSource(component) .onLineContaining("interface CComponent"); }); } @Test public void cyclicDependencyWithSetBinding() { Source component = CompilerTests.javaSource( "test.Outer", "package test;", "", "import dagger.Component;", "import dagger.Module;", "import dagger.Provides;", "import dagger.multibindings.IntoSet;", "import java.util.Set;", "import javax.inject.Inject;", "", "final class Outer {", " static class A {", " @Inject A(Set cSet) {}", " }", "", " static class B {", " @Inject B(A aParam) {}", " }", "", " static class C {", " @Inject C(B bParam) {}", " }", "", " @Component(modules = CModule.class)", " interface CComponent {", " C getC();", " }", "", " @Module", " static class CModule {", " @Provides @IntoSet", " static C c(C c) {", " return c;", " }", " }", "}"); CompilerTests.daggerCompiler(component) .withProcessingOptions(compilerMode.processorOptions()) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( String.join( "\n", "Found a dependency cycle:", " Outer.C is injected at", " Outer.CModule.c(c)", " Set is injected at", " Outer.A(cSet)", " Outer.A is injected at", " Outer.B(aParam)", " Outer.B is injected at", " Outer.C(bParam)", " Outer.C is injected at", " Outer.CModule.c(c)", " ...", "", "The cycle is requested via:", " Outer.C is requested at", " Outer.CComponent.getC()")) .onSource(component) .onLineContaining("interface CComponent"); }); } @Test public void falsePositiveCyclicDependencyIndirectionDetected() { Source component = CompilerTests.javaSource( "test.Outer", "package test;", "", "import dagger.Component;", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Inject;", "import javax.inject.Provider;", "", "final class Outer {", " static class A {", " @Inject A(C cParam) {}", " }", "", " static class B {", " @Inject B(A aParam) {}", " }", "", " static class C {", " @Inject C(B bParam) {}", " }", "", " static class D {", " @Inject D(Provider cParam) {}", " }", "", " @Component", " interface DComponent {", " D getD();", " }", "}"); CompilerTests.daggerCompiler(component) .withProcessingOptions(compilerMode.processorOptions()) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( String.join( "\n", "Found a dependency cycle:", " Outer.C is injected at", " Outer.A(cParam)", " Outer.A is injected at", " Outer.B(aParam)", " Outer.B is injected at", " Outer.C(bParam)", " Outer.C is injected at", " Outer.A(cParam)", " ...", "", "The cycle is requested via:", " Provider is injected at", " Outer.D(cParam)", " Outer.D is requested at", " Outer.DComponent.getD()")) .onSource(component) .onLineContaining("interface DComponent"); }); } @Test public void cyclicDependencyInSubcomponents() { Source parent = CompilerTests.javaSource( "test.Parent", "package test;", "", "import dagger.Component;", "", "@Component", "interface Parent {", " Child.Builder child();", "}"); Source child = CompilerTests.javaSource( "test.Child", "package test;", "", "import dagger.Subcomponent;", "", "@Subcomponent(modules = CycleModule.class)", "interface Child {", " Grandchild.Builder grandchild();", "", " @Subcomponent.Builder", " interface Builder {", " Child build();", " }", "}"); Source grandchild = CompilerTests.javaSource( "test.Grandchild", "package test;", "", "import dagger.Subcomponent;", "", "@Subcomponent", "interface Grandchild {", " String entry();", "", " @Subcomponent.Builder", " interface Builder {", " Grandchild build();", " }", "}"); Source cycleModule = CompilerTests.javaSource( "test.CycleModule", "package test;", "", "import dagger.Module;", "import dagger.Provides;", "", "@Module", "abstract class CycleModule {", " @Provides static Object object(String string) {", " return string;", " }", "", " @Provides static String string(Object object) {", " return object.toString();", " }", "}"); CompilerTests.daggerCompiler(parent, child, grandchild, cycleModule) .withProcessingOptions(compilerMode.processorOptions()) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( String.join( "\n", "Found a dependency cycle:", " String is injected at", " CycleModule.object(string)", " Object is injected at", " CycleModule.string(object)", " String is injected at", " CycleModule.object(string)", " ...", "", "The cycle is requested via:", " String is requested at", " Grandchild.entry()")) .onSource(parent) .onLineContaining("interface Parent"); }); } @Test public void cyclicDependencyInSubcomponentsWithChildren() { Source parent = CompilerTests.javaSource( "test.Parent", "package test;", "", "import dagger.Component;", "", "@Component", "interface Parent {", " Child.Builder child();", "}"); Source child = CompilerTests.javaSource( "test.Child", "package test;", "", "import dagger.Subcomponent;", "", "@Subcomponent(modules = CycleModule.class)", "interface Child {", " String entry();", "", " Grandchild.Builder grandchild();", "", " @Subcomponent.Builder", " interface Builder {", " Child build();", " }", "}"); // Grandchild has no entry point that depends on the cycle. http://b/111317986 Source grandchild = CompilerTests.javaSource( "test.Grandchild", "package test;", "", "import dagger.Subcomponent;", "", "@Subcomponent", "interface Grandchild {", "", " @Subcomponent.Builder", " interface Builder {", " Grandchild build();", " }", "}"); Source cycleModule = CompilerTests.javaSource( "test.CycleModule", "package test;", "", "import dagger.Module;", "import dagger.Provides;", "", "@Module", "abstract class CycleModule {", " @Provides static Object object(String string) {", " return string;", " }", "", " @Provides static String string(Object object) {", " return object.toString();", " }", "}"); CompilerTests.daggerCompiler(parent, child, grandchild, cycleModule) .withProcessingOptions(compilerMode.processorOptions()) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( String.join( "\n", "Found a dependency cycle:", " String is injected at", " CycleModule.object(string)", " Object is injected at", " CycleModule.string(object)", " String is injected at", " CycleModule.object(string)", " ...", "", "The cycle is requested via:", " String is requested at", " Child.entry() [Parent → Child]")) .onSource(parent) .onLineContaining("interface Parent"); }); } @Test public void circularBindsMethods() { Source qualifier = CompilerTests.javaSource( "test.SomeQualifier", "package test;", "", "import javax.inject.Qualifier;", "", "@Qualifier @interface SomeQualifier {}"); Source module = CompilerTests.javaSource( "test.TestModule", "package test;", "", "import dagger.Binds;", "import dagger.Module;", "", "@Module", "abstract class TestModule {", " @Binds abstract Object bindUnqualified(@SomeQualifier Object qualified);", " @Binds @SomeQualifier abstract Object bindQualified(Object unqualified);", "}"); Source component = CompilerTests.javaSource( "test.TestComponent", "package test;", "", "import dagger.Component;", "", "@Component(modules = TestModule.class)", "interface TestComponent {", " Object unqualified();", "}"); CompilerTests.daggerCompiler(qualifier, module, component) .withProcessingOptions(compilerMode.processorOptions()) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( String.join( "\n", "Found a dependency cycle:", " Object is injected at", " TestModule.bindQualified(unqualified)", " @SomeQualifier Object is injected at", " TestModule.bindUnqualified(qualified)", " Object is injected at", " TestModule.bindQualified(unqualified)", " ...", "", "The cycle is requested via:", " Object is requested at", " TestComponent.unqualified()")) .onSource(component) .onLineContaining("interface TestComponent"); }); } @Test public void selfReferentialBinds() { Source module = CompilerTests.javaSource( "test.TestModule", "package test;", "", "import dagger.Binds;", "import dagger.Module;", "", "@Module", "abstract class TestModule {", " @Binds abstract Object bindToSelf(Object sameKey);", "}"); Source component = CompilerTests.javaSource( "test.TestComponent", "package test;", "", "import dagger.Component;", "", "@Component(modules = TestModule.class)", "interface TestComponent {", " Object selfReferential();", "}"); CompilerTests.daggerCompiler(module, component) .withProcessingOptions(compilerMode.processorOptions()) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( String.join( "\n", "Found a dependency cycle:", " Object is injected at", " TestModule.bindToSelf(sameKey)", " Object is injected at", " TestModule.bindToSelf(sameKey)", " ...", "", "The cycle is requested via:", " Object is requested at", " TestComponent.selfReferential()")) .onSource(component) .onLineContaining("interface TestComponent"); }); } @Test public void cycleFromMembersInjectionMethod_WithSameKeyAsMembersInjectionMethod() { Source a = CompilerTests.javaSource( "test.A", "package test;", "", "import javax.inject.Inject;", "", "class A {", " @Inject A() {}", " @Inject B b;", "}"); Source b = CompilerTests.javaSource( "test.B", "package test;", "", "import javax.inject.Inject;", "", "class B {", " @Inject B() {}", " @Inject A a;", "}"); Source component = CompilerTests.javaSource( "test.CycleComponent", "package test;", "", "import dagger.Component;", "", "@Component", "interface CycleComponent {", " void inject(A a);", "}"); CompilerTests.daggerCompiler(a, b, component) .withProcessingOptions(compilerMode.processorOptions()) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( String.join( "\n", "Found a dependency cycle:", " test.B is injected at", " test.A.b", " test.A is injected at", " test.B.a", " test.B is injected at", " test.A.b", " ...", "", "The cycle is requested via:", " test.B is injected at", " test.A.b", " test.A is injected at", " CycleComponent.inject(test.A)")) .onSource(component) .onLineContaining("interface CycleComponent"); }); } @Test public void longCycleMaskedByShortBrokenCycles() { Source cycles = CompilerTests.javaSource( "test.Cycles", "package test;", "", "import javax.inject.Inject;", "import javax.inject.Provider;", "import dagger.Component;", "", "final class Cycles {", " static class A {", " @Inject A(Provider aProvider, B b) {}", " }", "", " static class B {", " @Inject B(Provider bProvider, A a) {}", " }", "", " @Component", " interface C {", " A a();", " }", "}"); CompilerTests.daggerCompiler(cycles) .withProcessingOptions(compilerMode.processorOptions()) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining("Found a dependency cycle:") .onSource(cycles) .onLineContaining("interface C"); }); } }