diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 8d305eef16e1..feef3fe82a3b 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1159,6 +1159,11 @@ class Definitions { "io.reactivex.annotations.NonNull" :: "org.jspecify.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/ImplicitNullInterop.scala b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala index 0426665eaca1..8ac6234e11a0 100644 --- a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala @@ -76,16 +76,22 @@ object ImplicitNullInterop: val skipResultType = sym.isConstructor || hasNotNullAnnot(sym) // Don't nullify Given/implicit parameters val skipCurrentLevel = sym.isOneOf(GivenOrImplicitVal) + // Use OrNull instead of flexible types if symbol is explicitly nullable + val explicitlyNullable = hasNullableAnnot(sym) val map = new ImplicitNullMap( javaDefined = sym.is(JavaDefined), skipResultType = skipResultType, - skipCurrentLevel = skipCurrentLevel) + skipCurrentLevel = skipCurrentLevel, + explicitlyNullable = explicitlyNullable) map(tp) 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) + /** A type map that implements the nullification function on types. Given a Java-sourced type or a type * coming from Scala code compiled without explicit nulls, this adds `| Null` or `FlexibleType` in the * right places to make nullability explicit in a conservative way (without forcing incomplete symbols). @@ -98,10 +104,15 @@ object ImplicitNullInterop: private class ImplicitNullMap( val javaDefined: Boolean, var skipResultType: Boolean = false, - var skipCurrentLevel: Boolean = false + var skipCurrentLevel: Boolean = false, + var explicitlyNullable: Boolean = false )(using Context) extends TypeMap: - def nullify(tp: Type): Type = if ctx.flexibleTypes then FlexibleType(tp) else OrNull(tp) + def nullify(tp: Type): Type = + if ctx.flexibleTypes && !explicitlyNullable then + FlexibleType(tp) + else + OrNull(tp) /** Should we nullify `tp` at the outermost level? * The symbols are still under construction, so we don't have precise information. @@ -109,7 +120,7 @@ object ImplicitNullInterop: * because doing so could force incomplete symbols or trigger cycles. Instead, we conservatively * nullify only when we can recognize a concrete reference type or type parameters from Java. */ - def needsNull(tp: Type): Boolean = + def needsNull(tp: Type): Boolean = trace(i"needsNull ${tp}"): if skipCurrentLevel || !tp.hasSimpleKind then false else tp.dealias match case tp: TypeRef => @@ -140,30 +151,36 @@ object ImplicitNullInterop: case tp: TypeRef if defn.isTupleClass(tp.symbol) => false case _ => true - override def apply(tp: Type): Type = tp match + override def apply(tp: Type): Type = trace(i"apply $tp"){ tp match case tp: TypeRef if needsNull(tp) => nullify(tp) case tp: TypeParamRef if needsNull(tp) => nullify(tp) case appTp @ AppliedType(tycon, targs) => val savedSkipCurrentLevel = skipCurrentLevel + val savedExplicitlyNullable = explicitlyNullable // If Java-defined tycon, don't nullify outer level of type args (Java classes are fully nullified) skipCurrentLevel = tp.classSymbol.is(JavaDefined) + explicitlyNullable = false val targs2 = targs.map(this) skipCurrentLevel = savedSkipCurrentLevel + explicitlyNullable = savedExplicitlyNullable val appTp2 = derivedAppliedType(appTp, tycon, targs2) if tyconNeedsNull(tycon) && tp.hasSimpleKind then nullify(appTp2) else appTp2 case ptp: PolyType => derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType)) case mtp: MethodType => val savedSkipCurrentLevel = skipCurrentLevel + val savedExplicitlyNullable = explicitlyNullable // Don't nullify param types for implicit/using sections skipCurrentLevel = mtp.isImplicitMethod + explicitlyNullable = false val paramInfos2 = mtp.paramInfos.map(this) + explicitlyNullable = savedExplicitlyNullable skipCurrentLevel = skipResultType val resType2 = this(mtp.resType) @@ -189,19 +206,38 @@ object ImplicitNullInterop: mapOver(tp) case tp: AnnotatedType => // We don't nullify the annotation part. - derivedAnnotatedType(tp, this(tp.underlying), tp.annot) + val savedSkipResultType = skipResultType + val savedSkipCurrentLevel = skipCurrentLevel + val savedExplicitlyNullable = explicitlyNullable + if (ctx.definitions.NullableAnnots.exists(ann => tp.hasAnnotation(ann))) { + explicitlyNullable = true + skipCurrentLevel = false + } + + if (ctx.definitions.NotNullAnnots.exists(ann => tp.hasAnnotation(ann))) { + skipResultType = true + skipCurrentLevel = false + } + val resType = this(tp.underlying) + explicitlyNullable = savedExplicitlyNullable + skipCurrentLevel = savedSkipCurrentLevel + skipResultType = savedSkipResultType + derivedAnnotatedType(tp, resType, tp.annot) case tp: RefinedType => val savedSkipCurrentLevel = skipCurrentLevel val savedSkipResultType = skipResultType + val savedExplicitlyNullable = explicitlyNullable val parent2 = this(tp.parent) skipCurrentLevel = false skipResultType = false + explicitlyNullable = false val refinedInfo2 = this(tp.refinedInfo) skipCurrentLevel = savedSkipCurrentLevel skipResultType = savedSkipResultType + explicitlyNullable = savedExplicitlyNullable parent2 match case FlexibleType(_, parent2a) if ctx.flexibleTypes => @@ -217,5 +253,6 @@ object ImplicitNullInterop: // complex computed types such as match types here; those remain as-is to avoid forcing // incomplete information during symbol construction. tp + } end apply end ImplicitNullMap \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala index c15e6c93bf90..1cce5928034c 100644 --- a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala @@ -287,8 +287,8 @@ object JavaParsers { } def typ(): Tree = - annotations() - optArrayBrackets { + val annots = annotations() + val tp = optArrayBrackets { if (in.token == FINAL) in.nextToken() if (in.token == IDENTIFIER) { var t = typeArgs(atSpan(in.offset)(Ident(ident()))) @@ -308,6 +308,7 @@ object JavaParsers { else basicType() } + annots.foldLeft(tp)((tp, ann) => Annotated(tp, ann)) def typeArgs(t: Tree): Tree = { var wildnum = 0 @@ -554,7 +555,7 @@ object JavaParsers { def formalParam(): ValDef = { val start = in.offset if (in.token == FINAL) in.nextToken() - annotations() + val annots = annotations() var t = typ() if (in.token == DOTDOTDOT) { in.nextToken() @@ -563,7 +564,7 @@ object JavaParsers { } } atSpan(start, in.offset) { - varDecl(Modifiers(Flags.JavaDefined | Flags.Param), t, ident().toTermName) + varDecl(Modifiers(Flags.JavaDefined | Flags.Param, annotations = annots), t, ident().toTermName) } } diff --git a/tests/explicit-nulls/neg/i21629/J.java b/tests/explicit-nulls/neg/i21629/J.java new file mode 100644 index 000000000000..305c9d85d424 --- /dev/null +++ b/tests/explicit-nulls/neg/i21629/J.java @@ -0,0 +1,61 @@ +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 q(String s) { + return "h: " + s; + } + + @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; + } + + public List<@Nullable String> listS(String s) { + List as = new ArrayList(); + as.add(null); + 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..846b34798968 --- /dev/null +++ b/tests/explicit-nulls/neg/i21629/Nullable.java @@ -0,0 +1,9 @@ +package javax.annotation; + +import java.lang.annotation.*; + +// A "fake" Nullable Annotation for jsr305 +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = ElementType.TYPE_USE) +@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..2f245d6794b8 --- /dev/null +++ b/tests/explicit-nulls/neg/i21629/S.scala @@ -0,0 +1,17 @@ +// 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 qq(s: String): String | Null = (new J).q(s) + def genericff(a: String): Array[String] = (new J).genericf(a) // error + def genericgg(a: String): java.util.List[String] = (new J).genericg(a) // error + def LList(s: String): java.util.List[String] = (new J).listS("") // error +} diff --git a/tests/explicit-nulls/neg/i21629_override/J.java b/tests/explicit-nulls/neg/i21629_override/J.java new file mode 100644 index 000000000000..c8a9204ad7e9 --- /dev/null +++ b/tests/explicit-nulls/neg/i21629_override/J.java @@ -0,0 +1,8 @@ +package javax.annotation; +import java.util.*; + +public class J { + public String p(@Nullable String nullableString) { + return nullableString; + } +} diff --git a/tests/explicit-nulls/neg/i21629_override/Nullable.java b/tests/explicit-nulls/neg/i21629_override/Nullable.java new file mode 100644 index 000000000000..53878991dd51 --- /dev/null +++ b/tests/explicit-nulls/neg/i21629_override/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_override/S.scala b/tests/explicit-nulls/neg/i21629_override/S.scala new file mode 100644 index 000000000000..a4d295398175 --- /dev/null +++ b/tests/explicit-nulls/neg/i21629_override/S.scala @@ -0,0 +1,7 @@ +// Test that Nullable annotations are working in Java files. + +import javax.annotation.J + +class S extends J { + override def p(s: String): String = ??? // error +}