• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 injected at",
86                 "        Outer.A(cParam)",
87                 "    ...",
88                 "",
89                 "The cycle is requested via:",
90                 "    Outer.C is requested at",
91                 "        Outer.CComponent.getC()"))
92         .inFile(SIMPLE_CYCLIC_DEPENDENCY)
93         .onLineContaining("interface CComponent");
94 
95     assertThat(compilation).hadErrorCount(1);
96   }
97 
98   @Test
cyclicDependencyWithModuleBindingValidation()99   public void cyclicDependencyWithModuleBindingValidation() {
100     // Cycle errors should not show a dependency trace to an entry point when doing full binding
101     // graph validation. So ensure that the message doesn't end with "test.Outer.C is requested at
102     // test.Outer.CComponent.getC()", as the previous test's message does.
103     Pattern moduleBindingValidationError =
104         endsWithMessage(
105             "Found a dependency cycle:",
106             "    Outer.C is injected at",
107             "        Outer.A(cParam)",
108             "    Outer.A is injected at",
109             "        Outer.B(aParam)",
110             "    Outer.B is injected at",
111             "        Outer.C(bParam)",
112             "    Outer.C is injected at",
113             "        Outer.A(cParam)",
114             "    ...",
115             "",
116             "======================",
117             "Full classname legend:",
118             "======================",
119             "Outer: test.Outer",
120             "========================",
121             "End of classname legend:",
122             "========================");
123 
124     Compilation compilation =
125         compilerWithOptions("-Adagger.fullBindingGraphValidation=ERROR")
126             .compile(SIMPLE_CYCLIC_DEPENDENCY);
127     assertThat(compilation).failed();
128 
129     assertThat(compilation)
130         .hadErrorContainingMatch(moduleBindingValidationError)
131         .inFile(SIMPLE_CYCLIC_DEPENDENCY)
132         .onLineContaining("interface MModule");
133 
134     assertThat(compilation)
135         .hadErrorContainingMatch(moduleBindingValidationError)
136         .inFile(SIMPLE_CYCLIC_DEPENDENCY)
137         .onLineContaining("interface CComponent");
138 
139     assertThat(compilation).hadErrorCount(2);
140   }
141 
cyclicDependencyNotIncludingEntryPoint()142   @Test public void cyclicDependencyNotIncludingEntryPoint() {
143     JavaFileObject component =
144         JavaFileObjects.forSourceLines(
145             "test.Outer",
146             "package test;",
147             "",
148             "import dagger.Component;",
149             "import dagger.Module;",
150             "import dagger.Provides;",
151             "import javax.inject.Inject;",
152             "",
153             "final class Outer {",
154             "  static class A {",
155             "    @Inject A(C cParam) {}",
156             "  }",
157             "",
158             "  static class B {",
159             "    @Inject B(A aParam) {}",
160             "  }",
161             "",
162             "  static class C {",
163             "    @Inject C(B bParam) {}",
164             "  }",
165             "",
166             "  static class D {",
167             "    @Inject D(C cParam) {}",
168             "  }",
169             "",
170             "  @Component",
171             "  interface DComponent {",
172             "    D getD();",
173             "  }",
174             "}");
175 
176     Compilation compilation = daggerCompiler().compile(component);
177     assertThat(compilation).failed();
178     assertThat(compilation)
179         .hadErrorContaining(
180             message(
181                 "Found a dependency cycle:",
182                 "    Outer.C is injected at",
183                 "        Outer.A(cParam)",
184                 "    Outer.A is injected at",
185                 "        Outer.B(aParam)",
186                 "    Outer.B is injected at",
187                 "        Outer.C(bParam)",
188                 "    Outer.C is injected at",
189                 "        Outer.A(cParam)",
190                 "    ...",
191                 "",
192                 "The cycle is requested via:",
193                 "    Outer.C is injected at",
194                 "        Outer.D(cParam)",
195                 "    Outer.D is requested at",
196                 "        Outer.DComponent.getD()"))
197         .inFile(component)
198         .onLineContaining("interface DComponent");
199   }
200 
201   @Test
cyclicDependencyNotBrokenByMapBinding()202   public void cyclicDependencyNotBrokenByMapBinding() {
203     JavaFileObject component =
204         JavaFileObjects.forSourceLines(
205             "test.Outer",
206             "package test;",
207             "",
208             "import dagger.Component;",
209             "import dagger.Module;",
210             "import dagger.Provides;",
211             "import dagger.multibindings.IntoMap;",
212             "import dagger.multibindings.StringKey;",
213             "import java.util.Map;",
214             "import javax.inject.Inject;",
215             "",
216             "final class Outer {",
217             "  static class A {",
218             "    @Inject A(Map<String, C> cMap) {}",
219             "  }",
220             "",
221             "  static class B {",
222             "    @Inject B(A aParam) {}",
223             "  }",
224             "",
225             "  static class C {",
226             "    @Inject C(B bParam) {}",
227             "  }",
228             "",
229             "  @Component(modules = CModule.class)",
230             "  interface CComponent {",
231             "    C getC();",
232             "  }",
233             "",
234             "  @Module",
235             "  static class CModule {",
236             "    @Provides @IntoMap",
237             "    @StringKey(\"C\")",
238             "    static C c(C c) {",
239             "      return c;",
240             "    }",
241             "  }",
242             "}");
243 
244     Compilation compilation = daggerCompiler().compile(component);
245     assertThat(compilation).failed();
246     assertThat(compilation)
247         .hadErrorContaining(
248             message(
249                 "Found a dependency cycle:",
250                 "    Outer.C is injected at",
251                 "        Outer.CModule.c(c)",
252                 "    Map<String,Outer.C> is injected at",
253                 "        Outer.A(cMap)",
254                 "    Outer.A is injected at",
255                 "        Outer.B(aParam)",
256                 "    Outer.B is injected at",
257                 "        Outer.C(bParam)",
258                 "    Outer.C is injected at",
259                 "        Outer.CModule.c(c)",
260                 "    ...",
261                 "",
262                 "The cycle is requested via:",
263                 "    Outer.C is requested at",
264                 "        Outer.CComponent.getC()"))
265         .inFile(component)
266         .onLineContaining("interface CComponent");
267   }
268 
269   @Test
cyclicDependencyWithSetBinding()270   public void cyclicDependencyWithSetBinding() {
271     JavaFileObject component =
272         JavaFileObjects.forSourceLines(
273             "test.Outer",
274             "package test;",
275             "",
276             "import dagger.Component;",
277             "import dagger.Module;",
278             "import dagger.Provides;",
279             "import dagger.multibindings.IntoSet;",
280             "import java.util.Set;",
281             "import javax.inject.Inject;",
282             "",
283             "final class Outer {",
284             "  static class A {",
285             "    @Inject A(Set<C> cSet) {}",
286             "  }",
287             "",
288             "  static class B {",
289             "    @Inject B(A aParam) {}",
290             "  }",
291             "",
292             "  static class C {",
293             "    @Inject C(B bParam) {}",
294             "  }",
295             "",
296             "  @Component(modules = CModule.class)",
297             "  interface CComponent {",
298             "    C getC();",
299             "  }",
300             "",
301             "  @Module",
302             "  static class CModule {",
303             "    @Provides @IntoSet",
304             "    static C c(C c) {",
305             "      return c;",
306             "    }",
307             "  }",
308             "}");
309 
310     Compilation compilation = daggerCompiler().compile(component);
311     assertThat(compilation).failed();
312     assertThat(compilation)
313         .hadErrorContaining(
314             message(
315                 "Found a dependency cycle:",
316                 "    Outer.C is injected at",
317                 "        Outer.CModule.c(c)",
318                 "    Set<Outer.C> is injected at",
319                 "        Outer.A(cSet)",
320                 "    Outer.A is injected at",
321                 "        Outer.B(aParam)",
322                 "    Outer.B is injected at",
323                 "        Outer.C(bParam)",
324                 "    Outer.C is injected at",
325                 "        Outer.CModule.c(c)",
326                 "    ...",
327                 "",
328                 "The cycle is requested via:",
329                 "    Outer.C is requested at",
330                 "        Outer.CComponent.getC()"))
331         .inFile(component)
332         .onLineContaining("interface CComponent");
333   }
334 
335   @Test
falsePositiveCyclicDependencyIndirectionDetected()336   public void falsePositiveCyclicDependencyIndirectionDetected() {
337     JavaFileObject component =
338         JavaFileObjects.forSourceLines(
339             "test.Outer",
340             "package test;",
341             "",
342             "import dagger.Component;",
343             "import dagger.Module;",
344             "import dagger.Provides;",
345             "import javax.inject.Inject;",
346             "import javax.inject.Provider;",
347             "",
348             "final class Outer {",
349             "  static class A {",
350             "    @Inject A(C cParam) {}",
351             "  }",
352             "",
353             "  static class B {",
354             "    @Inject B(A aParam) {}",
355             "  }",
356             "",
357             "  static class C {",
358             "    @Inject C(B bParam) {}",
359             "  }",
360             "",
361             "  static class D {",
362             "    @Inject D(Provider<C> cParam) {}",
363             "  }",
364             "",
365             "  @Component",
366             "  interface DComponent {",
367             "    D getD();",
368             "  }",
369             "}");
370 
371     Compilation compilation = daggerCompiler().compile(component);
372     assertThat(compilation).failed();
373     assertThat(compilation)
374         .hadErrorContaining(
375             message(
376                 "Found a dependency cycle:",
377                 "    Outer.C is injected at",
378                 "        Outer.A(cParam)",
379                 "    Outer.A is injected at",
380                 "        Outer.B(aParam)",
381                 "    Outer.B is injected at",
382                 "        Outer.C(bParam)",
383                 "    Outer.C is injected at",
384                 "        Outer.A(cParam)",
385                 "    ...",
386                 "",
387                 "The cycle is requested via:",
388                 "    Provider<Outer.C> is injected at",
389                 "        Outer.D(cParam)",
390                 "    Outer.D is requested at",
391                 "        Outer.DComponent.getD()"))
392         .inFile(component)
393         .onLineContaining("interface DComponent");
394   }
395 
396   @Test
cyclicDependencyInSubcomponents()397   public void cyclicDependencyInSubcomponents() {
398     JavaFileObject parent =
399         JavaFileObjects.forSourceLines(
400             "test.Parent",
401             "package test;",
402             "",
403             "import dagger.Component;",
404             "",
405             "@Component",
406             "interface Parent {",
407             "  Child.Builder child();",
408             "}");
409     JavaFileObject child =
410         JavaFileObjects.forSourceLines(
411             "test.Child",
412             "package test;",
413             "",
414             "import dagger.Subcomponent;",
415             "",
416             "@Subcomponent(modules = CycleModule.class)",
417             "interface Child {",
418             "  Grandchild.Builder grandchild();",
419             "",
420             "  @Subcomponent.Builder",
421             "  interface Builder {",
422             "    Child build();",
423             "  }",
424             "}");
425     JavaFileObject grandchild =
426         JavaFileObjects.forSourceLines(
427             "test.Grandchild",
428             "package test;",
429             "",
430             "import dagger.Subcomponent;",
431             "",
432             "@Subcomponent",
433             "interface Grandchild {",
434             "  String entry();",
435             "",
436             "  @Subcomponent.Builder",
437             "  interface Builder {",
438             "    Grandchild build();",
439             "  }",
440             "}");
441     JavaFileObject cycleModule =
442         JavaFileObjects.forSourceLines(
443             "test.CycleModule",
444             "package test;",
445             "",
446             "import dagger.Module;",
447             "import dagger.Provides;",
448             "",
449             "@Module",
450             "abstract class CycleModule {",
451             "  @Provides static Object object(String string) {",
452             "    return string;",
453             "  }",
454             "",
455             "  @Provides static String string(Object object) {",
456             "    return object.toString();",
457             "  }",
458             "}");
459 
460     Compilation compilation = daggerCompiler().compile(parent, child, grandchild, cycleModule);
461     assertThat(compilation).failed();
462     assertThat(compilation)
463         .hadErrorContaining(
464             message(
465                 "Found a dependency cycle:",
466                 "    String is injected at",
467                 "        CycleModule.object(string)",
468                 "    Object is injected at",
469                 "        CycleModule.string(object)",
470                 "    String is injected at",
471                 "        CycleModule.object(string)",
472                 "    ...",
473                 "",
474                 "The cycle is requested via:",
475                 "    String is requested at",
476                 "        Grandchild.entry()"))
477         .inFile(parent)
478         .onLineContaining("interface Parent");
479   }
480 
481   @Test
cyclicDependencyInSubcomponentsWithChildren()482   public void cyclicDependencyInSubcomponentsWithChildren() {
483     JavaFileObject parent =
484         JavaFileObjects.forSourceLines(
485             "test.Parent",
486             "package test;",
487             "",
488             "import dagger.Component;",
489             "",
490             "@Component",
491             "interface Parent {",
492             "  Child.Builder child();",
493             "}");
494     JavaFileObject child =
495         JavaFileObjects.forSourceLines(
496             "test.Child",
497             "package test;",
498             "",
499             "import dagger.Subcomponent;",
500             "",
501             "@Subcomponent(modules = CycleModule.class)",
502             "interface Child {",
503             "  String entry();",
504             "",
505             "  Grandchild.Builder grandchild();",
506             "",
507             "  @Subcomponent.Builder",
508             "  interface Builder {",
509             "    Child build();",
510             "  }",
511             "}");
512     // Grandchild has no entry point that depends on the cycle. http://b/111317986
513     JavaFileObject grandchild =
514         JavaFileObjects.forSourceLines(
515             "test.Grandchild",
516             "package test;",
517             "",
518             "import dagger.Subcomponent;",
519             "",
520             "@Subcomponent",
521             "interface Grandchild {",
522             "",
523             "  @Subcomponent.Builder",
524             "  interface Builder {",
525             "    Grandchild build();",
526             "  }",
527             "}");
528     JavaFileObject cycleModule =
529         JavaFileObjects.forSourceLines(
530             "test.CycleModule",
531             "package test;",
532             "",
533             "import dagger.Module;",
534             "import dagger.Provides;",
535             "",
536             "@Module",
537             "abstract class CycleModule {",
538             "  @Provides static Object object(String string) {",
539             "    return string;",
540             "  }",
541             "",
542             "  @Provides static String string(Object object) {",
543             "    return object.toString();",
544             "  }",
545             "}");
546 
547     Compilation compilation = daggerCompiler().compile(parent, child, grandchild, cycleModule);
548     assertThat(compilation).failed();
549     assertThat(compilation)
550         .hadErrorContaining(
551             message(
552                 "Found a dependency cycle:",
553                 "    String is injected at",
554                 "        CycleModule.object(string)",
555                 "    Object is injected at",
556                 "        CycleModule.string(object)",
557                 "    String is injected at",
558                 "        CycleModule.object(string)",
559                 "    ...",
560                 "",
561                 "The cycle is requested via:",
562                 "    String is requested at",
563                 "        Child.entry() [Parent → Child]"))
564         .inFile(parent)
565         .onLineContaining("interface Parent");
566   }
567 
568   @Test
circularBindsMethods()569   public void circularBindsMethods() {
570     JavaFileObject qualifier =
571         JavaFileObjects.forSourceLines(
572             "test.SomeQualifier",
573             "package test;",
574             "",
575             "import javax.inject.Qualifier;",
576             "",
577             "@Qualifier @interface SomeQualifier {}");
578     JavaFileObject module =
579         JavaFileObjects.forSourceLines(
580             "test.TestModule",
581             "package test;",
582             "",
583             "import dagger.Binds;",
584             "import dagger.Module;",
585             "",
586             "@Module",
587             "abstract class TestModule {",
588             "  @Binds abstract Object bindUnqualified(@SomeQualifier Object qualified);",
589             "  @Binds @SomeQualifier abstract Object bindQualified(Object unqualified);",
590             "}");
591     JavaFileObject component =
592         JavaFileObjects.forSourceLines(
593             "test.TestComponent",
594             "package test;",
595             "",
596             "import dagger.Component;",
597             "",
598             "@Component(modules = TestModule.class)",
599             "interface TestComponent {",
600             "  Object unqualified();",
601             "}");
602 
603     Compilation compilation = daggerCompiler().compile(qualifier, module, component);
604     assertThat(compilation).failed();
605     assertThat(compilation)
606         .hadErrorContaining(
607             message(
608                 "Found a dependency cycle:",
609                 "    Object is injected at",
610                 "        TestModule.bindQualified(unqualified)",
611                 "    @SomeQualifier Object is injected at",
612                 "        TestModule.bindUnqualified(qualified)",
613                 "    Object is injected at",
614                 "        TestModule.bindQualified(unqualified)",
615                 "    ...",
616                 "",
617                 "The cycle is requested via:",
618                 "    Object is requested at",
619                 "        TestComponent.unqualified()"))
620         .inFile(component)
621         .onLineContaining("interface TestComponent");
622   }
623 
624   @Test
selfReferentialBinds()625   public void selfReferentialBinds() {
626     JavaFileObject module =
627         JavaFileObjects.forSourceLines(
628             "test.TestModule",
629             "package test;",
630             "",
631             "import dagger.Binds;",
632             "import dagger.Module;",
633             "",
634             "@Module",
635             "abstract class TestModule {",
636             "  @Binds abstract Object bindToSelf(Object sameKey);",
637             "}");
638     JavaFileObject component =
639         JavaFileObjects.forSourceLines(
640             "test.TestComponent",
641             "package test;",
642             "",
643             "import dagger.Component;",
644             "",
645             "@Component(modules = TestModule.class)",
646             "interface TestComponent {",
647             "  Object selfReferential();",
648             "}");
649 
650     Compilation compilation = daggerCompiler().compile(module, component);
651     assertThat(compilation).failed();
652     assertThat(compilation)
653         .hadErrorContaining(
654             message(
655                 "Found a dependency cycle:",
656                 "    Object is injected at",
657                 "        TestModule.bindToSelf(sameKey)",
658                 "    Object is injected at",
659                 "        TestModule.bindToSelf(sameKey)",
660                 "    ...",
661                 "",
662                 "The cycle is requested via:",
663                 "    Object is requested at",
664                 "        TestComponent.selfReferential()"))
665         .inFile(component)
666         .onLineContaining("interface TestComponent");
667   }
668 
669   @Test
cycleFromMembersInjectionMethod_WithSameKeyAsMembersInjectionMethod()670   public void cycleFromMembersInjectionMethod_WithSameKeyAsMembersInjectionMethod() {
671     JavaFileObject a =
672         JavaFileObjects.forSourceLines(
673             "test.A",
674             "package test;",
675             "",
676             "import javax.inject.Inject;",
677             "",
678             "class A {",
679             "  @Inject A() {}",
680             "  @Inject B b;",
681             "}");
682     JavaFileObject b =
683         JavaFileObjects.forSourceLines(
684             "test.B",
685             "package test;",
686             "",
687             "import javax.inject.Inject;",
688             "",
689             "class B {",
690             "  @Inject B() {}",
691             "  @Inject A a;",
692             "}");
693     JavaFileObject component =
694         JavaFileObjects.forSourceLines(
695             "test.CycleComponent",
696             "package test;",
697             "",
698             "import dagger.Component;",
699             "",
700             "@Component",
701             "interface CycleComponent {",
702             "  void inject(A a);",
703             "}");
704 
705     Compilation compilation = daggerCompiler().compile(a, b, component);
706     assertThat(compilation).failed();
707     assertThat(compilation)
708         .hadErrorContaining(
709             message(
710                 "Found a dependency cycle:",
711                 "    test.B is injected at",
712                 "        test.A.b",
713                 "    test.A is injected at",
714                 "        test.B.a",
715                 "    test.B is injected at",
716                 "        test.A.b",
717                 "    ...",
718                 "",
719                 "The cycle is requested via:",
720                 "    test.B is injected at",
721                 "        test.A.b",
722                 "    test.A is injected at",
723                 "        CycleComponent.inject(test.A)"))
724         .inFile(component)
725         .onLineContaining("interface CycleComponent");
726   }
727 
728   @Test
longCycleMaskedByShortBrokenCycles()729   public void longCycleMaskedByShortBrokenCycles() {
730     JavaFileObject cycles =
731         JavaFileObjects.forSourceLines(
732             "test.Cycles",
733             "package test;",
734             "",
735             "import javax.inject.Inject;",
736             "import javax.inject.Provider;",
737             "import dagger.Component;",
738             "",
739             "final class Cycles {",
740             "  static class A {",
741             "    @Inject A(Provider<A> aProvider, B b) {}",
742             "  }",
743             "",
744             "  static class B {",
745             "    @Inject B(Provider<B> bProvider, A a) {}",
746             "  }",
747             "",
748             "  @Component",
749             "  interface C {",
750             "    A a();",
751             "  }",
752             "}");
753     Compilation compilation = daggerCompiler().compile(cycles);
754     assertThat(compilation).failed();
755     assertThat(compilation)
756         .hadErrorContaining("Found a dependency cycle:")
757         .inFile(cycles)
758         .onLineContaining("interface C");
759   }
760 }
761