Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
49 changes: 43 additions & 6 deletions compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -98,18 +104,23 @@ 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.
* We purposely do not rely on precise subtyping checks here (e.g., asking whether `tp <:< AnyRef`),
* 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 =>
Expand Down Expand Up @@ -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)

Expand All @@ -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 =>
Expand All @@ -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
9 changes: 5 additions & 4 deletions compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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())))
Expand All @@ -308,6 +308,7 @@ object JavaParsers {
else
basicType()
}
annots.foldLeft(tp)((tp, ann) => Annotated(tp, ann))

def typeArgs(t: Tree): Tree = {
var wildnum = 0
Expand Down Expand Up @@ -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()
Expand All @@ -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)
}
}

Expand Down
61 changes: 61 additions & 0 deletions tests/explicit-nulls/neg/i21629/J.java
Original file line number Diff line number Diff line change
@@ -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 <T> String[] genericf(T a) {
String[] as = new String[1];
as[0] = "" + a;
return as;
}

@Nullable
public <T> List<T> genericg(T a) {
List<T> as = new ArrayList<T>();
as.add(a);
return as;
}

public List<@Nullable String> listS(String s) {
List<String> as = new ArrayList<String>();
as.add(null);
return as;
}
}
9 changes: 9 additions & 0 deletions tests/explicit-nulls/neg/i21629/Nullable.java
Original file line number Diff line number Diff line change
@@ -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 {
}
17 changes: 17 additions & 0 deletions tests/explicit-nulls/neg/i21629/S.scala
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 8 additions & 0 deletions tests/explicit-nulls/neg/i21629_override/J.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package javax.annotation;
import java.util.*;

public class J {
public String p(@Nullable String nullableString) {
return nullableString;
}
}
8 changes: 8 additions & 0 deletions tests/explicit-nulls/neg/i21629_override/Nullable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package javax.annotation;

import java.lang.annotation.*;

// A "fake" Nullable Annotation for jsr305
@Retention(value = RetentionPolicy.RUNTIME)
@interface Nullable {
}
7 changes: 7 additions & 0 deletions tests/explicit-nulls/neg/i21629_override/S.scala
Original file line number Diff line number Diff line change
@@ -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
}
Loading