diff --git a/dagger-compiler/BUILD b/dagger-compiler/BUILD index f322a2a8a9d..48c0f7f8132 100644 --- a/dagger-compiler/BUILD +++ b/dagger-compiler/BUILD @@ -52,6 +52,7 @@ gen_maven_artifact( "//dagger-compiler/main/java/dagger/internal/codegen/validation", "//dagger-compiler/main/java/dagger/internal/codegen/writing", "//dagger-compiler/main/java/dagger/internal/codegen/xprocessing", + "//dagger-compiler/main/java/dagger/internal/codegen/xprocessing:nullability", "//dagger-compiler/main/java/dagger/internal/codegen/xprocessing:xpoet", ], artifact_target_maven_deps = [ diff --git a/dagger-compiler/main/java/dagger/internal/codegen/binding/BUILD b/dagger-compiler/main/java/dagger/internal/codegen/binding/BUILD index d135224e998..012df298565 100644 --- a/dagger-compiler/main/java/dagger/internal/codegen/binding/BUILD +++ b/dagger-compiler/main/java/dagger/internal/codegen/binding/BUILD @@ -30,6 +30,7 @@ java_library( "//dagger-compiler/main/java/dagger/internal/codegen/kotlin", "//dagger-compiler/main/java/dagger/internal/codegen/model", "//dagger-compiler/main/java/dagger/internal/codegen/xprocessing", + "//dagger-compiler/main/java/dagger/internal/codegen/xprocessing:nullability", "//dagger-compiler/main/java/dagger/internal/codegen/xprocessing:xpoet", "//dagger-runtime/main/java/dagger:core", "//dagger-spi", diff --git a/dagger-compiler/main/java/dagger/internal/codegen/compileroption/CompilerOptions.java b/dagger-compiler/main/java/dagger/internal/codegen/compileroption/CompilerOptions.java index 74f5f6c0052..788b692aa70 100644 --- a/dagger-compiler/main/java/dagger/internal/codegen/compileroption/CompilerOptions.java +++ b/dagger-compiler/main/java/dagger/internal/codegen/compileroption/CompilerOptions.java @@ -16,7 +16,9 @@ package dagger.internal.codegen.compileroption; +import androidx.room3.compiler.processing.XProcessingEnv; import androidx.room3.compiler.processing.XTypeElement; +import com.google.common.base.Ascii; import javax.tools.Diagnostic; /** A collection of options that dictate how the compiler will run. */ @@ -168,4 +170,34 @@ public int keysPerComponentShard(XTypeElement component) { * boundaries at compile time (for Maps) and runtime (for Sets). */ public abstract boolean mapMultibindingDuplicateDetectionFix(); + + /** + * Returns {@code true} if Dagger should also look for nullable type annotations. + * + * Note that when disabled, Dagger doesn't disallow usage of nullable type annotations. Instead, + * the behavior is simply that Dagger does not look for them, so types marked with nullable type + * annotations may appear to be non-nullable. + */ + public abstract boolean nullableTypeAnnotations(); + + /** + * Returns {@code true} if Dagger should also look for nullable type annotations. + * + * @deprecated use {@link CompilerOptions#nullableTypeAnnotations()}. This method should only be + * used for legacy code which requires accessing this flag from static methods that don't + * easily have access to an instance of {@link CompilerOptions}. + */ + @Deprecated + public static boolean nullableTypeAnnotations(XProcessingEnv processingEnv) { + String optionName = + ProcessingEnvironmentCompilerOptions.Feature.NULLABLE_TYPE_ANNOTATIONS.toString(); + String defaultValue = + ProcessingEnvironmentCompilerOptions.Feature.NULLABLE_TYPE_ANNOTATIONS + .defaultValue().name(); + String optionValue = + processingEnv.getOptions().containsKey(optionName) + ? processingEnv.getOptions().get(optionName) + : defaultValue; + return Ascii.equalsIgnoreCase(optionValue, FeatureStatus.ENABLED.name()); + } } diff --git a/dagger-compiler/main/java/dagger/internal/codegen/compileroption/ProcessingEnvironmentCompilerOptions.java b/dagger-compiler/main/java/dagger/internal/codegen/compileroption/ProcessingEnvironmentCompilerOptions.java index b3c9d59568b..db5a0ebacf6 100644 --- a/dagger-compiler/main/java/dagger/internal/codegen/compileroption/ProcessingEnvironmentCompilerOptions.java +++ b/dagger-compiler/main/java/dagger/internal/codegen/compileroption/ProcessingEnvironmentCompilerOptions.java @@ -32,6 +32,7 @@ import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.IGNORE_PROVISION_KEY_WILDCARDS; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.INCLUDE_STACKTRACE_WITH_DEFERRED_ERROR_MESSAGES; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.MAP_MULTIBINDING_DUPLICATE_DETECTION_FIX; +import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.NULLABLE_TYPE_ANNOTATIONS; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.PLUGINS_VISIT_FULL_BINDING_GRAPHS; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.STRICT_MULTIBINDING_VALIDATION; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.STRICT_SUPERFICIAL_VALIDATION; @@ -216,6 +217,11 @@ public boolean mapMultibindingDuplicateDetectionFix() { return isEnabled(MAP_MULTIBINDING_DUPLICATE_DETECTION_FIX); } + @Override + public boolean nullableTypeAnnotations() { + return isEnabled(NULLABLE_TYPE_ANNOTATIONS); + } + @Override public int keysPerComponentShard(XTypeElement component) { if (options.containsKey(KEYS_PER_COMPONENT_SHARD)) { @@ -357,7 +363,9 @@ enum Feature implements EnumOption { VALIDATE_TRANSITIVE_COMPONENT_DEPENDENCIES(ENABLED), - MAP_MULTIBINDING_DUPLICATE_DETECTION_FIX(DISABLED); + MAP_MULTIBINDING_DUPLICATE_DETECTION_FIX(DISABLED), + + NULLABLE_TYPE_ANNOTATIONS; final FeatureStatus defaultValue; diff --git a/dagger-compiler/main/java/dagger/internal/codegen/javac/JavacPluginCompilerOptions.java b/dagger-compiler/main/java/dagger/internal/codegen/javac/JavacPluginCompilerOptions.java index 660b0fc9222..065ed1fbb5d 100644 --- a/dagger-compiler/main/java/dagger/internal/codegen/javac/JavacPluginCompilerOptions.java +++ b/dagger-compiler/main/java/dagger/internal/codegen/javac/JavacPluginCompilerOptions.java @@ -145,4 +145,9 @@ public boolean ignoreProvisionKeyWildcards() { public boolean mapMultibindingDuplicateDetectionFix() { return false; } + + @Override + public boolean nullableTypeAnnotations() { + return false; + } } diff --git a/dagger-compiler/main/java/dagger/internal/codegen/validation/BUILD b/dagger-compiler/main/java/dagger/internal/codegen/validation/BUILD index 0453eb22aea..f46b8297046 100644 --- a/dagger-compiler/main/java/dagger/internal/codegen/validation/BUILD +++ b/dagger-compiler/main/java/dagger/internal/codegen/validation/BUILD @@ -31,6 +31,7 @@ java_library( "//dagger-compiler/main/java/dagger/internal/codegen/kotlin", "//dagger-compiler/main/java/dagger/internal/codegen/model", "//dagger-compiler/main/java/dagger/internal/codegen/xprocessing", + "//dagger-compiler/main/java/dagger/internal/codegen/xprocessing:nullability", "//dagger-runtime/main/java/dagger:core", "//dagger-spi", "//third_party/java/auto:value", diff --git a/dagger-compiler/main/java/dagger/internal/codegen/writing/BUILD b/dagger-compiler/main/java/dagger/internal/codegen/writing/BUILD index bbdeeed599d..0ae023a92f4 100644 --- a/dagger-compiler/main/java/dagger/internal/codegen/writing/BUILD +++ b/dagger-compiler/main/java/dagger/internal/codegen/writing/BUILD @@ -30,6 +30,7 @@ java_library( "//dagger-compiler/main/java/dagger/internal/codegen/compileroption", "//dagger-compiler/main/java/dagger/internal/codegen/model", "//dagger-compiler/main/java/dagger/internal/codegen/xprocessing", + "//dagger-compiler/main/java/dagger/internal/codegen/xprocessing:nullability", "//dagger-compiler/main/java/dagger/internal/codegen/xprocessing:xpoet", "//dagger-runtime/main/java/dagger:core", "//dagger-spi", diff --git a/dagger-compiler/main/java/dagger/internal/codegen/xprocessing/BUILD b/dagger-compiler/main/java/dagger/internal/codegen/xprocessing/BUILD index 5e8647965c7..703a6501e05 100644 --- a/dagger-compiler/main/java/dagger/internal/codegen/xprocessing/BUILD +++ b/dagger-compiler/main/java/dagger/internal/codegen/xprocessing/BUILD @@ -20,6 +20,10 @@ load("//tools:bazel_compat.bzl", "compat_kt_jvm_library") package(default_visibility = ["//dagger-compiler:internal"]) +NULLABILITY_SRCS = [ + "Nullability.java", +] + XPOET_SRCS = [ "Accessibility.java", "NullableTypeNames.java", @@ -33,10 +37,25 @@ XPOET_SRCS = [ "XTypeSpecs.java", ] +java_library( + name = "nullability", + srcs = NULLABILITY_SRCS, + deps = [ + ":xprocessing", + "//dagger-compiler/main/java/dagger/internal/codegen/compileroption", + "//dagger-spi", + "//third_party/java/auto:value", + "//third_party/java/guava/base", + "//third_party/java/guava/collect", + "//third_party/java/javapoet", + ], +) + java_library( name = "xpoet", srcs = XPOET_SRCS, deps = [ + ":nullability", ":xprocessing", "//dagger-compiler/main/java/dagger/internal/codegen/compileroption", "//dagger-spi", @@ -57,7 +76,7 @@ compat_kt_jvm_library( "*.java", "*.kt", ], - exclude = XPOET_SRCS, + exclude = XPOET_SRCS + NULLABILITY_SRCS, ), exports = [ ":xprocessing-lib", diff --git a/dagger-compiler/main/java/dagger/internal/codegen/xprocessing/Nullability.java b/dagger-compiler/main/java/dagger/internal/codegen/xprocessing/Nullability.java index 35ebf64f2d1..cb830fb8287 100644 --- a/dagger-compiler/main/java/dagger/internal/codegen/xprocessing/Nullability.java +++ b/dagger-compiler/main/java/dagger/internal/codegen/xprocessing/Nullability.java @@ -18,6 +18,7 @@ import static androidx.room3.compiler.processing.XElementKt.isMethod; import static androidx.room3.compiler.processing.XElementKt.isVariableElement; +import static androidx.room3.compiler.processing.compat.XConverters.getProcessingEnv; import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList; import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet; import static dagger.internal.codegen.xprocessing.XElements.asMethod; @@ -36,6 +37,7 @@ import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; +import dagger.internal.codegen.compileroption.CompilerOptions; import java.util.Optional; /** @@ -57,8 +59,12 @@ public abstract class Nullability { public static Nullability of(XElement element) { ImmutableSet nonTypeUseNullableAnnotations = getNullableAnnotations(element); Optional type = getType(element); - ImmutableSet typeUseNullableAnnotations = - ImmutableSet.of(); + ImmutableSet typeUseNullableAnnotations; + if (CompilerOptions.nullableTypeAnnotations(getProcessingEnv(element))) { + typeUseNullableAnnotations = type.map(Nullability::getNullableAnnotations).orElse(ImmutableSet.of()); + } else { + typeUseNullableAnnotations = ImmutableSet.of(); + } boolean isKotlinTypeNullable = // Note: Technically, it isn't possible for Java sources to have nullable types like in // Kotlin sources, but for some reason KSP treats certain types as nullable if they have a diff --git a/javatests/dagger/functional/jdk8/BUILD b/javatests/dagger/functional/jdk8/BUILD index c820e18000c..ec91f649acf 100644 --- a/javatests/dagger/functional/jdk8/BUILD +++ b/javatests/dagger/functional/jdk8/BUILD @@ -64,3 +64,27 @@ GenJavaTests( "//third_party/java/truth", ], ) + +GenJavaLibrary( + name = "type_use_nullability_classes", + srcs = ["TypeUseNullabilityClasses.java"], + javacopts = DOCLINT_HTML_AND_SYNTAX, + deps = [ + "//third_party/java/dagger", + ], +) + +# TODO(b/203233586): Replace with GenJavaTest +GenJavaTests( + name = "TypeUseNullabilityTest", + srcs = ["TypeUseNullabilityTest.java"], + gen_library_deps = [":type_use_nullability_classes"], + javacopts = DOCLINT_HTML_AND_SYNTAX + ["-Adagger.nullableTypeAnnotations=ENABLED"], + deps = [ + "//third_party/java/dagger", + "//third_party/java/guava/base", + "//third_party/java/guava/collect", + "//third_party/java/junit", + "//third_party/java/truth", + ], +) diff --git a/javatests/dagger/functional/jdk8/TypeUseNullabilityClasses.java b/javatests/dagger/functional/jdk8/TypeUseNullabilityClasses.java new file mode 100644 index 00000000000..91b91bfb8a2 --- /dev/null +++ b/javatests/dagger/functional/jdk8/TypeUseNullabilityClasses.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2026 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.functional.jdk8; + +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import javax.inject.Inject; + +/** + * This class is compiled separately from the test in order to test nullability across compilation + * boundaries. + */ +final class TypeUseNullabilityClasses { + + static final String EXPECTED_STRING = "foo"; + static final Integer EXPECTED_INTEGER = 123; + + static final class NullFoo { + final Integer nullableInteger; + + @Inject + NullFoo(@TypeUse.Nullable Integer nullableInteger) { + this.nullableInteger = nullableInteger; + } + + @Inject @TypeUse.Nullable Integer nullableIntegerField; + + Integer nullableMethodInjectedField; + + @Inject + void inject(@TypeUse.Nullable Integer nullableMethodInjectedField) { + this.nullableMethodInjectedField = nullableMethodInjectedField; + } + } + + static final class GenericFoo { + T t; + + GenericFoo(T t) { + this.t = t; + } + } + + static final class GenericBar { + @Inject @TypeUse.Nullable T t; + } + + static class TypeUse { + @Target(TYPE_USE) + @Retention(RUNTIME) + @interface Nullable {} + + private TypeUse() {} + } + + private TypeUseNullabilityClasses() {} +} diff --git a/javatests/dagger/functional/jdk8/TypeUseNullabilityTest.java b/javatests/dagger/functional/jdk8/TypeUseNullabilityTest.java new file mode 100644 index 00000000000..d2bc9cab43a --- /dev/null +++ b/javatests/dagger/functional/jdk8/TypeUseNullabilityTest.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2026 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.functional.jdk8; + +import static com.google.common.truth.Truth.assertThat; +import static dagger.functional.jdk8.TypeUseNullabilityClasses.EXPECTED_INTEGER; +import static dagger.functional.jdk8.TypeUseNullabilityClasses.EXPECTED_STRING; +import static org.junit.Assert.assertThrows; + +import dagger.Component; +import dagger.Module; +import dagger.Provides; +import dagger.functional.jdk8.TypeUseNullabilityClasses.GenericBar; +import dagger.functional.jdk8.TypeUseNullabilityClasses.GenericFoo; +import dagger.functional.jdk8.TypeUseNullabilityClasses.NullFoo; +import dagger.functional.jdk8.TypeUseNullabilityClasses.TypeUse; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Similar to {@link dagger.functional.nullables.NullabilityTest}, but for TYPE_USE annotated + * nullable annotations. + */ +@RunWith(JUnit4.class) +public final class TypeUseNullabilityTest { + + @Component(modules = NullModule.class) + interface NullComponent { + NullFoo nullFoo(); + + String nonNullableString(); + + @TypeUse.Nullable + Integer nullableInteger(); + + // TODO determine validity of this + GenericFoo<@TypeUse.Nullable Object> genericFoo(); + + void injectBarString(GenericBar stringBar); + + void injectBarInteger(GenericBar stringBar); + } + + @Component(dependencies = NullComponent.class) + interface NullComponentWithDependency { + NullFoo nullFoo(); + + String nonNullableString(); + + @TypeUse.Nullable + Integer nullableInteger(); + } + + @Module + static class NullModule { + private final String string; + private final Integer integer; + + NullModule(String string, Integer integer) { + this.string = string; + this.integer = integer; + } + + @Provides + String provideNonNullableString() { + return string; + } + + @Provides + @TypeUse.Nullable + Integer provideNullableType() { + return integer; + } + + @Provides + GenericFoo<@TypeUse.Nullable Object> provideGenericFoo() { + return new GenericFoo<>(new Object()); + } + } + + /** + * Baseline test case demonstrating that all requested bindings would actually work with non-null + * bindings. + */ + @Test + public void nonNull() { + NullModule nonNullModule = new NullModule(EXPECTED_STRING, EXPECTED_INTEGER); + NullComponent component = + DaggerTypeUseNullabilityTest_NullComponent.builder().nullModule(nonNullModule).build(); + NullFoo nullFoo = component.nullFoo(); + + assertThat(component.nonNullableString()).isEqualTo(EXPECTED_STRING); + assertThat(component.nullableInteger()).isEqualTo(EXPECTED_INTEGER); + + assertThat(nullFoo.nullableInteger).isEqualTo(EXPECTED_INTEGER); + assertThat(nullFoo.nullableIntegerField).isEqualTo(EXPECTED_INTEGER); + assertThat(nullFoo.nullableMethodInjectedField).isEqualTo(EXPECTED_INTEGER); + } + + @Test + public void testNullability_moduleProvides() { + NullModule nullModule = new NullModule(/* string= */ null, /* integer= */ null); + NullComponent component = + DaggerTypeUseNullabilityTest_NullComponent.builder().nullModule(nullModule).build(); + + NullPointerException expected = + assertThrows(NullPointerException.class, component::nonNullableString); + assertThat(expected) + .hasMessageThat() + .isEqualTo("Cannot return null from a non-@Nullable @Provides method"); + + assertThat(component.nullableInteger()).isNull(); + } + + @Test + public void testNullability_typeInjection() { + NullModule nullModule = new NullModule(/* string= */ null, /* integer= */ null); + NullComponent component = + DaggerTypeUseNullabilityTest_NullComponent.builder().nullModule(nullModule).build(); + NullFoo nullFoo = component.nullFoo(); + + assertThat(nullFoo.nullableInteger).isNull(); + assertThat(nullFoo.nullableIntegerField).isNull(); + assertThat(nullFoo.nullableMethodInjectedField).isNull(); + } + + @Test + public void testNullability_componentOverride() { + NullComponent nullComponent = + new NullComponent() { + @Override + public NullFoo nullFoo() { + return null; + } + + @Override + public String nonNullableString() { + return null; + } + + @Override + public Integer nullableInteger() { + return null; + } + + @Override + public GenericFoo genericFoo() { + return new GenericFoo<>(null); + } + + @Override + public void injectBarString(GenericBar stringBar) { + stringBar.t = null; + } + + @Override + public void injectBarInteger(GenericBar<@TypeUse.Nullable Integer> intBar) { + intBar.t = null; + } + }; + NullComponentWithDependency component = + DaggerTypeUseNullabilityTest_NullComponentWithDependency.builder() + .nullComponent(nullComponent) + .build(); + + NullPointerException expected = + assertThrows(NullPointerException.class, component::nonNullableString); + assertThat(expected) + .hasMessageThat() + .isEqualTo("Cannot return null from a non-@Nullable component method"); + + assertThat(component.nullableInteger()).isNull(); + } +}