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 java.lang.NullPointerException 25 import javax.inject.Inject 26 import javax.inject.Provider 27 import org.junit.Assert.fail 28 import org.junit.Test 29 import org.junit.runner.RunWith 30 import org.junit.runners.JUnit4 31 32 @RunWith(JUnit4::class) 33 class NullabilityTest { 34 @Component(dependencies = [NullComponent::class]) 35 internal interface NullComponentWithDependency { stringnull36 fun string(): String? 37 fun number(): Number 38 fun stringProvider(): Provider<String> 39 fun numberProvider(): Provider<Number> 40 } 41 42 @Component(modules = [NullModule::class]) 43 internal interface NullComponent { 44 fun string(): String? 45 fun integer(): Int? 46 fun nullFoo(): NullFoo 47 fun number(): Number 48 fun stringProvider(): Provider<String> 49 fun numberProvider(): Provider<Number> 50 } 51 52 @Module 53 internal class NullModule { 54 var numberValue: Number? = null 55 var integerCallCount = 0 56 provideNullableStringnull57 @Provides fun provideNullableString(): String? = null 58 59 @Provides fun provideNumber(): Number = numberValue!! 60 61 @Provides 62 @Reusable 63 fun provideNullReusableInteger(): Int? { 64 integerCallCount++ 65 return null 66 } 67 } 68 69 @Suppress("BadInject") // This is just for testing purposes. 70 internal class NullFoo 71 @Inject 72 constructor( 73 val string: String?, 74 val number: Number, 75 val stringProvider: Provider<String>, 76 val numberProvider: Provider<Number> 77 ) { 78 var methodInjectedString: String? = null 79 lateinit var methodInjectedNumber: Number 80 lateinit var methodInjectedStringProvider: Provider<String> 81 lateinit var methodInjectedNumberProvider: Provider<Number> 82 83 @Inject injectnull84 fun inject( 85 string: String?, 86 number: Number, 87 stringProvider: Provider<String>, 88 numberProvider: Provider<Number> 89 ) { 90 methodInjectedString = string 91 methodInjectedNumber = number 92 methodInjectedStringProvider = stringProvider 93 methodInjectedNumberProvider = numberProvider 94 } 95 96 @JvmField @Inject var fieldInjectedString: String? = null 97 @Inject lateinit var fieldInjectedNumber: Number 98 @Inject lateinit var fieldInjectedStringProvider: Provider<String> 99 @Inject lateinit var fieldInjectedNumberProvider: Provider<Number> 100 } 101 102 @Test testNullability_providesnull103 fun testNullability_provides() { 104 val module = NullModule() 105 val component = DaggerNullabilityTest_NullComponent.builder().nullModule(module).build() 106 107 // Can't construct NullFoo because it depends on Number, and Number was null. 108 try { 109 component.nullFoo() 110 fail() 111 } catch (npe: NullPointerException) { 112 // NOTE: In Java we would check that the Dagger error message is something like: 113 // "Cannot return null from a non-@Nullable @Provides method" 114 // However, in Kotlin there's no way to return a null value from a non-null return type 115 // without explicitly using `!!`, which results in an error before Dagger's runtime 116 // checkNotNull has a chance to run. 117 } 118 119 // set number to non-null so we can create 120 module.numberValue = 1 121 val nullFoo = component.nullFoo() 122 123 // Then set it back to null so we can test its providers. 124 module.numberValue = null 125 validate(nullFoo.string, nullFoo.stringProvider, nullFoo.numberProvider) 126 validate( 127 nullFoo.methodInjectedString, 128 nullFoo.methodInjectedStringProvider, 129 nullFoo.methodInjectedNumberProvider 130 ) 131 validate( 132 nullFoo.fieldInjectedString, 133 nullFoo.fieldInjectedStringProvider, 134 nullFoo.fieldInjectedNumberProvider 135 ) 136 } 137 138 @Test testNullability_reusuablenull139 fun testNullability_reusuable() { 140 val module = NullModule() 141 val component = DaggerNullabilityTest_NullComponent.builder().nullModule(module).build() 142 143 // Test that the @Nullable @Reusuable binding is cached properly even when the value is null. 144 assertThat(module.integerCallCount).isEqualTo(0) 145 assertThat(component.integer()).isNull() 146 assertThat(module.integerCallCount).isEqualTo(1) 147 assertThat(component.integer()).isNull() 148 assertThat(module.integerCallCount).isEqualTo(1) 149 } 150 151 @Test testNullability_componentsnull152 fun testNullability_components() { 153 val nullComponent: NullComponent = 154 object : NullComponent { 155 override fun string(): String? = null 156 override fun integer(): Int? = null 157 override fun stringProvider(): Provider<String> = Provider { null!! } 158 override fun numberProvider(): Provider<Number> = Provider { null!! } 159 override fun number(): Number = null!! 160 override fun nullFoo(): NullFoo = null!! 161 } 162 val component = 163 DaggerNullabilityTest_NullComponentWithDependency.builder() 164 .nullComponent(nullComponent) 165 .build() 166 validate(component.string(), component.stringProvider(), component.numberProvider()) 167 168 // Also validate that the component's number() method fails 169 try { 170 component.number() 171 fail() 172 } catch (npe: NullPointerException) { 173 // NOTE: In Java we would check that the Dagger error message is something like: 174 // "Cannot return null from a non-@Nullable @Provides method" 175 // However, in Kotlin there's no way to return a null value from a non-null return type 176 // without explicitly using `!!`, which results in an error before Dagger's runtime 177 // checkNotNull has a chance to run. 178 } 179 } 180 validatenull181 private fun validate( 182 string: String?, 183 stringProvider: Provider<String>, 184 numberProvider: Provider<Number> 185 ) { 186 assertThat(string).isNull() 187 assertThat(numberProvider).isNotNull() 188 try { 189 numberProvider.get() 190 fail() 191 } catch (npe: NullPointerException) { 192 // NOTE: In Java we would check that the Dagger error message is something like: 193 // "Cannot return null from a non-@Nullable @Provides method" 194 // However, in Kotlin there's no way to return a null value from a non-null return type 195 // without explicitly using `!!`, which results in an error before Dagger's runtime 196 // checkNotNull has a chance to run. 197 } 198 assertThat(stringProvider.get()).isNull() 199 } 200 } 201