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