Skip to content

Commit 3f66b1e

Browse files
committed
Add Nullable annot and parse annots in parameter position
1 parent 07883c1 commit 3f66b1e

File tree

9 files changed

+163
-10
lines changed

9 files changed

+163
-10
lines changed

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,6 +1159,11 @@ class Definitions {
11591159
"io.reactivex.annotations.NonNull" ::
11601160
"org.jspecify.annotations.NonNull" :: Nil)
11611161

1162+
@tu lazy val NullableAnnots: List[ClassSymbol] = getClassesIfDefined(
1163+
"javax.annotation.Nullable" ::
1164+
"org.jetbrains.annotations.Nullable" ::
1165+
"org.jspecify.annotations.Nullable" :: Nil)
1166+
11621167
// convenient one-parameter method types
11631168
def methOfAny(tp: Type): MethodType = MethodType(List(AnyType), tp)
11641169
def methOfAnyVal(tp: Type): MethodType = MethodType(List(AnyValType), tp)

compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,22 @@ object ImplicitNullInterop:
7676
val skipResultType = sym.isConstructor || hasNotNullAnnot(sym)
7777
// Don't nullify Given/implicit parameters
7878
val skipCurrentLevel = sym.isOneOf(GivenOrImplicitVal)
79+
// Use OrNull instead of flexible types if symbol is explicitly nullable
80+
val explicitlyNullable = hasNullableAnnot(sym)
7981

8082
val map = new ImplicitNullMap(
8183
javaDefined = sym.is(JavaDefined),
8284
skipResultType = skipResultType,
83-
skipCurrentLevel = skipCurrentLevel)
85+
skipCurrentLevel = skipCurrentLevel,
86+
explicitlyNullable = explicitlyNullable)
8487
map(tp)
8588

8689
private def hasNotNullAnnot(sym: Symbol)(using Context): Boolean =
8790
ctx.definitions.NotNullAnnots.exists(nna => sym.unforcedAnnotation(nna).isDefined)
8891

92+
private def hasNullableAnnot(sym: Symbol)(using Context): Boolean =
93+
ctx.definitions.NullableAnnots.exists(nna => sym.unforcedAnnotation(nna).isDefined)
94+
8995
/** A type map that implements the nullification function on types. Given a Java-sourced type or a type
9096
* coming from Scala code compiled without explicit nulls, this adds `| Null` or `FlexibleType` in the
9197
* right places to make nullability explicit in a conservative way (without forcing incomplete symbols).
@@ -98,18 +104,23 @@ object ImplicitNullInterop:
98104
private class ImplicitNullMap(
99105
val javaDefined: Boolean,
100106
var skipResultType: Boolean = false,
101-
var skipCurrentLevel: Boolean = false
107+
var skipCurrentLevel: Boolean = false,
108+
var explicitlyNullable: Boolean = false
102109
)(using Context) extends TypeMap:
103110

104-
def nullify(tp: Type): Type = if ctx.flexibleTypes then FlexibleType(tp) else OrNull(tp)
111+
def nullify(tp: Type): Type =
112+
if ctx.flexibleTypes && !explicitlyNullable then
113+
FlexibleType(tp)
114+
else
115+
OrNull(tp)
105116

106117
/** Should we nullify `tp` at the outermost level?
107118
* The symbols are still under construction, so we don't have precise information.
108119
* We purposely do not rely on precise subtyping checks here (e.g., asking whether `tp <:< AnyRef`),
109120
* because doing so could force incomplete symbols or trigger cycles. Instead, we conservatively
110121
* nullify only when we can recognize a concrete reference type or type parameters from Java.
111122
*/
112-
def needsNull(tp: Type): Boolean =
123+
def needsNull(tp: Type): Boolean = trace(i"needsNull ${tp}"):
113124
if skipCurrentLevel || !tp.hasSimpleKind then false
114125
else tp.dealias match
115126
case tp: TypeRef =>
@@ -140,30 +151,36 @@ object ImplicitNullInterop:
140151
case tp: TypeRef if defn.isTupleClass(tp.symbol) => false
141152
case _ => true
142153

143-
override def apply(tp: Type): Type = tp match
154+
override def apply(tp: Type): Type = trace(i"apply $tp"){ tp match
144155
case tp: TypeRef if needsNull(tp) =>
145156
nullify(tp)
146157
case tp: TypeParamRef if needsNull(tp) =>
147158
nullify(tp)
148159
case appTp @ AppliedType(tycon, targs) =>
149160
val savedSkipCurrentLevel = skipCurrentLevel
161+
val savedExplicitlyNullable = explicitlyNullable
150162

151163
// If Java-defined tycon, don't nullify outer level of type args (Java classes are fully nullified)
152164
skipCurrentLevel = tp.classSymbol.is(JavaDefined)
165+
explicitlyNullable = false
153166
val targs2 = targs.map(this)
154167

155168
skipCurrentLevel = savedSkipCurrentLevel
169+
explicitlyNullable = savedExplicitlyNullable
156170
val appTp2 = derivedAppliedType(appTp, tycon, targs2)
157171
if tyconNeedsNull(tycon) && tp.hasSimpleKind then nullify(appTp2) else appTp2
158172
case ptp: PolyType =>
159173
derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType))
160174
case mtp: MethodType =>
161175
val savedSkipCurrentLevel = skipCurrentLevel
176+
val savedExplicitlyNullable = explicitlyNullable
162177

163178
// Don't nullify param types for implicit/using sections
164179
skipCurrentLevel = mtp.isImplicitMethod
180+
explicitlyNullable = false
165181
val paramInfos2 = mtp.paramInfos.map(this)
166182

183+
explicitlyNullable = savedExplicitlyNullable
167184
skipCurrentLevel = skipResultType
168185
val resType2 = this(mtp.resType)
169186

@@ -189,19 +206,38 @@ object ImplicitNullInterop:
189206
mapOver(tp)
190207
case tp: AnnotatedType =>
191208
// We don't nullify the annotation part.
192-
derivedAnnotatedType(tp, this(tp.underlying), tp.annot)
209+
val savedSkipResultType = skipResultType
210+
val savedSkipCurrentLevel = skipCurrentLevel
211+
val savedExplicitlyNullable = explicitlyNullable
212+
if (ctx.definitions.NullableAnnots.exists(ann => tp.hasAnnotation(ann))) {
213+
explicitlyNullable = true
214+
skipCurrentLevel = false
215+
}
216+
217+
if (ctx.definitions.NotNullAnnots.exists(ann => tp.hasAnnotation(ann))) {
218+
skipResultType = true
219+
skipCurrentLevel = false
220+
}
221+
val resType = this(tp.underlying)
222+
explicitlyNullable = savedExplicitlyNullable
223+
skipCurrentLevel = savedSkipCurrentLevel
224+
skipResultType = savedSkipResultType
225+
derivedAnnotatedType(tp, resType, tp.annot)
193226
case tp: RefinedType =>
194227
val savedSkipCurrentLevel = skipCurrentLevel
195228
val savedSkipResultType = skipResultType
229+
val savedExplicitlyNullable = explicitlyNullable
196230

197231
val parent2 = this(tp.parent)
198232

199233
skipCurrentLevel = false
200234
skipResultType = false
235+
explicitlyNullable = false
201236
val refinedInfo2 = this(tp.refinedInfo)
202237

203238
skipCurrentLevel = savedSkipCurrentLevel
204239
skipResultType = savedSkipResultType
240+
explicitlyNullable = savedExplicitlyNullable
205241

206242
parent2 match
207243
case FlexibleType(_, parent2a) if ctx.flexibleTypes =>
@@ -217,5 +253,6 @@ object ImplicitNullInterop:
217253
// complex computed types such as match types here; those remain as-is to avoid forcing
218254
// incomplete information during symbol construction.
219255
tp
256+
}
220257
end apply
221258
end ImplicitNullMap

compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,8 @@ object JavaParsers {
287287
}
288288

289289
def typ(): Tree =
290-
annotations()
291-
optArrayBrackets {
290+
val annots = annotations()
291+
val tp = optArrayBrackets {
292292
if (in.token == FINAL) in.nextToken()
293293
if (in.token == IDENTIFIER) {
294294
var t = typeArgs(atSpan(in.offset)(Ident(ident())))
@@ -308,6 +308,7 @@ object JavaParsers {
308308
else
309309
basicType()
310310
}
311+
annots.foldLeft(tp)((tp, ann) => Annotated(tp, ann))
311312

312313
def typeArgs(t: Tree): Tree = {
313314
var wildnum = 0
@@ -554,7 +555,7 @@ object JavaParsers {
554555
def formalParam(): ValDef = {
555556
val start = in.offset
556557
if (in.token == FINAL) in.nextToken()
557-
annotations()
558+
val annots = annotations()
558559
var t = typ()
559560
if (in.token == DOTDOTDOT) {
560561
in.nextToken()
@@ -563,7 +564,7 @@ object JavaParsers {
563564
}
564565
}
565566
atSpan(start, in.offset) {
566-
varDecl(Modifiers(Flags.JavaDefined | Flags.Param), t, ident().toTermName)
567+
varDecl(Modifiers(Flags.JavaDefined | Flags.Param, annotations = annots), t, ident().toTermName)
567568
}
568569
}
569570

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package javax.annotation;
2+
import java.util.*;
3+
4+
public class J {
5+
6+
private static String getK() {
7+
return "k";
8+
}
9+
10+
@Nullable
11+
public static final String k = getK();
12+
13+
@Nullable
14+
public static String l = "l";
15+
16+
@Nullable
17+
public final String m = null;
18+
19+
@Nullable
20+
public String n = "n";
21+
22+
@Nullable
23+
public static final String f(int i) {
24+
return "f: " + i;
25+
}
26+
27+
@Nullable
28+
public static String g(int i) {
29+
return "g: " + i;
30+
}
31+
32+
@Nullable
33+
public String h(int i) {
34+
return "h: " + i;
35+
}
36+
37+
@Nullable
38+
public String q(String s) {
39+
return "h: " + s;
40+
}
41+
42+
@Nullable
43+
public <T> String[] genericf(T a) {
44+
String[] as = new String[1];
45+
as[0] = "" + a;
46+
return as;
47+
}
48+
49+
@Nullable
50+
public <T> List<T> genericg(T a) {
51+
List<T> as = new ArrayList<T>();
52+
as.add(a);
53+
return as;
54+
}
55+
56+
public List<@Nullable String> listS(String s) {
57+
List<String> as = new ArrayList<String>();
58+
as.add(null);
59+
return as;
60+
}
61+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package javax.annotation;
2+
3+
import java.lang.annotation.*;
4+
5+
// A "fake" Nullable Annotation for jsr305
6+
@Retention(value = RetentionPolicy.RUNTIME)
7+
@Target(value = ElementType.TYPE_USE)
8+
@interface Nullable {
9+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Test that Nullable annotations are working in Java files.
2+
3+
import javax.annotation.J
4+
5+
class S_3 {
6+
def kk: String = J.k // error
7+
def ll: String = J.l // error
8+
def mm: String = (new J).m // error
9+
def nn: String = (new J).n // error
10+
def ff(i: Int): String = J.f(i) // error
11+
def gg(i: Int): String = J.g(i) // error
12+
def hh(i: Int): String = (new J).h(i) // error
13+
def qq(s: String): String | Null = (new J).q(s)
14+
def genericff(a: String): Array[String] = (new J).genericf(a) // error
15+
def genericgg(a: String): java.util.List[String] = (new J).genericg(a) // error
16+
def LList(s: String): java.util.List[String] = (new J).listS("") // error
17+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package javax.annotation;
2+
import java.util.*;
3+
4+
public class J {
5+
public String p(@Nullable String nullableString) {
6+
return nullableString;
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package javax.annotation;
2+
3+
import java.lang.annotation.*;
4+
5+
// A "fake" Nullable Annotation for jsr305
6+
@Retention(value = RetentionPolicy.RUNTIME)
7+
@interface Nullable {
8+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Test that Nullable annotations are working in Java files.
2+
3+
import javax.annotation.J
4+
5+
class S extends J {
6+
override def p(s: String): String = ??? // error
7+
}

0 commit comments

Comments
 (0)