1 /* 2 * Copyright (C) 2023 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.functional.kotlinsrc.nullables 18 19 import com.google.common.truth.Truth.assertThat 20 import dagger.Component 21 import dagger.Module 22 import dagger.Provides 23 import dagger.Reusable 24 import dagger.multibindings.IntoSet 25 import java.lang.NullPointerException 26 import javax.inject.Inject 27 import javax.inject.Provider 28 import javax.inject.Singleton 29 import org.junit.Assert.fail 30 import org.junit.Test 31 import org.junit.runner.RunWith 32 import org.junit.runners.JUnit4 33 34 @RunWith(JUnit4::class) 35 class NullabilityTest { 36 @Component(dependencies = [NullComponent::class]) 37 internal interface NullComponentWithDependency { stringnull38 fun string(): String? 39 40 fun number(): Number 41 42 fun stringProvider(): Provider<String> 43 44 fun numberProvider(): Provider<Number> 45 } 46 47 interface Bar<T> {} 48 49 class BarNoNull : Bar<String> 50 51 class BarWithNull : Bar<String?> 52 53 @Component(modules = [NullModule::class]) 54 internal interface NullComponent { stringnull55 fun string(): String? 56 57 fun integer(): Int? 58 59 fun nullFoo(): NullFoo 60 61 fun number(): Number 62 63 fun stringProvider(): Provider<String> 64 65 fun numberProvider(): Provider<Number> 66 67 fun setOfBar(): Set<Bar<String>> 68 69 fun setOfBarWithNullableArg(): Set<Bar<String?>> 70 } 71 72 @Module 73 internal class NullModule { 74 var numberValue: Number? = null 75 var integerCallCount = 0 76 77 @IntoSet @Provides fun providesNullableStringInToSet(): Bar<String?> = BarWithNull() 78 79 @IntoSet @Provides fun providesNonNullStringIntoSet(): Bar<String> = BarNoNull() 80 81 @Provides fun provideNullableString(): String? = null 82 83 @Provides fun provideNumber(): Number = numberValue!! 84 85 @Provides 86 @Reusable 87 fun provideNullReusableInteger(): Int? { 88 integerCallCount++ 89 return null 90 } 91 } 92 93 @Suppress("BadInject") // This is just for testing purposes. 94 internal class NullFoo 95 @Inject 96 constructor( 97 val string: String?, 98 val number: Number, 99 val stringProvider: Provider<String>, 100 val numberProvider: Provider<Number>, 101 ) { 102 var methodInjectedString: String? = null 103 lateinit var methodInjectedNumber: Number 104 lateinit var methodInjectedStringProvider: Provider<String> 105 lateinit var methodInjectedNumberProvider: Provider<Number> 106 107 @Inject injectnull108 fun inject( 109 string: String?, 110 number: Number, 111 stringProvider: Provider<String>, 112 numberProvider: Provider<Number>, 113 ) { 114 methodInjectedString = string 115 methodInjectedNumber = number 116 methodInjectedStringProvider = stringProvider 117 methodInjectedNumberProvider = numberProvider 118 } 119 120 @JvmField @Inject var fieldInjectedString: String? = null 121 @Inject lateinit var fieldInjectedNumber: Number 122 @Inject lateinit var fieldInjectedStringProvider: Provider<String> 123 @Inject lateinit var fieldInjectedNumberProvider: Provider<Number> 124 } 125 126 @Test testNullability_providesnull127 fun testNullability_provides() { 128 val module = NullModule() 129 val component = DaggerNullabilityTest_NullComponent.builder().nullModule(module).build() 130 131 // Can't construct NullFoo because it depends on Number, and Number was null. 132 try { 133 component.nullFoo() 134 fail() 135 } catch (npe: NullPointerException) { 136 // NOTE: In Java we would check that the Dagger error message is something like: 137 // "Cannot return null from a non-@Nullable @Provides method" 138 // However, in Kotlin there's no way to return a null value from a non-null return type 139 // without explicitly using `!!`, which results in an error before Dagger's runtime 140 // checkNotNull has a chance to run. 141 } 142 143 // set number to non-null so we can create 144 module.numberValue = 1 145 val nullFoo = component.nullFoo() 146 147 // Then set it back to null so we can test its providers. 148 module.numberValue = null 149 validate(nullFoo.string, nullFoo.stringProvider, nullFoo.numberProvider) 150 validate( 151 nullFoo.methodInjectedString, 152 nullFoo.methodInjectedStringProvider, 153 nullFoo.methodInjectedNumberProvider, 154 ) 155 validate( 156 nullFoo.fieldInjectedString, 157 nullFoo.fieldInjectedStringProvider, 158 nullFoo.fieldInjectedNumberProvider, 159 ) 160 } 161 162 @Test testNullability_reusuablenull163 fun testNullability_reusuable() { 164 val module = NullModule() 165 val component = DaggerNullabilityTest_NullComponent.builder().nullModule(module).build() 166 167 // Test that the @Nullable @Reusuable binding is cached properly even when the value is null. 168 assertThat(module.integerCallCount).isEqualTo(0) 169 assertThat(component.integer()).isNull() 170 assertThat(module.integerCallCount).isEqualTo(1) 171 assertThat(component.integer()).isNull() 172 assertThat(module.integerCallCount).isEqualTo(1) 173 assertThat(component.setOfBar().size).isEqualTo(2) 174 assertThat(component.setOfBarWithNullableArg().size).isEqualTo(2) 175 } 176 177 @Test testNullability_componentsnull178 fun testNullability_components() { 179 val nullComponent: NullComponent = 180 object : NullComponent { 181 override fun string(): String? = null 182 183 override fun integer(): Int? = null 184 185 override fun stringProvider(): Provider<String> = Provider { null!! } 186 187 override fun numberProvider(): Provider<Number> = Provider { null!! } 188 189 override fun number(): Number = null!! 190 191 override fun nullFoo(): NullFoo = null!! 192 193 override fun setOfBar(): Set<Bar<String>> = emptySet() 194 195 override fun setOfBarWithNullableArg(): Set<Bar<String?>> = emptySet() 196 } 197 val component = 198 DaggerNullabilityTest_NullComponentWithDependency.builder() 199 .nullComponent(nullComponent) 200 .build() 201 validate(component.string(), component.stringProvider(), component.numberProvider()) 202 203 // Also validate that the component's number() method fails 204 try { 205 component.number() 206 fail() 207 } catch (npe: NullPointerException) { 208 // NOTE: In Java we would check that the Dagger error message is something like: 209 // "Cannot return null from a non-@Nullable @Provides method" 210 // However, in Kotlin there's no way to return a null value from a non-null return type 211 // without explicitly using `!!`, which results in an error before Dagger's runtime 212 // checkNotNull has a chance to run. 213 } 214 } 215 validatenull216 private fun validate( 217 string: String?, 218 stringProvider: Provider<String>, 219 numberProvider: Provider<Number>, 220 ) { 221 assertThat(string).isNull() 222 assertThat(numberProvider).isNotNull() 223 try { 224 numberProvider.get() 225 fail() 226 } catch (npe: NullPointerException) { 227 // NOTE: In Java we would check that the Dagger error message is something like: 228 // "Cannot return null from a non-@Nullable @Provides method" 229 // However, in Kotlin there's no way to return a null value from a non-null return type 230 // without explicitly using `!!`, which results in an error before Dagger's runtime 231 // checkNotNull has a chance to run. 232 } 233 assertThat(stringProvider.get()).isNull() 234 } 235 } 236