Skip to content

Commit 8101aeb

Browse files
authored
Merge pull request #7546 from abeln/dotty-explicit-nulls-typesystem
Dotty with explicit nulls (and flow typing)
2 parents 3130729 + 85c0855 commit 8101aeb

File tree

137 files changed

+3571
-187
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

137 files changed

+3571
-187
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,9 @@ object Contexts {
425425
def useColors: Boolean =
426426
base.settings.color.value == "always"
427427

428+
/** Is the explicit nulls option set? */
429+
def explicitNulls: Boolean = base.settings.YexplicitNulls.value
430+
428431
protected def init(outer: Context, origin: Context): this.type = {
429432
util.Stats.record("Context.fresh")
430433
_outer = outer

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

Lines changed: 83 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ package core
44

55
import scala.annotation.{threadUnsafe => tu}
66
import Types._, Contexts._, Symbols._, SymDenotations._, StdNames._, Names._
7-
import Flags._, Scopes._, Decorators._, NameOps._, Periods._
7+
import Flags._, Scopes._, Decorators._, NameOps._, Periods._, NullOpsDecorator._
88
import unpickleScala2.Scala2Unpickler.ensureConstructor
99
import scala.collection.mutable
1010
import collection.mutable
@@ -269,7 +269,7 @@ class Definitions {
269269
@tu lazy val Any_asInstanceOf: TermSymbol = enterT1ParameterlessMethod(AnyClass, nme.asInstanceOf_, _.paramRefs(0), Final)
270270
@tu lazy val Any_typeTest: TermSymbol = enterT1ParameterlessMethod(AnyClass, nme.isInstanceOfPM, _ => BooleanType, Final | Synthetic | Artifact)
271271
@tu lazy val Any_typeCast: TermSymbol = enterT1ParameterlessMethod(AnyClass, nme.asInstanceOfPM, _.paramRefs(0), Final | Synthetic | Artifact | StableRealizable)
272-
// generated by pattern matcher, eliminated by erasure
272+
// generated by pattern matcher and explicit nulls, eliminated by erasure
273273

274274
/** def getClass[A >: this.type](): Class[? <: A] */
275275
@tu lazy val Any_getClass: TermSymbol =
@@ -347,11 +347,29 @@ class Definitions {
347347
ScalaPackageClass, tpnme.Nothing, AbstractFinal, List(AnyClass.typeRef))
348348
def NothingType: TypeRef = NothingClass.typeRef
349349
@tu lazy val RuntimeNothingModuleRef: TermRef = ctx.requiredModuleRef("scala.runtime.Nothing")
350-
@tu lazy val NullClass: ClassSymbol = enterCompleteClassSymbol(
351-
ScalaPackageClass, tpnme.Null, AbstractFinal, List(ObjectClass.typeRef))
350+
@tu lazy val NullClass: ClassSymbol = {
351+
val parent = if (ctx.explicitNulls) AnyType else ObjectType
352+
enterCompleteClassSymbol(ScalaPackageClass, tpnme.Null, AbstractFinal, parent :: Nil)
353+
}
352354
def NullType: TypeRef = NullClass.typeRef
353355
@tu lazy val RuntimeNullModuleRef: TermRef = ctx.requiredModuleRef("scala.runtime.Null")
354356

357+
/** An alias for null values that originate in Java code.
358+
* This type gets special treatment in the Typer. Specifically, `JavaNull` can be selected through:
359+
* e.g.
360+
* ```
361+
* // x: String|Null
362+
* x.length // error: `Null` has no `length` field
363+
* // x2: String|JavaNull
364+
* x2.length // allowed by the Typer, but unsound (might throw NPE)
365+
* ```
366+
*/
367+
lazy val JavaNullAlias: TypeSymbol = {
368+
assert(ctx.explicitNulls)
369+
enterAliasType(tpnme.JavaNull, NullType)
370+
}
371+
def JavaNullAliasType: TypeRef = JavaNullAlias.typeRef
372+
355373
@tu lazy val ImplicitScrutineeTypeSym =
356374
newSymbol(ScalaPackageClass, tpnme.IMPLICITkw, EmptyFlags, TypeBounds.empty).entered
357375
def ImplicitScrutineeTypeRef: TypeRef = ImplicitScrutineeTypeSym.typeRef
@@ -441,12 +459,12 @@ class Definitions {
441459
@tu lazy val Boolean_|| : Symbol = BooleanClass.requiredMethod(nme.ZOR)
442460
@tu lazy val Boolean_== : Symbol =
443461
BooleanClass.info.member(nme.EQ).suchThat(_.info.firstParamTypes match {
444-
case List(pt) => (pt isRef BooleanClass)
462+
case List(pt) => pt.isRef(BooleanClass)
445463
case _ => false
446464
}).symbol
447465
@tu lazy val Boolean_!= : Symbol =
448466
BooleanClass.info.member(nme.NE).suchThat(_.info.firstParamTypes match {
449-
case List(pt) => (pt isRef BooleanClass)
467+
case List(pt) => pt.isRef(BooleanClass)
450468
case _ => false
451469
}).symbol
452470

@@ -509,7 +527,7 @@ class Definitions {
509527
@tu lazy val StringModule: Symbol = StringClass.linkedClass
510528
@tu lazy val String_+ : TermSymbol = enterMethod(StringClass, nme.raw.PLUS, methOfAny(StringType), Final)
511529
@tu lazy val String_valueOf_Object: Symbol = StringModule.info.member(nme.valueOf).suchThat(_.info.firstParamTypes match {
512-
case List(pt) => (pt isRef AnyClass) || (pt isRef ObjectClass)
530+
case List(pt) => pt.isRef(AnyClass) || pt.isRef(ObjectClass)
513531
case _ => false
514532
}).symbol
515533

@@ -520,12 +538,16 @@ class Definitions {
520538
@tu lazy val BoxedNumberClass: ClassSymbol = ctx.requiredClass("java.lang.Number")
521539
@tu lazy val ClassCastExceptionClass: ClassSymbol = ctx.requiredClass("java.lang.ClassCastException")
522540
@tu lazy val ClassCastExceptionClass_stringConstructor: TermSymbol = ClassCastExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match {
523-
case List(pt) => (pt isRef StringClass)
541+
case List(pt) =>
542+
val pt1 = if (ctx.explicitNulls) pt.stripNull() else pt
543+
pt1.isRef(StringClass)
524544
case _ => false
525545
}).symbol.asTerm
526546
@tu lazy val ArithmeticExceptionClass: ClassSymbol = ctx.requiredClass("java.lang.ArithmeticException")
527547
@tu lazy val ArithmeticExceptionClass_stringConstructor: TermSymbol = ArithmeticExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match {
528-
case List(pt) => (pt isRef StringClass)
548+
case List(pt) =>
549+
val pt1 = if (ctx.explicitNulls) pt.stripNull() else pt
550+
pt1.isRef(StringClass)
529551
case _ => false
530552
}).symbol.asTerm
531553

@@ -793,6 +815,31 @@ class Definitions {
793815
@tu lazy val InfixAnnot: ClassSymbol = ctx.requiredClass("scala.annotation.infix")
794816
@tu lazy val AlphaAnnot: ClassSymbol = ctx.requiredClass("scala.annotation.alpha")
795817

818+
// A list of annotations that are commonly used to indicate that a field/method argument or return
819+
// type is not null. These annotations are used by the nullification logic in JavaNullInterop to
820+
// improve the precision of type nullification.
821+
// We don't require that any of these annotations be present in the class path, but we want to
822+
// create Symbols for the ones that are present, so they can be checked during nullification.
823+
@tu lazy val NotNullAnnots: List[ClassSymbol] = ctx.getClassesIfDefined(
824+
"javax.annotation.Nonnull" ::
825+
"javax.validation.constraints.NotNull" ::
826+
"androidx.annotation.NonNull" ::
827+
"android.support.annotation.NonNull" ::
828+
"android.annotation.NonNull" ::
829+
"com.android.annotations.NonNull" ::
830+
"org.eclipse.jdt.annotation.NonNull" ::
831+
"edu.umd.cs.findbugs.annotations.NonNull" ::
832+
"org.checkerframework.checker.nullness.qual.NonNull" ::
833+
"org.checkerframework.checker.nullness.compatqual.NonNullDecl" ::
834+
"org.jetbrains.annotations.NotNull" ::
835+
"org.springframework.lang.NonNull" ::
836+
"org.springframework.lang.NonNullApi" ::
837+
"org.springframework.lang.NonNullFields" ::
838+
"lombok.NonNull" ::
839+
"reactor.util.annotation.NonNull" ::
840+
"reactor.util.annotation.NonNullApi" ::
841+
"io.reactivex.annotations.NonNull" :: Nil map PreNamedString)
842+
796843
// convenient one-parameter method types
797844
def methOfAny(tp: Type): MethodType = MethodType(List(AnyType), tp)
798845
def methOfAnyVal(tp: Type): MethodType = MethodType(List(AnyValType), tp)
@@ -845,7 +892,7 @@ class Definitions {
845892
if (ctx.erasedTypes) JavaArrayType(elem)
846893
else ArrayType.appliedTo(elem :: Nil)
847894
def unapply(tp: Type)(implicit ctx: Context): Option[Type] = tp.dealias match {
848-
case AppliedType(at, arg :: Nil) if at isRef ArrayType.symbol => Some(arg)
895+
case AppliedType(at, arg :: Nil) if at.isRef(ArrayType.symbol) => Some(arg)
849896
case _ => None
850897
}
851898
}
@@ -957,8 +1004,16 @@ class Definitions {
9571004
name.drop(prefix.length).forall(_.isDigit))
9581005

9591006
def isBottomClass(cls: Symbol): Boolean =
960-
cls == NothingClass || cls == NullClass
1007+
if (ctx.explicitNulls && !ctx.phase.erasedTypes) cls == NothingClass
1008+
else isBottomClassAfterErasure(cls)
1009+
1010+
def isBottomClassAfterErasure(cls: Symbol): Boolean = cls == NothingClass || cls == NullClass
1011+
9611012
def isBottomType(tp: Type): Boolean =
1013+
if (ctx.explicitNulls && !ctx.phase.erasedTypes) tp.derivesFrom(NothingClass)
1014+
else isBottomTypeAfterErasure(tp)
1015+
1016+
def isBottomTypeAfterErasure(tp: Type): Boolean =
9621017
tp.derivesFrom(NothingClass) || tp.derivesFrom(NullClass)
9631018

9641019
/** Is a function class.
@@ -1089,7 +1144,7 @@ class Definitions {
10891144

10901145
def isTupleType(tp: Type)(implicit ctx: Context): Boolean = {
10911146
val arity = tp.dealias.argInfos.length
1092-
arity <= MaxTupleArity && TupleType(arity) != null && (tp isRef TupleType(arity).symbol)
1147+
arity <= MaxTupleArity && TupleType(arity) != null && tp.isRef(TupleType(arity).symbol)
10931148
}
10941149

10951150
def tupleType(elems: List[Type]): Type = {
@@ -1302,18 +1357,22 @@ class Definitions {
13021357
// ----- Initialization ---------------------------------------------------
13031358

13041359
/** Lists core classes that don't have underlying bytecode, but are synthesized on-the-fly in every reflection universe */
1305-
@tu lazy val syntheticScalaClasses: List[TypeSymbol] = List(
1306-
AnyClass,
1307-
AnyRefAlias,
1308-
AnyKindClass,
1309-
andType,
1310-
orType,
1311-
RepeatedParamClass,
1312-
ByNameParamClass2x,
1313-
AnyValClass,
1314-
NullClass,
1315-
NothingClass,
1316-
SingletonClass)
1360+
@tu lazy val syntheticScalaClasses: List[TypeSymbol] = {
1361+
val synth = List(
1362+
AnyClass,
1363+
AnyRefAlias,
1364+
AnyKindClass,
1365+
andType,
1366+
orType,
1367+
RepeatedParamClass,
1368+
ByNameParamClass2x,
1369+
AnyValClass,
1370+
NullClass,
1371+
NothingClass,
1372+
SingletonClass)
1373+
1374+
if (ctx.explicitNulls) synth :+ JavaNullAlias else synth
1375+
}
13171376

13181377
@tu lazy val syntheticCoreClasses: List[Symbol] = syntheticScalaClasses ++ List(
13191378
EmptyPackageVal,

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,6 @@ object Flags {
452452
val AfterLoadFlags: FlagSet = commonFlags(
453453
FromStartFlags, AccessFlags, Final, AccessorOrSealed, LazyOrTrait, SelfName, JavaDefined)
454454

455-
456455
/** A value that's unstable unless complemented with a Stable flag */
457456
val UnstableValueFlags: FlagSet = Mutable | Method
458457

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package dotty.tools.dotc.core
2+
3+
import dotty.tools.dotc.core.Contexts.Context
4+
import dotty.tools.dotc.core.Flags.JavaDefined
5+
import dotty.tools.dotc.core.StdNames.{jnme, nme}
6+
import dotty.tools.dotc.core.Symbols._
7+
import dotty.tools.dotc.core.Types._
8+
import NullOpsDecorator._
9+
10+
/** This module defines methods to interpret types of Java symbols, which are implicitly nullable in Java,
11+
* as Scala types, which are explicitly nullable.
12+
*
13+
* The transformation is (conceptually) a function `n` that adheres to the following rules:
14+
* (1) n(T) = T|JavaNull if T is a reference type
15+
* (2) n(T) = T if T is a value type
16+
* (3) n(C[T]) = C[T]|JavaNull if C is Java-defined
17+
* (4) n(C[T]) = C[n(T)]|JavaNull if C is Scala-defined
18+
* (5) n(A|B) = n(A)|n(B)|JavaNull
19+
* (6) n(A&B) = n(A) & n(B)
20+
* (7) n((A1, ..., Am)R) = (n(A1), ..., n(Am))n(R) for a method with arguments (A1, ..., Am) and return type R
21+
* (8) n(T) = T otherwise
22+
*
23+
* Treatment of generics (rules 3 and 4):
24+
* - if `C` is Java-defined, then `n(C[T]) = C[T]|JavaNull`. That is, we don't recurse
25+
* on the type argument, and only add JavaNull on the outside. This is because
26+
* `C` itself will be nullified, and in particular so will be usages of `C`'s type argument within C's body.
27+
* e.g. calling `get` on a `java.util.List[String]` already returns `String|Null` and not `String`, so
28+
* we don't need to write `java.util.List[String|Null]`.
29+
* - if `C` is Scala-defined, however, then we want `n(C[T]) = C[n(T)]|JavaNull`. This is because
30+
* `C` won't be nullified, so we need to indicate that its type argument is nullable.
31+
*
32+
* Notice that since the transformation is only applied to types attached to Java symbols, it doesn't need
33+
* to handle the full spectrum of Scala types. Additionally, some kinds of symbols like constructors and
34+
* enum instances get special treatment.
35+
*/
36+
object JavaNullInterop {
37+
38+
/** Transforms the type `tp` of Java member `sym` to be explicitly nullable.
39+
* `tp` is needed because the type inside `sym` might not be set when this method is called.
40+
*
41+
* e.g. given a Java method
42+
* String foo(String arg) { return arg; }
43+
*
44+
* After calling `nullifyMember`, Scala will see the method as
45+
*
46+
* def foo(arg: String|JavaNull): String|JavaNull
47+
*
48+
* This nullability function uses `JavaNull` instead of vanilla `Null`, for usability.
49+
* This means that we can select on the return of `foo`:
50+
*
51+
* val len = foo("hello").length
52+
*
53+
* But the selection can throw an NPE if the returned value is `null`.
54+
*/
55+
def nullifyMember(sym: Symbol, tp: Type, isEnumValueDef: Boolean)(implicit ctx: Context): Type = {
56+
assert(ctx.explicitNulls)
57+
assert(sym.is(JavaDefined), "can only nullify java-defined members")
58+
59+
// Some special cases when nullifying the type
60+
if (isEnumValueDef || sym.name == nme.TYPE_)
61+
// Don't nullify the `TYPE` field in every class and Java enum instances
62+
tp
63+
else if (sym.name == nme.toString_ || sym.isConstructor || hasNotNullAnnot(sym))
64+
// Don't nullify the return type of the `toString` method.
65+
// Don't nullify the return type of constructors.
66+
// Don't nullify the return type of methods with a not-null annotation.
67+
nullifyExceptReturnType(tp)
68+
else
69+
// Otherwise, nullify everything
70+
nullifyType(tp)
71+
}
72+
73+
private def hasNotNullAnnot(sym: Symbol)(implicit ctx: Context): Boolean =
74+
ctx.definitions.NotNullAnnots.exists(nna => sym.unforcedAnnotation(nna).isDefined)
75+
76+
/** If tp is a MethodType, the parameters and the inside of return type are nullified,
77+
* but the result return type is not nullable.
78+
* If tp is a type of a field, the inside of the type is nullified,
79+
* but the result type is not nullable.
80+
*/
81+
private def nullifyExceptReturnType(tp: Type)(implicit ctx: Context): Type =
82+
new JavaNullMap(true)(ctx)(tp)
83+
84+
/** Nullifies a Java type by adding `| JavaNull` in the relevant places. */
85+
private def nullifyType(tp: Type)(implicit ctx: Context): Type =
86+
new JavaNullMap(false)(ctx)(tp)
87+
88+
/** A type map that implements the nullification function on types. Given a Java-sourced type, this adds `| JavaNull`
89+
* in the right places to make the nulls explicit in Scala.
90+
*
91+
* @param outermostLevelAlreadyNullable whether this type is already nullable at the outermost level.
92+
* For example, `Array[String]|JavaNull` is already nullable at the
93+
* outermost level, but `Array[String|JavaNull]` isn't.
94+
* If this parameter is set to true, then the types of fields, and the return
95+
* types of methods will not be nullified.
96+
* This is useful for e.g. constructors, and also so that `A & B` is nullified
97+
* to `(A & B) | JavaNull`, instead of `(A|JavaNull & B|JavaNull) | JavaNull`.
98+
*/
99+
private class JavaNullMap(var outermostLevelAlreadyNullable: Boolean)(implicit ctx: Context) extends TypeMap {
100+
/** Should we nullify `tp` at the outermost level? */
101+
def needsNull(tp: Type): Boolean =
102+
!outermostLevelAlreadyNullable && (tp match {
103+
case tp: TypeRef =>
104+
// We don't modify value types because they're non-nullable even in Java.
105+
!tp.symbol.isValueClass &&
106+
// We don't modify `Any` because it's already nullable.
107+
!tp.isRef(defn.AnyClass) &&
108+
// We don't nullify Java varargs at the top level.
109+
// Example: if `setNames` is a Java method with signature `void setNames(String... names)`,
110+
// then its Scala signature will be `def setNames(names: (String|JavaNull)*): Unit`.
111+
// This is because `setNames(null)` passes as argument a single-element array containing the value `null`,
112+
// and not a `null` array.
113+
!tp.isRef(defn.RepeatedParamClass)
114+
case _ => true
115+
})
116+
117+
override def apply(tp: Type): Type = tp match {
118+
case tp: TypeRef if needsNull(tp) => OrJavaNull(tp)
119+
case appTp @ AppliedType(tycon, targs) =>
120+
val oldOutermostNullable = outermostLevelAlreadyNullable
121+
// We don't make the outmost levels of type arguements nullable if tycon is Java-defined.
122+
// This is because Java classes are _all_ nullified, so both `java.util.List[String]` and
123+
// `java.util.List[String|Null]` contain nullable elements.
124+
outermostLevelAlreadyNullable = tp.classSymbol.is(JavaDefined)
125+
val targs2 = targs map this
126+
outermostLevelAlreadyNullable = oldOutermostNullable
127+
val appTp2 = derivedAppliedType(appTp, tycon, targs2)
128+
if (needsNull(tycon)) OrJavaNull(appTp2) else appTp2
129+
case ptp: PolyType =>
130+
derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType))
131+
case mtp: MethodType =>
132+
val oldOutermostNullable = outermostLevelAlreadyNullable
133+
outermostLevelAlreadyNullable = false
134+
val paramInfos2 = mtp.paramInfos map this
135+
outermostLevelAlreadyNullable = oldOutermostNullable
136+
derivedLambdaType(mtp)(paramInfos2, this(mtp.resType))
137+
case tp: TypeAlias => mapOver(tp)
138+
case tp: AndType =>
139+
// nullify(A & B) = (nullify(A) & nullify(B)) | JavaNull, but take care not to add
140+
// duplicate `JavaNull`s at the outermost level inside `A` and `B`.
141+
outermostLevelAlreadyNullable = true
142+
OrJavaNull(derivedAndType(tp, this(tp.tp1), this(tp.tp2)))
143+
case tp: TypeParamRef if needsNull(tp) => OrJavaNull(tp)
144+
// In all other cases, return the type unchanged.
145+
// In particular, if the type is a ConstantType, then we don't nullify it because it is the
146+
// type of a final non-nullable field.
147+
case _ => tp
148+
}
149+
}
150+
}

0 commit comments

Comments
 (0)