/* * Copyright (C) 2014 Google, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package dagger.internal.codegen; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.testing.compile.JavaFileObjects; import java.util.Arrays; import javax.tools.JavaFileObject; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import static com.google.common.truth.Truth.assertAbout; import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource; import static com.google.testing.compile.JavaSourcesSubjectFactory.javaSources; import static dagger.internal.codegen.ErrorMessages.nullableToNonNullable; @RunWith(JUnit4.class) public class GraphValidationTest { private final JavaFileObject NULLABLE = JavaFileObjects.forSourceLines("test.Nullable", "package test;", "public @interface Nullable {}"); @Test public void componentOnConcreteClass() { JavaFileObject component = JavaFileObjects.forSourceLines("test.MyComponent", "package test;", "", "import dagger.Component;", "", "@Component", "interface MyComponent {", " Foo getFoo();", "}"); JavaFileObject injectable = JavaFileObjects.forSourceLines("test.Foo", "package test;", "", "import javax.inject.Inject;", "", "class Foo {", " @Inject Foo(Bar bar) {}", "}"); JavaFileObject nonInjectable = JavaFileObjects.forSourceLines("test.Bar", "package test;", "", "import javax.inject.Inject;", "", "interface Bar {}"); assertAbout(javaSources()).that(Arrays.asList(component, injectable, nonInjectable)) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining("test.Bar cannot be provided without an @Provides-annotated method.") .in(component).onLine(7); } @Test public void componentProvisionWithNoDependencyChain() { JavaFileObject component = JavaFileObjects.forSourceLines( "test.TestClass", "package test;", "", "import dagger.Component;", "import javax.inject.Qualifier;", "", "final class TestClass {", " @Qualifier @interface Q {}", " interface A {}", "", " @Component()", " interface AComponent {", " A getA();", " @Q A qualifiedA();", " }", "}"); assertAbout(javaSource()) .that(component) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining( "test.TestClass.A cannot be provided without an @Provides-annotated method.") .in(component) .onLine(12) .and() .withErrorContaining( "@test.TestClass.Q test.TestClass.A " + "cannot be provided without an @Provides-annotated method.") .in(component) .onLine(13); } @Test public void constructorInjectionWithoutAnnotation() { JavaFileObject component = JavaFileObjects.forSourceLines("test.TestClass", "package test;", "", "import dagger.Component;", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Inject;", "", "final class TestClass {", " static class A {", " A() {}", " }", "", " @Component()", " interface AComponent {", " A getA();", " }", "}"); String expectedError = "test.TestClass.A cannot be provided without an " + "@Inject constructor or from an @Provides-annotated method."; assertAbout(javaSource()).that(component) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(expectedError).in(component).onLine(15); } @Test public void membersInjectWithoutProvision() { JavaFileObject component = JavaFileObjects.forSourceLines("test.TestClass", "package test;", "", "import dagger.Component;", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Inject;", "", "final class TestClass {", " static class A {", " @Inject A() {}", " }", "", " static class B {", " @Inject A a;", " }", "", " @Component()", " interface AComponent {", " B getB();", " }", "}"); String expectedError = "test.TestClass.B cannot be provided without an " + "@Inject constructor or from an @Provides-annotated method. " + "This type supports members injection but cannot be implicitly provided."; assertAbout(javaSource()).that(component) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(expectedError).in(component).onLine(19); } @Test public void cyclicDependency() { JavaFileObject component = JavaFileObjects.forSourceLines("test.Outer", "package test;", "", "import dagger.Component;", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Inject;", "", "final class Outer {", " static class A {", " @Inject A(C cParam) {}", " }", "", " static class B {", " @Inject B(A aParam) {}", " }", "", " static class C {", " @Inject C(B bParam) {}", " }", "", " @Component()", " interface CComponent {", " C getC();", " }", "}"); String expectedError = "test.Outer.CComponent.getC() contains a dependency cycle:\n" + " test.Outer.C.<init>(test.Outer.B bParam)\n" + " [parameter: test.Outer.B bParam]\n" + " test.Outer.B.<init>(test.Outer.A aParam)\n" + " [parameter: test.Outer.A aParam]\n" + " test.Outer.A.<init>(test.Outer.C cParam)\n" + " [parameter: test.Outer.C cParam]"; assertAbout(javaSource()).that(component) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(expectedError).in(component).onLine(23); } @Test public void cyclicDependencyNotIncludingEntryPoint() { JavaFileObject component = JavaFileObjects.forSourceLines( "test.Outer", "package test;", "", "import dagger.Component;", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Inject;", "", "final class Outer {", " static class A {", " @Inject A(C cParam) {}", " }", "", " static class B {", " @Inject B(A aParam) {}", " }", "", " static class C {", " @Inject C(B bParam) {}", " }", "", " static class D {", " @Inject D(C cParam) {}", " }", "", " @Component()", " interface DComponent {", " D getD();", " }", "}"); String expectedError = "test.Outer.DComponent.getD() contains a dependency cycle:\n" + " test.Outer.D.<init>(test.Outer.C cParam)\n" + " [parameter: test.Outer.C cParam]\n" + " test.Outer.C.<init>(test.Outer.B bParam)\n" + " [parameter: test.Outer.B bParam]\n" + " test.Outer.B.<init>(test.Outer.A aParam)\n" + " [parameter: test.Outer.A aParam]\n" + " test.Outer.A.<init>(test.Outer.C cParam)\n" + " [parameter: test.Outer.C cParam]"; assertAbout(javaSource()) .that(component) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(expectedError) .in(component) .onLine(27); } @Test public void cyclicDependencyNotBrokenByMapBinding() { JavaFileObject component = JavaFileObjects.forSourceLines( "test.Outer", "package test;", "", "import dagger.Component;", "import dagger.MapKey;", "import dagger.Module;", "import dagger.Provides;", "import java.util.Map;", "import javax.inject.Inject;", "", "final class Outer {", " static class A {", " @Inject A(Map<String, C> cMap) {}", " }", "", " static class B {", " @Inject B(A aParam) {}", " }", "", " static class C {", " @Inject C(B bParam) {}", " }", "", " @Component(modules = CModule.class)", " interface CComponent {", " C getC();", " }", "", " @Module", " static class CModule {", " @Provides(type = Provides.Type.MAP)", " @StringKey(\"C\")", " static C c(C c) {", " return c;", " }", " }", "", " @MapKey", " @interface StringKey {", " String value();", " }", "}"); String expectedError = Joiner.on('\n') .join( "test.Outer.CComponent.getC() contains a dependency cycle:", " test.Outer.C.<init>(test.Outer.B bParam)", " [parameter: test.Outer.B bParam]", " test.Outer.B.<init>(test.Outer.A aParam)", " [parameter: test.Outer.A aParam]", " test.Outer.A.<init>(java.util.Map<java.lang.String,test.Outer.C> cMap)", " [parameter: java.util.Map<java.lang.String,test.Outer.C> cMap]", " test.Outer.A.<init>(java.util.Map<java.lang.String,test.Outer.C> cMap)", " [parameter: java.util.Map<java.lang.String,test.Outer.C> cMap]", " test.Outer.CModule.c(test.Outer.C c)", " [parameter: test.Outer.C c]"); assertAbout(javaSource()) .that(component) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(expectedError) .in(component) .onLine(25); } @Test public void falsePositiveCyclicDependencyIndirectionDetected() { JavaFileObject component = JavaFileObjects.forSourceLines( "test.Outer", "package test;", "", "import dagger.Component;", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Inject;", "import javax.inject.Provider;", "", "final class Outer {", " static class A {", " @Inject A(C cParam) {}", " }", "", " static class B {", " @Inject B(A aParam) {}", " }", "", " static class C {", " @Inject C(B bParam) {}", " }", "", " static class D {", " @Inject D(Provider<C> cParam) {}", " }", "", " @Component()", " interface DComponent {", " D getD();", " }", "}"); String expectedError = "test.Outer.DComponent.getD() contains a dependency cycle:\n" + " test.Outer.D.<init>(javax.inject.Provider<test.Outer.C> cParam)\n" + " [parameter: javax.inject.Provider<test.Outer.C> cParam]\n" + " test.Outer.C.<init>(test.Outer.B bParam)\n" + " [parameter: test.Outer.B bParam]\n" + " test.Outer.B.<init>(test.Outer.A aParam)\n" + " [parameter: test.Outer.A aParam]\n" + " test.Outer.A.<init>(test.Outer.C cParam)\n" + " [parameter: test.Outer.C cParam]"; assertAbout(javaSource()) .that(component) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(expectedError) .in(component) .onLine(28); } @Ignore @Test public void cyclicDependencySimpleProviderIndirectionWarning() { JavaFileObject component = JavaFileObjects.forSourceLines( "test.Outer", "package test;", "", "import dagger.Component;", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Inject;", "import javax.inject.Provider;", "", "final class Outer {", " static class A {", " @Inject A(B bParam) {}", " }", "", " static class B {", " @Inject B(C bParam, D dParam) {}", " }", "", " static class C {", " @Inject C(Provider<A> aParam) {}", " }", "", " static class D {", " @Inject D() {}", " }", "", " @Component()", " interface CComponent {", " C get();", " }", "}"); /* String expectedWarning = "test.Outer.CComponent.get() contains a dependency cycle:" + " test.Outer.C.<init>(javax.inject.Provider<test.Outer.A> aParam)" + " [parameter: javax.inject.Provider<test.Outer.A> aParam]" + " test.Outer.A.<init>(test.Outer.B bParam)" + " [parameter: test.Outer.B bParam]" + " test.Outer.B.<init>(test.Outer.C bParam, test.Outer.D dParam)" + " [parameter: test.Outer.C bParam]"; */ assertAbout(javaSource()) // TODO(cgruber): Implement warning checks. .that(component) .processedWith(new ComponentProcessor()) .compilesWithoutError(); //.withWarningContaining(expectedWarning).in(component).onLine(X); } @Ignore @Test public void cyclicDependencySimpleProviderIndirectionWarningSuppressed() { JavaFileObject component = JavaFileObjects.forSourceLines( "test.Outer", "package test;", "", "import dagger.Component;", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Inject;", "import javax.inject.Provider;", "", "final class Outer {", " static class A {", " @Inject A(B bParam) {}", " }", "", " static class B {", " @Inject B(C bParam, D dParam) {}", " }", "", " static class C {", " @Inject C(Provider<A> aParam) {}", " }", "", " static class D {", " @Inject D() {}", " }", "", " @SuppressWarnings(\"dependency-cycle\")", " @Component()", " interface CComponent {", " C get();", " }", "}"); assertAbout(javaSource()) .that(component) .processedWith(new ComponentProcessor()) .compilesWithoutError(); //.compilesWithoutWarning(); //TODO(cgruber) } @Test public void duplicateExplicitBindings_ProvidesAndComponentProvision() { JavaFileObject component = JavaFileObjects.forSourceLines("test.Outer", "package test;", "", "import dagger.Component;", "import dagger.Module;", "import dagger.Provides;", "", "final class Outer {", " interface A {}", "", " interface B {}", "", " @Module", " static class AModule {", " @Provides String provideString() { return \"\"; }", " @Provides A provideA(String s) { return new A() {}; }", " }", "", " @Component(modules = AModule.class)", " interface Parent {", " A getA();", " }", "", " @Module", " static class BModule {", " @Provides B provideB(A a) { return new B() {}; }", " }", "", " @Component(dependencies = Parent.class, modules = { BModule.class, AModule.class})", " interface Child {", " B getB();", " }", "}"); String expectedError = "test.Outer.A is bound multiple times:\n" + " test.Outer.A test.Outer.Parent.getA()\n" + " @Provides test.Outer.A test.Outer.AModule.provideA(String)"; assertAbout(javaSource()).that(component) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(expectedError).in(component).onLine(30); } @Test public void duplicateExplicitBindings_TwoProvidesMethods() { JavaFileObject component = JavaFileObjects.forSourceLines("test.Outer", "package test;", "", "import dagger.Component;", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Inject;", "", "final class Outer {", " interface A {}", "", " @Module", " static class Module1 {", " @Provides A provideA1() { return new A() {}; }", " }", "", " @Module", " static class Module2 {", " @Provides String provideString() { return \"\"; }", " @Provides A provideA2(String s) { return new A() {}; }", " }", "", " @Component(modules = { Module1.class, Module2.class})", " interface TestComponent {", " A getA();", " }", "}"); String expectedError = "test.Outer.A is bound multiple times:\n" + " @Provides test.Outer.A test.Outer.Module1.provideA1()\n" + " @Provides test.Outer.A test.Outer.Module2.provideA2(String)"; assertAbout(javaSource()).that(component) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(expectedError).in(component).onLine(24); } @Test public void duplicateExplicitBindings_MultipleProvisionTypes() { JavaFileObject component = JavaFileObjects.forSourceLines("test.Outer", "package test;", "", "import dagger.Component;", "import dagger.MapKey;", "import dagger.Module;", "import dagger.Provides;", "import dagger.MapKey;", "import java.util.HashMap;", "import java.util.HashSet;", "import java.util.Map;", "import java.util.Set;", "", "import static java.lang.annotation.RetentionPolicy.RUNTIME;", "import static dagger.Provides.Type.MAP;", "import static dagger.Provides.Type.SET;", "", "final class Outer {", " @MapKey(unwrapValue = true)", " @interface StringKey {", " String value();", " }", "", " @Module", " static class TestModule1 {", " @Provides(type = MAP)", " @StringKey(\"foo\")", " String stringMapEntry() { return \"\"; }", "", " @Provides(type = SET) String stringSetElement() { return \"\"; }", " }", "", " @Module", " static class TestModule2 {", " @Provides Set<String> stringSet() { return new HashSet<String>(); }", "", " @Provides Map<String, String> stringMap() {", " return new HashMap<String, String>();", " }", " }", "", " @Component(modules = { TestModule1.class, TestModule2.class })", " interface TestComponent {", " Set<String> getStringSet();", " Map<String, String> getStringMap();", " }", "}"); String expectedSetError = "java.util.Set<java.lang.String> has incompatible bindings:\n" + " Set bindings:\n" + " @Provides(type=SET) String test.Outer.TestModule1.stringSetElement()\n" + " Unique bindings:\n" + " @Provides Set<String> test.Outer.TestModule2.stringSet()"; String expectedMapError = "java.util.Map<java.lang.String,java.lang.String> has incompatible bindings:\n" + " Map bindings:\n" + " @Provides(type=MAP) @test.Outer.StringKey(\"foo\") String" + " test.Outer.TestModule1.stringMapEntry()\n" + " Unique bindings:\n" + " @Provides Map<String,String> test.Outer.TestModule2.stringMap()"; assertAbout(javaSource()).that(component) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(expectedSetError).in(component).onLine(43) .and().withErrorContaining(expectedMapError).in(component).onLine(44); } @Test public void duplicateBindings_TruncateAfterLimit() { JavaFileObject component = JavaFileObjects.forSourceLines("test.Outer", "package test;", "", "import dagger.Component;", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Inject;", "", "final class Outer {", " interface A {}", "", " @Module", " static class Module1 {", " @Provides A provideA() { return new A() {}; }", " }", "", " @Module", " static class Module2 {", " @Provides A provideA() { return new A() {}; }", " }", "", " @Module", " static class Module3 {", " @Provides A provideA() { return new A() {}; }", " }", "", " @Module", " static class Module4 {", " @Provides A provideA() { return new A() {}; }", " }", "", " @Module", " static class Module5 {", " @Provides A provideA() { return new A() {}; }", " }", "", " @Module", " static class Module6 {", " @Provides A provideA() { return new A() {}; }", " }", "", " @Module", " static class Module7 {", " @Provides A provideA() { return new A() {}; }", " }", "", " @Module", " static class Module8 {", " @Provides A provideA() { return new A() {}; }", " }", "", " @Module", " static class Module9 {", " @Provides A provideA() { return new A() {}; }", " }", "", " @Module", " static class Module10 {", " @Provides A provideA() { return new A() {}; }", " }", "", " @Module", " static class Module11 {", " @Provides A provideA() { return new A() {}; }", " }", "", " @Module", " static class Module12 {", " @Provides A provideA() { return new A() {}; }", " }", "", " @Component(modules = {", " Module1.class,", " Module2.class,", " Module3.class,", " Module4.class,", " Module5.class,", " Module6.class,", " Module7.class,", " Module8.class,", " Module9.class,", " Module10.class,", " Module11.class,", " Module12.class", " })", " interface TestComponent {", " A getA();", " }", "}"); String expectedError = "test.Outer.A is bound multiple times:\n" + " @Provides test.Outer.A test.Outer.Module1.provideA()\n" + " @Provides test.Outer.A test.Outer.Module2.provideA()\n" + " @Provides test.Outer.A test.Outer.Module3.provideA()\n" + " @Provides test.Outer.A test.Outer.Module4.provideA()\n" + " @Provides test.Outer.A test.Outer.Module5.provideA()\n" + " @Provides test.Outer.A test.Outer.Module6.provideA()\n" + " @Provides test.Outer.A test.Outer.Module7.provideA()\n" + " @Provides test.Outer.A test.Outer.Module8.provideA()\n" + " @Provides test.Outer.A test.Outer.Module9.provideA()\n" + " @Provides test.Outer.A test.Outer.Module10.provideA()\n" + " and 2 others"; assertAbout(javaSource()).that(component) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(expectedError).in(component).onLine(86); } @Test public void longChainOfDependencies() { JavaFileObject component = JavaFileObjects.forSourceLines("test.TestClass", "package test;", "", "import dagger.Component;", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Inject;", "", "final class TestClass {", " interface A {}", "", " static class B {", " @Inject B(A a) {}", " }", "", " static class C {", " @Inject B b;", " @Inject C(B b) {}", " }", "", " interface D { }", "", " static class DImpl implements D {", " @Inject DImpl(C c, B b) {}", " }", "", " @Module", " static class DModule {", " @Provides D d(DImpl impl) { return impl; }", " }", "", " @Component(modules = { DModule.class })", " interface AComponent {", " D getFoo();", " C injectC(C c);", " }", "}"); String errorText = "test.TestClass.A cannot be provided without an @Provides-annotated method.\n"; String firstError = errorText + " test.TestClass.DModule.d(test.TestClass.DImpl impl)\n" + " [parameter: test.TestClass.DImpl impl]\n" + " test.TestClass.DImpl.<init>(test.TestClass.C c, test.TestClass.B b)\n" + " [parameter: test.TestClass.C c]\n" + " test.TestClass.C.b\n" + " [injected field of type: test.TestClass.B b]\n" + " test.TestClass.B.<init>(test.TestClass.A a)\n" + " [parameter: test.TestClass.A a]"; String secondError = errorText + " test.TestClass.C.b\n" + " [injected field of type: test.TestClass.B b]\n" + " test.TestClass.B.<init>(test.TestClass.A a)\n" + " [parameter: test.TestClass.A a]"; assertAbout(javaSource()).that(component) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(firstError).in(component).onLine(33) .and().withErrorContaining(secondError).in(component).onLine(34); } @Test public void resolvedParametersInDependencyTrace() { JavaFileObject generic = JavaFileObjects.forSourceLines("test.Generic", "package test;", "", "import javax.inject.Inject;", "import javax.inject.Provider;", "", "final class Generic<T> {", " @Inject Generic(T t) {}", "}"); JavaFileObject testClass = JavaFileObjects.forSourceLines("test.TestClass", "package test;", "", "import javax.inject.Inject;", "import java.util.List;", "", "final class TestClass {", " @Inject TestClass(List list) {}", "}"); JavaFileObject usesTest = JavaFileObjects.forSourceLines("test.UsesTest", "package test;", "", "import javax.inject.Inject;", "", "final class UsesTest {", " @Inject UsesTest(Generic<TestClass> genericTestClass) {}", "}"); JavaFileObject component = JavaFileObjects.forSourceLines("test.TestComponent", "package test;", "", "import dagger.Component;", "", "@Component", "interface TestComponent {", " UsesTest usesTest();", "}"); String expectedMsg = Joiner.on("\n").join( "java.util.List cannot be provided without an @Provides-annotated method.", " test.UsesTest.<init>(test.Generic<test.TestClass> genericTestClass)", " [parameter: test.Generic<test.TestClass> genericTestClass]", " test.Generic.<init>(test.TestClass t)", " [parameter: test.TestClass t]", " test.TestClass.<init>(java.util.List list)", " [parameter: java.util.List list]"); assertAbout(javaSources()).that(ImmutableList.of(generic, testClass, usesTest, component)) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(expectedMsg); } @Test public void resolvedVariablesInDependencyTrace() { JavaFileObject generic = JavaFileObjects.forSourceLines("test.Generic", "package test;", "", "import javax.inject.Inject;", "import javax.inject.Provider;", "", "final class Generic<T> {", " @Inject T t;", " @Inject Generic() {}", "}"); JavaFileObject testClass = JavaFileObjects.forSourceLines("test.TestClass", "package test;", "", "import javax.inject.Inject;", "import java.util.List;", "", "final class TestClass {", " @Inject TestClass(List list) {}", "}"); JavaFileObject usesTest = JavaFileObjects.forSourceLines("test.UsesTest", "package test;", "", "import javax.inject.Inject;", "", "final class UsesTest {", " @Inject UsesTest(Generic<TestClass> genericTestClass) {}", "}"); JavaFileObject component = JavaFileObjects.forSourceLines("test.TestComponent", "package test;", "", "import dagger.Component;", "", "@Component", "interface TestComponent {", " UsesTest usesTest();", "}"); String expectedMsg = Joiner.on("\n").join( "java.util.List cannot be provided without an @Provides-annotated method.", " test.UsesTest.<init>(test.Generic<test.TestClass> genericTestClass)", " [parameter: test.Generic<test.TestClass> genericTestClass]", " test.Generic.t", " [injected field of type: test.TestClass t]", " test.TestClass.<init>(java.util.List list)", " [parameter: java.util.List list]"); assertAbout(javaSources()).that(ImmutableList.of(generic, testClass, usesTest, component)) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(expectedMsg); } @Test public void nullCheckForConstructorParameters() { JavaFileObject a = JavaFileObjects.forSourceLines("test.A", "package test;", "", "import javax.inject.Inject;", "", "final class A {", " @Inject A(String string) {}", "}"); JavaFileObject module = JavaFileObjects.forSourceLines("test.TestModule", "package test;", "", "import dagger.Provides;", "import javax.inject.Inject;", "", "@dagger.Module", "final class TestModule {", " @Nullable @Provides String provideString() { return null; }", "}"); JavaFileObject component = JavaFileObjects.forSourceLines("test.TestComponent", "package test;", "", "import dagger.Component;", "", "@Component(modules = TestModule.class)", "interface TestComponent {", " A a();", "}"); assertAbout(javaSources()).that(ImmutableList.of(NULLABLE, a, module, component)) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining( nullableToNonNullable( "java.lang.String", "@test.Nullable @Provides String test.TestModule.provideString()")); // but if we disable the validation, then it compiles fine. assertAbout(javaSources()).that(ImmutableList.of(NULLABLE, a, module, component)) .withCompilerOptions("-Adagger.nullableValidation=WARNING") .processedWith(new ComponentProcessor()) .compilesWithoutError(); } @Test public void nullCheckForMembersInjectParam() { JavaFileObject a = JavaFileObjects.forSourceLines("test.A", "package test;", "", "import javax.inject.Inject;", "", "final class A {", " @Inject A() {}", " @Inject void register(String string) {}", "}"); JavaFileObject module = JavaFileObjects.forSourceLines("test.TestModule", "package test;", "", "import dagger.Provides;", "import javax.inject.Inject;", "", "@dagger.Module", "final class TestModule {", " @Nullable @Provides String provideString() { return null; }", "}"); JavaFileObject component = JavaFileObjects.forSourceLines("test.TestComponent", "package test;", "", "import dagger.Component;", "", "@Component(modules = TestModule.class)", "interface TestComponent {", " A a();", "}"); assertAbout(javaSources()).that(ImmutableList.of(NULLABLE, a, module, component)) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining( nullableToNonNullable( "java.lang.String", "@test.Nullable @Provides String test.TestModule.provideString()")); // but if we disable the validation, then it compiles fine. assertAbout(javaSources()).that(ImmutableList.of(NULLABLE, a, module, component)) .withCompilerOptions("-Adagger.nullableValidation=WARNING") .processedWith(new ComponentProcessor()) .compilesWithoutError(); } @Test public void nullCheckForVariable() { JavaFileObject a = JavaFileObjects.forSourceLines("test.A", "package test;", "", "import javax.inject.Inject;", "", "final class A {", " @Inject String string;", " @Inject A() {}", "}"); JavaFileObject module = JavaFileObjects.forSourceLines("test.TestModule", "package test;", "", "import dagger.Provides;", "import javax.inject.Inject;", "", "@dagger.Module", "final class TestModule {", " @Nullable @Provides String provideString() { return null; }", "}"); JavaFileObject component = JavaFileObjects.forSourceLines("test.TestComponent", "package test;", "", "import dagger.Component;", "", "@Component(modules = TestModule.class)", "interface TestComponent {", " A a();", "}"); assertAbout(javaSources()).that(ImmutableList.of(NULLABLE, a, module, component)) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining( nullableToNonNullable( "java.lang.String", "@test.Nullable @Provides String test.TestModule.provideString()")); // but if we disable the validation, then it compiles fine. assertAbout(javaSources()).that(ImmutableList.of(NULLABLE, a, module, component)) .withCompilerOptions("-Adagger.nullableValidation=WARNING") .processedWith(new ComponentProcessor()) .compilesWithoutError(); } @Test public void nullCheckForComponentReturn() { JavaFileObject module = JavaFileObjects.forSourceLines("test.TestModule", "package test;", "", "import dagger.Provides;", "import javax.inject.Inject;", "", "@dagger.Module", "final class TestModule {", " @Nullable @Provides String provideString() { return null; }", "}"); JavaFileObject component = JavaFileObjects.forSourceLines("test.TestComponent", "package test;", "", "import dagger.Component;", "", "@Component(modules = TestModule.class)", "interface TestComponent {", " String string();", "}"); assertAbout(javaSources()).that(ImmutableList.of(NULLABLE, module, component)) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining( nullableToNonNullable( "java.lang.String", "@test.Nullable @Provides String test.TestModule.provideString()")); // but if we disable the validation, then it compiles fine. assertAbout(javaSources()).that(ImmutableList.of(NULLABLE, module, component)) .withCompilerOptions("-Adagger.nullableValidation=WARNING") .processedWith(new ComponentProcessor()) .compilesWithoutError(); } @Test public void componentDependencyMustNotCycle_Direct() { JavaFileObject shortLifetime = JavaFileObjects.forSourceLines("test.ComponentShort", "package test;", "", "import dagger.Component;", "", "@Component(dependencies = ComponentShort.class)", "interface ComponentShort {", "}"); String errorMessage = "test.ComponentShort contains a cycle in its component dependencies:\n" + " test.ComponentShort"; assertAbout(javaSource()) .that(shortLifetime) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(errorMessage); } @Test public void componentDependencyMustNotCycle_Indirect() { JavaFileObject longLifetime = JavaFileObjects.forSourceLines("test.ComponentLong", "package test;", "", "import dagger.Component;", "", "@Component(dependencies = ComponentMedium.class)", "interface ComponentLong {", "}"); JavaFileObject mediumLifetime = JavaFileObjects.forSourceLines("test.ComponentMedium", "package test;", "", "import dagger.Component;", "", "@Component(dependencies = ComponentLong.class)", "interface ComponentMedium {", "}"); JavaFileObject shortLifetime = JavaFileObjects.forSourceLines("test.ComponentShort", "package test;", "", "import dagger.Component;", "", "@Component(dependencies = ComponentMedium.class)", "interface ComponentShort {", "}"); String longErrorMessage = "test.ComponentLong contains a cycle in its component dependencies:\n" + " test.ComponentLong\n" + " test.ComponentMedium\n" + " test.ComponentLong"; String mediumErrorMessage = "test.ComponentMedium contains a cycle in its component dependencies:\n" + " test.ComponentMedium\n" + " test.ComponentLong\n" + " test.ComponentMedium"; String shortErrorMessage = "test.ComponentShort contains a cycle in its component dependencies:\n" + " test.ComponentMedium\n" + " test.ComponentLong\n" + " test.ComponentMedium\n" + " test.ComponentShort"; assertAbout(javaSources()) .that(ImmutableList.of(longLifetime, mediumLifetime, shortLifetime)) .processedWith(new ComponentProcessor()) .failsToCompile() .withErrorContaining(longErrorMessage).in(longLifetime) .and() .withErrorContaining(mediumErrorMessage).in(mediumLifetime) .and() .withErrorContaining(shortErrorMessage).in(shortLifetime); } }