/* * Copyright (C) 2017 The Dagger Authors. * * 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.android.processor; import androidx.room.compiler.processing.util.Source; import com.google.common.base.Joiner; import dagger.testing.compile.CompilerTests; import dagger.testing.compile.CompilerTests.DaggerCompiler; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class AndroidMapKeyValidatorTest { private static final Source FOO_ACTIVITY = CompilerTests.javaSource( "test.FooActivity", "package test;", "", "import android.app.Activity;", "import dagger.android.AndroidInjector;", "", "public class FooActivity extends Activity {", " interface Factory extends AndroidInjector.Factory {}", " abstract static class Builder extends AndroidInjector.Builder {}", "}"); private static final Source BAR_ACTIVITY = CompilerTests.javaSource( "test.BarActivity", "package test;", "", "import android.app.Activity;", "", "public class BarActivity extends Activity {}"); private static Source moduleWithMethod(String... lines) { return CompilerTests.javaSource( "test.AndroidModule", "package test;", "", "import android.app.Activity;", "import android.app.Fragment;", "import dagger.Module;", "import dagger.*;", "import dagger.android.*;", "import dagger.multibindings.*;", "import javax.inject.*;", "", "@Module", "abstract class AndroidModule {", " " + Joiner.on("\n ").join(lines), "}"); } // TODO(dpb): Change these tests to use onLineContaining() instead of onLine(). private static final int LINES_BEFORE_METHOD = 12; @Test public void rawFactoryType() { Source module = moduleWithMethod( "@Binds", "@IntoMap", "@ClassKey(FooActivity.class)", "abstract AndroidInjector.Factory bindRawFactory(FooActivity.Factory factory);"); compile(module, FOO_ACTIVITY) .compile( subject -> { subject.compilationDidFail(); subject.hasErrorContaining( "should bind dagger.android.AndroidInjector.Factory, " + "not dagger.android.AndroidInjector.Factory"); }); } @Test public void wildCardFactoryType() { Source module = CompilerTests.kotlinSource( "AndroidModule.kt", "package test", "", "import dagger.Module", "import dagger.Binds", "import dagger.android.AndroidInjector", "import dagger.multibindings.ClassKey", "import dagger.multibindings.IntoMap", "", "@Module", "internal abstract class AndroidModule {", " @Binds", " @IntoMap", " @ClassKey(FooActivity::class)", " abstract fun bindWildcardFactory(factory: FooActivity.Factory):" + " AndroidInjector.Factory<*>", "}"); compile(module, FOO_ACTIVITY).compile(subject -> subject.hasErrorCount(0)); } @Test public void rawBuilderType() { Source module = moduleWithMethod( "@Binds", "@IntoMap", "@ClassKey(FooActivity.class)", "abstract AndroidInjector.Builder bindRawBuilder(FooActivity.Builder builder);"); compile(module, FOO_ACTIVITY) .compile( subject -> { subject.compilationDidFail(); subject.hasErrorContaining( "should bind dagger.android.AndroidInjector.Factory, " + "not dagger.android.AndroidInjector.Builder"); }); } @Test public void bindsToBuilderNotFactory() { Source module = moduleWithMethod( "@Binds", "@IntoMap", "@ClassKey(FooActivity.class)", "abstract AndroidInjector.Builder bindBuilder(", " FooActivity.Builder builder);"); compile(module, FOO_ACTIVITY) .compile( subject -> { subject.compilationDidFail(); subject.hasErrorContaining( "should bind dagger.android.AndroidInjector.Factory, not " + "dagger.android.AndroidInjector.Builder"); }); } @Test public void providesToBuilderNotFactory() { Source module = moduleWithMethod( "@Provides", "@IntoMap", "@ClassKey(FooActivity.class)", "static AndroidInjector.Builder bindBuilder(FooActivity.Builder builder) {", " return builder;", "}"); compile(module, FOO_ACTIVITY) .compile( subject -> { subject.compilationDidFail(); subject.hasErrorContaining( "should bind dagger.android.AndroidInjector.Factory, not " + "dagger.android.AndroidInjector.Builder"); }); } @Test public void bindsToConcreteTypeInsteadOfWildcard() { Source module = moduleWithMethod( "@Binds", "@IntoMap", "@ClassKey(FooActivity.class)", "abstract AndroidInjector.Builder bindBuilder(", " FooActivity.Builder builder);"); compile(module, FOO_ACTIVITY) .compile( subject -> { subject.compilationDidFail(); subject.hasErrorContaining( "should bind dagger.android.AndroidInjector.Factory, not " + "dagger.android.AndroidInjector.Builder"); }); } @Test public void bindsToBaseTypeInsteadOfWildcard() { Source module = moduleWithMethod( "@Binds", "@IntoMap", "@ClassKey(FooActivity.class)", "abstract AndroidInjector.Builder bindBuilder(", " FooActivity.Builder builder);"); compile(module, FOO_ACTIVITY) .compile( subject -> { subject.compilationDidFail(); subject.hasErrorContaining( "@Binds methods' parameter type must be assignable to the return type"); }); } @Test public void bindsCorrectType() { Source module = moduleWithMethod( "@Binds", "@IntoMap", "@ClassKey(FooActivity.class)", "abstract AndroidInjector.Factory bindCorrectType(FooActivity.Builder builder);"); compile(module, FOO_ACTIVITY) .compile( subject -> { subject.hasErrorCount(0); subject.hasNoWarnings(); }); } @Test public void bindsCorrectType_AndroidInjectionKey() { Source module = moduleWithMethod( "@Binds", "@IntoMap", "@AndroidInjectionKey(\"test.FooActivity\")", "abstract AndroidInjector.Factory bindCorrectType(FooActivity.Builder builder);"); compile(module, FOO_ACTIVITY) .compile( subject -> { subject.hasErrorCount(0); subject.hasNoWarnings(); }); } @Test public void bindsCorrectType_AndroidInjectionKey_unbounded() { Source module = moduleWithMethod( "@Binds", "@IntoMap", "@AndroidInjectionKey(\"test.FooActivity\")", "abstract AndroidInjector.Factory bindCorrectType(FooActivity.Builder builder);"); compile(module, FOO_ACTIVITY) .compile( subject -> { subject.hasErrorCount(0); subject.hasNoWarnings(); }); } @Test public void bindsWithScope() { Source module = moduleWithMethod( "@Binds", "@IntoMap", "@ClassKey(FooActivity.class)", "@Singleton", "abstract AndroidInjector.Factory bindWithScope(FooActivity.Builder builder);"); compile(module, FOO_ACTIVITY) .compile( subject -> { subject.compilationDidFail(); subject.hasErrorContaining("should not be scoped"); }); } @Test public void bindsWithScope_suppressWarnings() { Source module = moduleWithMethod( "@SuppressWarnings(\"dagger.android.ScopedInjectorFactory\")", "@Binds", "@IntoMap", "@ClassKey(FooActivity.class)", "@Singleton", "abstract AndroidInjector.Factory bindWithScope(FooActivity.Builder builder);"); compile(module, FOO_ACTIVITY) .compile( subject -> { subject.hasErrorCount(0); subject.hasNoWarnings(); }); } @Test public void mismatchedMapKey_bindsFactory() { Source module = moduleWithMethod( "@Binds", "@IntoMap", "@ClassKey(BarActivity.class)", "abstract AndroidInjector.Factory mismatchedFactory(FooActivity.Factory factory);"); compile(module, FOO_ACTIVITY, BAR_ACTIVITY) .compile( subject -> { subject.compilationDidFail(); subject .hasErrorContaining( "test.FooActivity.Factory does not implement" + " AndroidInjector") .onLine(LINES_BEFORE_METHOD + 3); }); } @Test public void mismatchedMapKey_bindsBuilder() { Source module = moduleWithMethod( "@Binds", "@IntoMap", "@ClassKey(BarActivity.class)", "abstract AndroidInjector.Factory mismatchedBuilder(FooActivity.Builder builder);"); compile(module, FOO_ACTIVITY, BAR_ACTIVITY) .compile( subject -> { subject.compilationDidFail(); subject .hasErrorContaining( "test.FooActivity.Builder does not implement" + " AndroidInjector") .onLine(LINES_BEFORE_METHOD + 3); }); } @Test public void mismatchedMapKey_bindsBuilder_androidInjectionKey() { Source module = moduleWithMethod( "@Binds", "@IntoMap", "@AndroidInjectionKey(\"test.BarActivity\")", "abstract AndroidInjector.Factory mismatchedBuilder(FooActivity.Builder builder);"); compile(module, FOO_ACTIVITY, BAR_ACTIVITY) .compile( subject -> { subject.compilationDidFail(); subject .hasErrorContaining( "test.FooActivity.Builder does not implement" + " AndroidInjector") .onLine(LINES_BEFORE_METHOD + 3); }); } @Test public void mismatchedMapKey_providesBuilder() { Source module = moduleWithMethod( "@Provides", "@IntoMap", "@ClassKey(BarActivity.class)", "static AndroidInjector.Factory mismatchedBuilder(FooActivity.Builder builder) {", " return builder;", "}"); compile(module, FOO_ACTIVITY, BAR_ACTIVITY) .compile( subject -> { subject.hasErrorCount(0); subject.hasNoWarnings(); }); } @Test public void bindsQualifier_ignoresChecks() { Source module = moduleWithMethod( "@Binds", "@IntoMap", "@ClassKey(FooActivity.class)", "@Named(\"unused\")", // normally this should fail, since it is binding to a Builder not a Factory "abstract AndroidInjector.Builder bindsBuilderWithQualifier(", " FooActivity.Builder builder);"); compile(module, FOO_ACTIVITY) .compile( subject -> { subject.hasErrorCount(0); subject.hasNoWarnings(); }); } @Test public void bindToPrimitive() { Source module = moduleWithMethod( "@Binds", "@IntoMap", "@AndroidInjectionKey(\"test.FooActivity\")", "abstract int bindInt(@Named(\"unused\") int otherInt);"); compile(module, FOO_ACTIVITY) .compile( subject -> { subject.hasErrorCount(0); subject.hasNoWarnings(); }); } @Test public void bindToNonFrameworkClass() { Source module = moduleWithMethod( "@Binds", "@IntoMap", "@AndroidInjectionKey(\"test.FooActivity\")", "abstract Number bindInt(Integer integer);"); compile(module, FOO_ACTIVITY) .compile( subject -> { subject.hasErrorCount(0); subject.hasNoWarnings(); }); } @Test public void invalidBindsMethod() { Source module = moduleWithMethod( "@Binds", "@IntoMap", "@ClassKey(FooActivity.class)", "abstract AndroidInjector.Factory bindCorrectType(", " FooActivity.Builder builder, FooActivity.Builder builder2);"); compile(module, FOO_ACTIVITY).compile(subject -> subject.compilationDidFail()); } private DaggerCompiler compile(Source... files) { return CompilerTests.daggerCompiler(files) .withAdditionalJavacProcessors(new AndroidProcessor()) .withAdditionalKspProcessors(new KspAndroidProcessor.Provider()); } }