diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 0195a4ddbf34..57fdfa010847 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1125,6 +1125,11 @@ class Definitions { "reactor.util.annotation.NonNullApi" :: "io.reactivex.annotations.NonNull" :: Nil) + @tu lazy val NullableAnnots: List[ClassSymbol] = getClassesIfDefined( + "javax.annotation.Nullable" :: + "org.jetbrains.annotations.Nullable" :: + "org.jspecify.annotations.Nullable" :: Nil) + // convenient one-parameter method types def methOfAny(tp: Type): MethodType = MethodType(List(AnyType), tp) def methOfAnyVal(tp: Type): MethodType = MethodType(List(AnyValType), tp) diff --git a/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala b/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala index 46ce0d2d7852..09a3995a266f 100644 --- a/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala @@ -66,23 +66,26 @@ object JavaNullInterop { nullifyExceptReturnType(tp) else // Otherwise, nullify everything - nullifyType(tp) + nullifyType(tp, explicitlyNullable = hasNullableAnnot(sym)) } private def hasNotNullAnnot(sym: Symbol)(using Context): Boolean = ctx.definitions.NotNullAnnots.exists(nna => sym.unforcedAnnotation(nna).isDefined) + private def hasNullableAnnot(sym: Symbol)(using Context): Boolean = + ctx.definitions.NullableAnnots.exists(nna => sym.unforcedAnnotation(nna).isDefined) + /** If tp is a MethodType, the parameters and the inside of return type are nullified, * but the result return type is not nullable. * If tp is a type of a field, the inside of the type is nullified, * but the result type is not nullable. */ private def nullifyExceptReturnType(tp: Type)(using Context): Type = - new JavaNullMap(outermostLevelAlreadyNullable = true)(tp) + new JavaNullMap(outermostLevelAlreadyNullable = true, explicitlyNullable = false)(tp) /** Nullifies a Java type by adding `| Null` in the relevant places. */ - private def nullifyType(tp: Type)(using Context): Type = - new JavaNullMap(outermostLevelAlreadyNullable = false)(tp) + private def nullifyType(tp: Type, explicitlyNullable: Boolean = false)(using Context): Type = + new JavaNullMap(outermostLevelAlreadyNullable = false, explicitlyNullable)(tp) /** A type map that implements the nullification function on types. Given a Java-sourced type, this adds `| Null` * in the right places to make the nulls explicit in Scala. @@ -95,8 +98,17 @@ object JavaNullInterop { * This is useful for e.g. constructors, and also so that `A & B` is nullified * to `(A & B) | Null`, instead of `(A | Null & B | Null) | Null`. */ - private class JavaNullMap(var outermostLevelAlreadyNullable: Boolean)(using Context) extends TypeMap { - def nullify(tp: Type): Type = if ctx.flexibleTypes then FlexibleType(tp) else OrNull(tp) + private class JavaNullMap(var outermostLevelAlreadyNullable: Boolean, explicitlyNullable: Boolean)(using Context) extends TypeMap { + def nullify(tp: Type): Type = + if ctx.flexibleTypes then { + if explicitlyNullable then { + OrNull(tp) + } else { + FlexibleType(tp) + } + } else { + OrNull(tp) + } /** Should we nullify `tp` at the outermost level? */ def needsNull(tp: Type): Boolean = diff --git a/tests/explicit-nulls/neg/i21629/J.java b/tests/explicit-nulls/neg/i21629/J.java new file mode 100644 index 000000000000..181bd1ae77ee --- /dev/null +++ b/tests/explicit-nulls/neg/i21629/J.java @@ -0,0 +1,50 @@ +package javax.annotation; +import java.util.*; + +public class J { + + private static String getK() { + return "k"; + } + + @Nullable + public static final String k = getK(); + + @Nullable + public static String l = "l"; + + @Nullable + public final String m = null; + + @Nullable + public String n = "n"; + + @Nullable + public static final String f(int i) { + return "f: " + i; + } + + @Nullable + public static String g(int i) { + return "g: " + i; + } + + @Nullable + public String h(int i) { + return "h: " + i; + } + + @Nullable + public String[] genericf(T a) { + String[] as = new String[1]; + as[0] = "" + a; + return as; + } + + @Nullable + public List genericg(T a) { + List as = new ArrayList(); + as.add(a); + return as; + } +} diff --git a/tests/explicit-nulls/neg/i21629/Nullable.java b/tests/explicit-nulls/neg/i21629/Nullable.java new file mode 100644 index 000000000000..53878991dd51 --- /dev/null +++ b/tests/explicit-nulls/neg/i21629/Nullable.java @@ -0,0 +1,8 @@ +package javax.annotation; + +import java.lang.annotation.*; + +// A "fake" Nullable Annotation for jsr305 +@Retention(value = RetentionPolicy.RUNTIME) +@interface Nullable { +} diff --git a/tests/explicit-nulls/neg/i21629/S.scala b/tests/explicit-nulls/neg/i21629/S.scala new file mode 100644 index 000000000000..8ef4aa311e21 --- /dev/null +++ b/tests/explicit-nulls/neg/i21629/S.scala @@ -0,0 +1,15 @@ +// Test that Nullable annotations are working in Java files. + +import javax.annotation.J + +class S_3 { + def kk: String = J.k // error + def ll: String = J.l // error + def mm: String = (new J).m // error + def nn: String = (new J).n // error + def ff(i: Int): String = J.f(i) // error + def gg(i: Int): String = J.g(i) // error + def hh(i: Int): String = (new J).h(i) // error + def genericff(a: String | Null): Array[String | Null] = (new J).genericf(a) // error + def genericgg(a: String | Null): java.util.List[String] = (new J).genericg(a) // error +}