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