1 /*
2  * Copyright (C) 2018 The Dagger Authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package dagger.internal.codegen;
18 
19 import static com.google.testing.compile.CompilationSubject.assertThat;
20 import static dagger.internal.codegen.Compilers.compilerWithOptions;
21 import static dagger.internal.codegen.Compilers.daggerCompiler;
22 import static dagger.internal.codegen.TestUtils.endsWithMessage;
23 import static dagger.internal.codegen.TestUtils.message;
24 
25 import com.google.testing.compile.Compilation;
26 import com.google.testing.compile.JavaFileObjects;
27 import java.util.regex.Pattern;
28 import javax.tools.JavaFileObject;
29 import org.junit.Test;
30 import org.junit.runner.RunWith;
31 import org.junit.runners.JUnit4;
32 
33 @RunWith(JUnit4.class)
34 public class DependencyCycleValidationTest {
35   private static final JavaFileObject SIMPLE_CYCLIC_DEPENDENCY =
36       JavaFileObjects.forSourceLines(
37           "test.Outer",
38           "package test;",
39           "",
40           "import dagger.Binds;",
41           "import dagger.Component;",
42           "import dagger.Module;",
43           "import dagger.Provides;",
44           "import javax.inject.Inject;",
45           "",
46           "final class Outer {",
47           "  static class A {",
48           "    @Inject A(C cParam) {}",
49           "  }",
50           "",
51           "  static class B {",
52           "    @Inject B(A aParam) {}",
53           "  }",
54           "",
55           "  static class C {",
56           "    @Inject C(B bParam) {}",
57           "  }",
58           "",
59           "  @Module",
60           "  interface MModule {",
61           "    @Binds Object object(C c);",
62           "  }",
63           "",
64           "  @Component",
65           "  interface CComponent {",
66           "    C getC();",
67           "  }",
68           "}");
69 
70   @Test
cyclicDependency()71   public void cyclicDependency() {
72     Compilation compilation = daggerCompiler().compile(SIMPLE_CYCLIC_DEPENDENCY);
73     assertThat(compilation).failed();
74 
75     assertThat(compilation)
76         .hadErrorContaining(
77             message(
78                 "Found a dependency cycle:",
79                 "    Outer.C is injected at",
80                 "        Outer.A(cParam)",
81                 "    Outer.A is injected at",
82                 "        Outer.B(aParam)",
83                 "    Outer.B is injected at",
84                 "        Outer.C(bParam)",
85                 "    Outer.C is requested at",
86                 "        Outer.CComponent.getC()"))
87         .inFile(SIMPLE_CYCLIC_DEPENDENCY)
88         .onLineContaining("interface CComponent");
89 
90     assertThat(compilation).hadErrorCount(1);
91   }
92 
93   @Test
cyclicDependencyWithModuleBindingValidation()94   public void cyclicDependencyWithModuleBindingValidation() {
95     // Cycle errors should not show a dependency trace to an entry point when doing full binding
96     // graph validation. So ensure that the message doesn't end with "test.Outer.C is requested at
97     // test.Outer.CComponent.getC()", as the previous test's message does.
98     Pattern moduleBindingValidationError =
99         endsWithMessage(
100             "Found a dependency cycle:",
101             "    Outer.C is injected at",
102             "        Outer.A(cParam)",
103             "    Outer.A is injected at",
104             "        Outer.B(aParam)",
105             "    Outer.B is injected at",
106             "        Outer.C(bParam)",
107             "",
108             "======================",
109             "Full classname legend:",
110             "======================",
111             "Outer: test.Outer",
112             "========================",
113             "End of classname legend:",
114             "========================");
115 
116     Compilation compilation =
117         compilerWithOptions("-Adagger.fullBindingGraphValidation=ERROR")
118             .compile(SIMPLE_CYCLIC_DEPENDENCY);
119     assertThat(compilation).failed();
120 
121     assertThat(compilation)
122         .hadErrorContainingMatch(moduleBindingValidationError)
123         .inFile(SIMPLE_CYCLIC_DEPENDENCY)
124         .onLineContaining("interface MModule");
125 
126     assertThat(compilation)
127         .hadErrorContainingMatch(moduleBindingValidationError)
128         .inFile(SIMPLE_CYCLIC_DEPENDENCY)
129         .onLineContaining("interface CComponent");
130 
131     assertThat(compilation).hadErrorCount(2);
132   }
133 
cyclicDependencyNotIncludingEntryPoint()134   @Test public void cyclicDependencyNotIncludingEntryPoint() {
135     JavaFileObject component =
136         JavaFileObjects.forSourceLines(
137             "test.Outer",
138             "package test;",
139             "",
140             "import dagger.Component;",
141             "import dagger.Module;",
142             "import dagger.Provides;",
143             "import javax.inject.Inject;",
144             "",
145             "final class Outer {",
146             "  static class A {",
147             "    @Inject A(C cParam) {}",
148             "  }",
149             "",
150             "  static class B {",
151             "    @Inject B(A aParam) {}",
152             "  }",
153             "",
154             "  static class C {",
155             "    @Inject C(B bParam) {}",
156             "  }",
157             "",
158             "  static class D {",
159             "    @Inject D(C cParam) {}",
160             "  }",
161             "",
162             "  @Component",
163             "  interface DComponent {",
164             "    D getD();",
165             "  }",
166             "}");
167 
168     Compilation compilation = daggerCompiler().compile(component);
169     assertThat(compilation).failed();
170     assertThat(compilation)
171         .hadErrorContaining(
172             message(
173                 "Found a dependency cycle:",
174                 "    Outer.C is injected at",
175                 "        Outer.A(cParam)",
176                 "    Outer.A is injected at",
177                 "        Outer.B(aParam)",
178                 "    Outer.B is injected at",
179                 "        Outer.C(bParam)",
180                 "    Outer.C is injected at",
181                 "        Outer.D(cParam)",
182                 "    Outer.D is requested at",
183                 "        Outer.DComponent.getD()"))
184         .inFile(component)
185         .onLineContaining("interface DComponent");
186   }
187 
188   @Test
cyclicDependencyNotBrokenByMapBinding()189   public void cyclicDependencyNotBrokenByMapBinding() {
190     JavaFileObject component =
191         JavaFileObjects.forSourceLines(
192             "test.Outer",
193             "package test;",
194             "",
195             "import dagger.Component;",
196             "import dagger.Module;",
197             "import dagger.Provides;",
198             "import dagger.multibindings.IntoMap;",
199             "import dagger.multibindings.StringKey;",
200             "import java.util.Map;",
201             "import javax.inject.Inject;",
202             "",
203             "final class Outer {",
204             "  static class A {",
205             "    @Inject A(Map<String, C> cMap) {}",
206             "  }",
207             "",
208             "  static class B {",
209             "    @Inject B(A aParam) {}",
210             "  }",
211             "",
212             "  static class C {",
213             "    @Inject C(B bParam) {}",
214             "  }",
215             "",
216             "  @Component(modules = CModule.class)",
217             "  interface CComponent {",
218             "    C getC();",
219             "  }",
220             "",
221             "  @Module",
222             "  static class CModule {",
223             "    @Provides @IntoMap",
224             "    @StringKey(\"C\")",
225             "    static C c(C c) {",
226             "      return c;",
227             "    }",
228             "  }",
229             "}");
230 
231     Compilation compilation = daggerCompiler().compile(component);
232     assertThat(compilation).failed();
233     assertThat(compilation)
234         .hadErrorContaining(
235             message(
236                 "Found a dependency cycle:",
237                 "    Outer.C is injected at",
238                 "        Outer.CModule.c(c)",
239                 "    Map<String,Outer.C> is injected at",
240                 "        Outer.A(cMap)",
241                 "    Outer.A is injected at",
242                 "        Outer.B(aParam)",
243                 "    Outer.B is injected at",
244                 "        Outer.C(bParam)",
245                 "    Outer.C is requested at",
246                 "        Outer.CComponent.getC()"))
247         .inFile(component)
248         .onLineContaining("interface CComponent");
249   }
250 
251   @Test
cyclicDependencyWithSetBinding()252   public void cyclicDependencyWithSetBinding() {
253     JavaFileObject component =
254         JavaFileObjects.forSourceLines(
255             "test.Outer",
256             "package test;",
257             "",
258             "import dagger.Component;",
259             "import dagger.Module;",
260             "import dagger.Provides;",
261             "import dagger.multibindings.IntoSet;",
262             "import java.util.Set;",
263             "import javax.inject.Inject;",
264             "",
265             "final class Outer {",
266             "  static class A {",
267             "    @Inject A(Set<C> cSet) {}",
268             "  }",
269             "",
270             "  static class B {",
271             "    @Inject B(A aParam) {}",
272             "  }",
273             "",
274             "  static class C {",
275             "    @Inject C(B bParam) {}",
276             "  }",
277             "",
278             "  @Component(modules = CModule.class)",
279             "  interface CComponent {",
280             "    C getC();",
281             "  }",
282             "",
283             "  @Module",
284             "  static class CModule {",
285             "    @Provides @IntoSet",
286             "    static C c(C c) {",
287             "      return c;",
288             "    }",
289             "  }",
290             "}");
291 
292     Compilation compilation = daggerCompiler().compile(component);
293     assertThat(compilation).failed();
294     assertThat(compilation)
295         .hadErrorContaining(
296             message(
297                 "Found a dependency cycle:",
298                 "    Outer.C is injected at",
299                 "        Outer.CModule.c(c)",
300                 "    Set<Outer.C> is injected at",
301                 "        Outer.A(cSet)",
302                 "    Outer.A is injected at",
303                 "        Outer.B(aParam)",
304                 "    Outer.B is injected at",
305                 "        Outer.C(bParam)",
306                 "    Outer.C is requested at",
307                 "        Outer.CComponent.getC()"))
308         .inFile(component)
309         .onLineContaining("interface CComponent");
310   }
311 
312   @Test
falsePositiveCyclicDependencyIndirectionDetected()313   public void falsePositiveCyclicDependencyIndirectionDetected() {
314     JavaFileObject component =
315         JavaFileObjects.forSourceLines(
316             "test.Outer",
317             "package test;",
318             "",
319             "import dagger.Component;",
320             "import dagger.Module;",
321             "import dagger.Provides;",
322             "import javax.inject.Inject;",
323             "import javax.inject.Provider;",
324             "",
325             "final class Outer {",
326             "  static class A {",
327             "    @Inject A(C cParam) {}",
328             "  }",
329             "",
330             "  static class B {",
331             "    @Inject B(A aParam) {}",
332             "  }",
333             "",
334             "  static class C {",
335             "    @Inject C(B bParam) {}",
336             "  }",
337             "",
338             "  static class D {",
339             "    @Inject D(Provider<C> cParam) {}",
340             "  }",
341             "",
342             "  @Component",
343             "  interface DComponent {",
344             "    D getD();",
345             "  }",
346             "}");
347 
348     Compilation compilation = daggerCompiler().compile(component);
349     assertThat(compilation).failed();
350     assertThat(compilation)
351         .hadErrorContaining(
352             message(
353                 "Found a dependency cycle:",
354                 "    Outer.C is injected at",
355                 "        Outer.A(cParam)",
356                 "    Outer.A is injected at",
357                 "        Outer.B(aParam)",
358                 "    Outer.B is injected at",
359                 "        Outer.C(bParam)",
360                 "    Provider<Outer.C> is injected at",
361                 "        Outer.D(cParam)",
362                 "    Outer.D is requested at",
363                 "        Outer.DComponent.getD()"))
364         .inFile(component)
365         .onLineContaining("interface DComponent");
366   }
367 
368   @Test
cyclicDependencyInSubcomponents()369   public void cyclicDependencyInSubcomponents() {
370     JavaFileObject parent =
371         JavaFileObjects.forSourceLines(
372             "test.Parent",
373             "package test;",
374             "",
375             "import dagger.Component;",
376             "",
377             "@Component",
378             "interface Parent {",
379             "  Child.Builder child();",
380             "}");
381     JavaFileObject child =
382         JavaFileObjects.forSourceLines(
383             "test.Child",
384             "package test;",
385             "",
386             "import dagger.Subcomponent;",
387             "",
388             "@Subcomponent(modules = CycleModule.class)",
389             "interface Child {",
390             "  Grandchild.Builder grandchild();",
391             "",
392             "  @Subcomponent.Builder",
393             "  interface Builder {",
394             "    Child build();",
395             "  }",
396             "}");
397     JavaFileObject grandchild =
398         JavaFileObjects.forSourceLines(
399             "test.Grandchild",
400             "package test;",
401             "",
402             "import dagger.Subcomponent;",
403             "",
404             "@Subcomponent",
405             "interface Grandchild {",
406             "  String entry();",
407             "",
408             "  @Subcomponent.Builder",
409             "  interface Builder {",
410             "    Grandchild build();",
411             "  }",
412             "}");
413     JavaFileObject cycleModule =
414         JavaFileObjects.forSourceLines(
415             "test.CycleModule",
416             "package test;",
417             "",
418             "import dagger.Module;",
419             "import dagger.Provides;",
420             "",
421             "@Module",
422             "abstract class CycleModule {",
423             "  @Provides static Object object(String string) {",
424             "    return string;",
425             "  }",
426             "",
427             "  @Provides static String string(Object object) {",
428             "    return object.toString();",
429             "  }",
430             "}");
431 
432     Compilation compilation = daggerCompiler().compile(parent, child, grandchild, cycleModule);
433     assertThat(compilation).failed();
434     assertThat(compilation)
435         .hadErrorContaining(
436             message(
437                 "Found a dependency cycle:",
438                 "    String is injected at",
439                 "        CycleModule.object(string)",
440                 "    Object is injected at",
441                 "        CycleModule.string(object)",
442                 "    String is requested at",
443                 "        Grandchild.entry()"))
444         .inFile(parent)
445         .onLineContaining("interface Parent");
446   }
447 
448   @Test
cyclicDependencyInSubcomponentsWithChildren()449   public void cyclicDependencyInSubcomponentsWithChildren() {
450     JavaFileObject parent =
451         JavaFileObjects.forSourceLines(
452             "test.Parent",
453             "package test;",
454             "",
455             "import dagger.Component;",
456             "",
457             "@Component",
458             "interface Parent {",
459             "  Child.Builder child();",
460             "}");
461     JavaFileObject child =
462         JavaFileObjects.forSourceLines(
463             "test.Child",
464             "package test;",
465             "",
466             "import dagger.Subcomponent;",
467             "",
468             "@Subcomponent(modules = CycleModule.class)",
469             "interface Child {",
470             "  String entry();",
471             "",
472             "  Grandchild.Builder grandchild();",
473             "",
474             "  @Subcomponent.Builder",
475             "  interface Builder {",
476             "    Child build();",
477             "  }",
478             "}");
479     // Grandchild has no entry point that depends on the cycle. http://b/111317986
480     JavaFileObject grandchild =
481         JavaFileObjects.forSourceLines(
482             "test.Grandchild",
483             "package test;",
484             "",
485             "import dagger.Subcomponent;",
486             "",
487             "@Subcomponent",
488             "interface Grandchild {",
489             "",
490             "  @Subcomponent.Builder",
491             "  interface Builder {",
492             "    Grandchild build();",
493             "  }",
494             "}");
495     JavaFileObject cycleModule =
496         JavaFileObjects.forSourceLines(
497             "test.CycleModule",
498             "package test;",
499             "",
500             "import dagger.Module;",
501             "import dagger.Provides;",
502             "",
503             "@Module",
504             "abstract class CycleModule {",
505             "  @Provides static Object object(String string) {",
506             "    return string;",
507             "  }",
508             "",
509             "  @Provides static String string(Object object) {",
510             "    return object.toString();",
511             "  }",
512             "}");
513 
514     Compilation compilation = daggerCompiler().compile(parent, child, grandchild, cycleModule);
515     assertThat(compilation).failed();
516     assertThat(compilation)
517         .hadErrorContaining(
518             message(
519                 "Found a dependency cycle:",
520                 "    String is injected at",
521                 "        CycleModule.object(string)",
522                 "    Object is injected at",
523                 "        CycleModule.string(object)",
524                 "    String is requested at",
525                 "        Child.entry() [Parent → Child]"))
526         .inFile(parent)
527         .onLineContaining("interface Parent");
528   }
529 
530   @Test
circularBindsMethods()531   public void circularBindsMethods() {
532     JavaFileObject qualifier =
533         JavaFileObjects.forSourceLines(
534             "test.SomeQualifier",
535             "package test;",
536             "",
537             "import javax.inject.Qualifier;",
538             "",
539             "@Qualifier @interface SomeQualifier {}");
540     JavaFileObject module =
541         JavaFileObjects.forSourceLines(
542             "test.TestModule",
543             "package test;",
544             "",
545             "import dagger.Binds;",
546             "import dagger.Module;",
547             "",
548             "@Module",
549             "abstract class TestModule {",
550             "  @Binds abstract Object bindUnqualified(@SomeQualifier Object qualified);",
551             "  @Binds @SomeQualifier abstract Object bindQualified(Object unqualified);",
552             "}");
553     JavaFileObject component =
554         JavaFileObjects.forSourceLines(
555             "test.TestComponent",
556             "package test;",
557             "",
558             "import dagger.Component;",
559             "",
560             "@Component(modules = TestModule.class)",
561             "interface TestComponent {",
562             "  Object unqualified();",
563             "}");
564 
565     Compilation compilation = daggerCompiler().compile(qualifier, module, component);
566     assertThat(compilation).failed();
567     assertThat(compilation)
568         .hadErrorContaining(
569             message(
570                 "Found a dependency cycle:",
571                 "    Object is injected at",
572                 "        TestModule.bindQualified(unqualified)",
573                 "    @SomeQualifier Object is injected at",
574                 "        TestModule.bindUnqualified(qualified)",
575                 "    Object is requested at",
576                 "        TestComponent.unqualified()"))
577         .inFile(component)
578         .onLineContaining("interface TestComponent");
579   }
580 
581   @Test
selfReferentialBinds()582   public void selfReferentialBinds() {
583     JavaFileObject module =
584         JavaFileObjects.forSourceLines(
585             "test.TestModule",
586             "package test;",
587             "",
588             "import dagger.Binds;",
589             "import dagger.Module;",
590             "",
591             "@Module",
592             "abstract class TestModule {",
593             "  @Binds abstract Object bindToSelf(Object sameKey);",
594             "}");
595     JavaFileObject component =
596         JavaFileObjects.forSourceLines(
597             "test.TestComponent",
598             "package test;",
599             "",
600             "import dagger.Component;",
601             "",
602             "@Component(modules = TestModule.class)",
603             "interface TestComponent {",
604             "  Object selfReferential();",
605             "}");
606 
607     Compilation compilation = daggerCompiler().compile(module, component);
608     assertThat(compilation).failed();
609     assertThat(compilation)
610         .hadErrorContaining(
611             message(
612                 "Found a dependency cycle:",
613                 "    Object is injected at",
614                 "        TestModule.bindToSelf(sameKey)",
615                 "    Object is requested at",
616                 "        TestComponent.selfReferential()"))
617         .inFile(component)
618         .onLineContaining("interface TestComponent");
619   }
620 
621   @Test
cycleFromMembersInjectionMethod_WithSameKeyAsMembersInjectionMethod()622   public void cycleFromMembersInjectionMethod_WithSameKeyAsMembersInjectionMethod() {
623     JavaFileObject a =
624         JavaFileObjects.forSourceLines(
625             "test.A",
626             "package test;",
627             "",
628             "import javax.inject.Inject;",
629             "",
630             "class A {",
631             "  @Inject A() {}",
632             "  @Inject B b;",
633             "}");
634     JavaFileObject b =
635         JavaFileObjects.forSourceLines(
636             "test.B",
637             "package test;",
638             "",
639             "import javax.inject.Inject;",
640             "",
641             "class B {",
642             "  @Inject B() {}",
643             "  @Inject A a;",
644             "}");
645     JavaFileObject component =
646         JavaFileObjects.forSourceLines(
647             "test.CycleComponent",
648             "package test;",
649             "",
650             "import dagger.Component;",
651             "",
652             "@Component",
653             "interface CycleComponent {",
654             "  void inject(A a);",
655             "}");
656 
657     Compilation compilation = daggerCompiler().compile(a, b, component);
658     assertThat(compilation).failed();
659     assertThat(compilation)
660         .hadErrorContaining(
661             message(
662                 "Found a dependency cycle:",
663                 "    test.B is injected at",
664                 "        test.A.b",
665                 "    test.A is injected at",
666                 "        test.B.a",
667                 "    test.B is injected at",
668                 "        test.A.b",
669                 "    test.A is injected at",
670                 "        CycleComponent.inject(test.A)"))
671         .inFile(component)
672         .onLineContaining("interface CycleComponent");
673   }
674 
675   @Test
longCycleMaskedByShortBrokenCycles()676   public void longCycleMaskedByShortBrokenCycles() {
677     JavaFileObject cycles =
678         JavaFileObjects.forSourceLines(
679             "test.Cycles",
680             "package test;",
681             "",
682             "import javax.inject.Inject;",
683             "import javax.inject.Provider;",
684             "import dagger.Component;",
685             "",
686             "final class Cycles {",
687             "  static class A {",
688             "    @Inject A(Provider<A> aProvider, B b) {}",
689             "  }",
690             "",
691             "  static class B {",
692             "    @Inject B(Provider<B> bProvider, A a) {}",
693             "  }",
694             "",
695             "  @Component",
696             "  interface C {",
697             "    A a();",
698             "  }",
699             "}");
700     Compilation compilation = daggerCompiler().compile(cycles);
701     assertThat(compilation).failed();
702     assertThat(compilation)
703         .hadErrorContaining("Found a dependency cycle:")
704         .inFile(cycles)
705         .onLineContaining("interface C");
706   }
707 }
708