1 /* 2 * Copyright (C) 2017 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.android.processor; 18 19 import androidx.room.compiler.processing.util.Source; 20 import com.google.common.base.Joiner; 21 import dagger.testing.compile.CompilerTests; 22 import dagger.testing.compile.CompilerTests.DaggerCompiler; 23 import org.junit.Test; 24 import org.junit.runner.RunWith; 25 import org.junit.runners.JUnit4; 26 27 @RunWith(JUnit4.class) 28 public class AndroidMapKeyValidatorTest { 29 private static final Source FOO_ACTIVITY = 30 CompilerTests.javaSource( 31 "test.FooActivity", 32 "package test;", 33 "", 34 "import android.app.Activity;", 35 "import dagger.android.AndroidInjector;", 36 "", 37 "public class FooActivity extends Activity {", 38 " interface Factory extends AndroidInjector.Factory<FooActivity> {}", 39 " abstract static class Builder extends AndroidInjector.Builder<FooActivity> {}", 40 "}"); 41 private static final Source BAR_ACTIVITY = 42 CompilerTests.javaSource( 43 "test.BarActivity", 44 "package test;", 45 "", 46 "import android.app.Activity;", 47 "", 48 "public class BarActivity extends Activity {}"); 49 moduleWithMethod(String... lines)50 private static Source moduleWithMethod(String... lines) { 51 return CompilerTests.javaSource( 52 "test.AndroidModule", 53 "package test;", 54 "", 55 "import android.app.Activity;", 56 "import android.app.Fragment;", 57 "import dagger.Module;", 58 "import dagger.*;", 59 "import dagger.android.*;", 60 "import dagger.multibindings.*;", 61 "import javax.inject.*;", 62 "", 63 "@Module", 64 "abstract class AndroidModule {", 65 " " + Joiner.on("\n ").join(lines), 66 "}"); 67 } 68 69 // TODO(dpb): Change these tests to use onLineContaining() instead of onLine(). 70 private static final int LINES_BEFORE_METHOD = 12; 71 72 @Test rawFactoryType()73 public void rawFactoryType() { 74 Source module = 75 moduleWithMethod( 76 "@Binds", 77 "@IntoMap", 78 "@ClassKey(FooActivity.class)", 79 "abstract AndroidInjector.Factory bindRawFactory(FooActivity.Factory factory);"); 80 compile(module, FOO_ACTIVITY) 81 .compile( 82 subject -> { 83 subject.compilationDidFail(); 84 subject.hasErrorContaining( 85 "should bind dagger.android.AndroidInjector.Factory<?>, " 86 + "not dagger.android.AndroidInjector.Factory"); 87 }); 88 } 89 90 @Test wildCardFactoryType()91 public void wildCardFactoryType() { 92 Source module = 93 CompilerTests.kotlinSource( 94 "AndroidModule.kt", 95 "package test", 96 "", 97 "import dagger.Module", 98 "import dagger.Binds", 99 "import dagger.android.AndroidInjector", 100 "import dagger.multibindings.ClassKey", 101 "import dagger.multibindings.IntoMap", 102 "", 103 "@Module", 104 "internal abstract class AndroidModule {", 105 " @Binds", 106 " @IntoMap", 107 " @ClassKey(FooActivity::class)", 108 " abstract fun bindWildcardFactory(factory: FooActivity.Factory):" 109 + " AndroidInjector.Factory<*>", 110 "}"); 111 compile(module, FOO_ACTIVITY).compile(subject -> subject.hasErrorCount(0)); 112 } 113 114 @Test rawBuilderType()115 public void rawBuilderType() { 116 Source module = 117 moduleWithMethod( 118 "@Binds", 119 "@IntoMap", 120 "@ClassKey(FooActivity.class)", 121 "abstract AndroidInjector.Builder bindRawBuilder(FooActivity.Builder builder);"); 122 compile(module, FOO_ACTIVITY) 123 .compile( 124 subject -> { 125 subject.compilationDidFail(); 126 subject.hasErrorContaining( 127 "should bind dagger.android.AndroidInjector.Factory<?>, " 128 + "not dagger.android.AndroidInjector.Builder"); 129 }); 130 } 131 132 @Test bindsToBuilderNotFactory()133 public void bindsToBuilderNotFactory() { 134 Source module = 135 moduleWithMethod( 136 "@Binds", 137 "@IntoMap", 138 "@ClassKey(FooActivity.class)", 139 "abstract AndroidInjector.Builder<?> bindBuilder(", 140 " FooActivity.Builder builder);"); 141 compile(module, FOO_ACTIVITY) 142 .compile( 143 subject -> { 144 subject.compilationDidFail(); 145 subject.hasErrorContaining( 146 "should bind dagger.android.AndroidInjector.Factory<?>, not " 147 + "dagger.android.AndroidInjector.Builder<?>"); 148 }); 149 } 150 151 @Test providesToBuilderNotFactory()152 public void providesToBuilderNotFactory() { 153 Source module = 154 moduleWithMethod( 155 "@Provides", 156 "@IntoMap", 157 "@ClassKey(FooActivity.class)", 158 "static AndroidInjector.Builder<?> bindBuilder(FooActivity.Builder builder) {", 159 " return builder;", 160 "}"); 161 compile(module, FOO_ACTIVITY) 162 .compile( 163 subject -> { 164 subject.compilationDidFail(); 165 subject.hasErrorContaining( 166 "should bind dagger.android.AndroidInjector.Factory<?>, not " 167 + "dagger.android.AndroidInjector.Builder<?>"); 168 }); 169 } 170 171 @Test bindsToConcreteTypeInsteadOfWildcard()172 public void bindsToConcreteTypeInsteadOfWildcard() { 173 Source module = 174 moduleWithMethod( 175 "@Binds", 176 "@IntoMap", 177 "@ClassKey(FooActivity.class)", 178 "abstract AndroidInjector.Builder<FooActivity> bindBuilder(", 179 " FooActivity.Builder builder);"); 180 compile(module, FOO_ACTIVITY) 181 .compile( 182 subject -> { 183 subject.compilationDidFail(); 184 subject.hasErrorContaining( 185 "should bind dagger.android.AndroidInjector.Factory<?>, not " 186 + "dagger.android.AndroidInjector.Builder<test.FooActivity>"); 187 }); 188 } 189 190 @Test bindsToBaseTypeInsteadOfWildcard()191 public void bindsToBaseTypeInsteadOfWildcard() { 192 Source module = 193 moduleWithMethod( 194 "@Binds", 195 "@IntoMap", 196 "@ClassKey(FooActivity.class)", 197 "abstract AndroidInjector.Builder<Activity> bindBuilder(", 198 " FooActivity.Builder builder);"); 199 compile(module, FOO_ACTIVITY) 200 .compile( 201 subject -> { 202 subject.compilationDidFail(); 203 subject.hasErrorContaining( 204 "@Binds methods' parameter type must be assignable to the return type"); 205 }); 206 } 207 208 @Test bindsCorrectType()209 public void bindsCorrectType() { 210 Source module = 211 moduleWithMethod( 212 "@Binds", 213 "@IntoMap", 214 "@ClassKey(FooActivity.class)", 215 "abstract AndroidInjector.Factory<?> bindCorrectType(FooActivity.Builder builder);"); 216 compile(module, FOO_ACTIVITY) 217 .compile( 218 subject -> { 219 subject.hasErrorCount(0); 220 subject.hasNoWarnings(); 221 }); 222 } 223 224 @Test bindsCorrectType_AndroidInjectionKey()225 public void bindsCorrectType_AndroidInjectionKey() { 226 Source module = 227 moduleWithMethod( 228 "@Binds", 229 "@IntoMap", 230 "@AndroidInjectionKey(\"test.FooActivity\")", 231 "abstract AndroidInjector.Factory<?> bindCorrectType(FooActivity.Builder builder);"); 232 compile(module, FOO_ACTIVITY) 233 .compile( 234 subject -> { 235 subject.hasErrorCount(0); 236 subject.hasNoWarnings(); 237 }); 238 } 239 240 @Test bindsCorrectType_AndroidInjectionKey_unbounded()241 public void bindsCorrectType_AndroidInjectionKey_unbounded() { 242 Source module = 243 moduleWithMethod( 244 "@Binds", 245 "@IntoMap", 246 "@AndroidInjectionKey(\"test.FooActivity\")", 247 "abstract AndroidInjector.Factory<?> bindCorrectType(FooActivity.Builder builder);"); 248 compile(module, FOO_ACTIVITY) 249 .compile( 250 subject -> { 251 subject.hasErrorCount(0); 252 subject.hasNoWarnings(); 253 }); 254 } 255 256 @Test bindsWithScope()257 public void bindsWithScope() { 258 Source module = 259 moduleWithMethod( 260 "@Binds", 261 "@IntoMap", 262 "@ClassKey(FooActivity.class)", 263 "@Singleton", 264 "abstract AndroidInjector.Factory<?> bindWithScope(FooActivity.Builder builder);"); 265 compile(module, FOO_ACTIVITY) 266 .compile( 267 subject -> { 268 subject.compilationDidFail(); 269 subject.hasErrorContaining("should not be scoped"); 270 }); 271 } 272 273 @Test bindsWithScope_suppressWarnings()274 public void bindsWithScope_suppressWarnings() { 275 Source module = 276 moduleWithMethod( 277 "@SuppressWarnings(\"dagger.android.ScopedInjectorFactory\")", 278 "@Binds", 279 "@IntoMap", 280 "@ClassKey(FooActivity.class)", 281 "@Singleton", 282 "abstract AndroidInjector.Factory<?> bindWithScope(FooActivity.Builder builder);"); 283 compile(module, FOO_ACTIVITY) 284 .compile( 285 subject -> { 286 subject.hasErrorCount(0); 287 subject.hasNoWarnings(); 288 }); 289 } 290 291 @Test mismatchedMapKey_bindsFactory()292 public void mismatchedMapKey_bindsFactory() { 293 Source module = 294 moduleWithMethod( 295 "@Binds", 296 "@IntoMap", 297 "@ClassKey(BarActivity.class)", 298 "abstract AndroidInjector.Factory<?> mismatchedFactory(FooActivity.Factory factory);"); 299 compile(module, FOO_ACTIVITY, BAR_ACTIVITY) 300 .compile( 301 subject -> { 302 subject.compilationDidFail(); 303 subject 304 .hasErrorContaining( 305 "test.FooActivity.Factory does not implement" 306 + " AndroidInjector<test.BarActivity>") 307 .onLine(LINES_BEFORE_METHOD + 3); 308 }); 309 } 310 311 @Test mismatchedMapKey_bindsBuilder()312 public void mismatchedMapKey_bindsBuilder() { 313 Source module = 314 moduleWithMethod( 315 "@Binds", 316 "@IntoMap", 317 "@ClassKey(BarActivity.class)", 318 "abstract AndroidInjector.Factory<?> mismatchedBuilder(FooActivity.Builder builder);"); 319 compile(module, FOO_ACTIVITY, BAR_ACTIVITY) 320 .compile( 321 subject -> { 322 subject.compilationDidFail(); 323 subject 324 .hasErrorContaining( 325 "test.FooActivity.Builder does not implement" 326 + " AndroidInjector<test.BarActivity>") 327 .onLine(LINES_BEFORE_METHOD + 3); 328 }); 329 } 330 331 @Test mismatchedMapKey_bindsBuilder_androidInjectionKey()332 public void mismatchedMapKey_bindsBuilder_androidInjectionKey() { 333 Source module = 334 moduleWithMethod( 335 "@Binds", 336 "@IntoMap", 337 "@AndroidInjectionKey(\"test.BarActivity\")", 338 "abstract AndroidInjector.Factory<?> mismatchedBuilder(FooActivity.Builder builder);"); 339 compile(module, FOO_ACTIVITY, BAR_ACTIVITY) 340 .compile( 341 subject -> { 342 subject.compilationDidFail(); 343 subject 344 .hasErrorContaining( 345 "test.FooActivity.Builder does not implement" 346 + " AndroidInjector<test.BarActivity>") 347 .onLine(LINES_BEFORE_METHOD + 3); 348 }); 349 } 350 351 @Test mismatchedMapKey_providesBuilder()352 public void mismatchedMapKey_providesBuilder() { 353 Source module = 354 moduleWithMethod( 355 "@Provides", 356 "@IntoMap", 357 "@ClassKey(BarActivity.class)", 358 "static AndroidInjector.Factory<?> mismatchedBuilder(FooActivity.Builder builder) {", 359 " return builder;", 360 "}"); 361 compile(module, FOO_ACTIVITY, BAR_ACTIVITY) 362 .compile( 363 subject -> { 364 subject.hasErrorCount(0); 365 subject.hasNoWarnings(); 366 }); 367 } 368 369 @Test bindsQualifier_ignoresChecks()370 public void bindsQualifier_ignoresChecks() { 371 Source module = 372 moduleWithMethod( 373 "@Binds", 374 "@IntoMap", 375 "@ClassKey(FooActivity.class)", 376 "@Named(\"unused\")", 377 // normally this should fail, since it is binding to a Builder not a Factory 378 "abstract AndroidInjector.Builder<?> bindsBuilderWithQualifier(", 379 " FooActivity.Builder builder);"); 380 compile(module, FOO_ACTIVITY) 381 .compile( 382 subject -> { 383 subject.hasErrorCount(0); 384 subject.hasNoWarnings(); 385 }); 386 } 387 388 @Test bindToPrimitive()389 public void bindToPrimitive() { 390 Source module = 391 moduleWithMethod( 392 "@Binds", 393 "@IntoMap", 394 "@AndroidInjectionKey(\"test.FooActivity\")", 395 "abstract int bindInt(@Named(\"unused\") int otherInt);"); 396 compile(module, FOO_ACTIVITY) 397 .compile( 398 subject -> { 399 subject.hasErrorCount(0); 400 subject.hasNoWarnings(); 401 }); 402 } 403 404 @Test bindToNonFrameworkClass()405 public void bindToNonFrameworkClass() { 406 Source module = 407 moduleWithMethod( 408 "@Binds", 409 "@IntoMap", 410 "@AndroidInjectionKey(\"test.FooActivity\")", 411 "abstract Number bindInt(Integer integer);"); 412 compile(module, FOO_ACTIVITY) 413 .compile( 414 subject -> { 415 subject.hasErrorCount(0); 416 subject.hasNoWarnings(); 417 }); 418 } 419 420 @Test invalidBindsMethod()421 public void invalidBindsMethod() { 422 Source module = 423 moduleWithMethod( 424 "@Binds", 425 "@IntoMap", 426 "@ClassKey(FooActivity.class)", 427 "abstract AndroidInjector.Factory<?> bindCorrectType(", 428 " FooActivity.Builder builder, FooActivity.Builder builder2);"); 429 compile(module, FOO_ACTIVITY).compile(subject -> subject.compilationDidFail()); 430 } 431 compile(Source... files)432 private DaggerCompiler compile(Source... files) { 433 return CompilerTests.daggerCompiler(files) 434 .withAdditionalJavacProcessors(new AndroidProcessor()) 435 .withAdditionalKspProcessors(new KspAndroidProcessor.Provider()); 436 } 437 } 438