Skip to content

Commit d59a792

Browse files
authored
Merge pull request scala#43 from noti0na1/dotty-explicit-nulls-only
Remove flow typing from main branch
2 parents a923e6a + b48643f commit d59a792

File tree

105 files changed

+2154
-84
lines changed

Some content is hidden

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

105 files changed

+2154
-84
lines changed

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ class ScalaSettings extends Settings.SettingGroup {
162162

163163
// Extremely experimental language features
164164
val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting("-Yno-kind-polymorphism", "Enable kind polymorphism (see https://dotty.epfl.ch/docs/reference/kind-polymorphism.html). Potentially unsound.")
165+
val YexplicitNulls: Setting[Boolean] = BooleanSetting("-Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.")
165166

166167
/** Area-specific debug output */
167168
val YexplainLowlevel: Setting[Boolean] = BooleanSetting("-Yexplain-lowlevel", "When explaining type errors, show types at a lower level.")

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

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

424+
/** Is the explicit nulls option set? */
425+
def explicitNulls: Boolean = base.settings.YexplicitNulls.value
426+
424427
protected def init(outer: Context, origin: Context): this.type = {
425428
util.Stats.record("Context.fresh")
426429
_outer = outer

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

Lines changed: 82 additions & 21 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
@@ -295,8 +295,16 @@ class Definitions {
295295
@tu lazy val AnyRefAlias: TypeSymbol = enterAliasType(tpnme.AnyRef, ObjectType)
296296
def AnyRefType: TypeRef = AnyRefAlias.typeRef
297297

298-
@tu lazy val Object_eq: TermSymbol = enterMethod(ObjectClass, nme.eq, methOfAnyRef(BooleanType), Final)
299-
@tu lazy val Object_ne: TermSymbol = enterMethod(ObjectClass, nme.ne, methOfAnyRef(BooleanType), Final)
298+
@tu lazy val Object_eq: TermSymbol = {
299+
// If explicit nulls is enabled, then we want to allow `(x: String).eq(null)`, so we need
300+
// to adjust the signature of `eq` accordingly.
301+
enterMethod(ObjectClass, nme.eq, methOfAnyRefOrNull(BooleanType), Final)
302+
}
303+
@tu lazy val Object_ne: TermSymbol = {
304+
// If explicit nulls is enabled, then we want to allow `(x: String).ne(null)`, so we need
305+
// to adjust the signature of `ne` accordingly.
306+
enterMethod(ObjectClass, nme.ne, methOfAnyRefOrNull(BooleanType), Final)
307+
}
300308
@tu lazy val Object_synchronized: TermSymbol = enterPolyMethod(ObjectClass, nme.synchronized_, 1,
301309
pt => MethodType(List(pt.paramRefs(0)), pt.paramRefs(0)), Final)
302310
@tu lazy val Object_clone: TermSymbol = enterMethod(ObjectClass, nme.clone_, MethodType(Nil, ObjectType), Protected)
@@ -336,11 +344,29 @@ class Definitions {
336344
ScalaPackageClass, tpnme.Nothing, AbstractFinal, List(AnyClass.typeRef))
337345
def NothingType: TypeRef = NothingClass.typeRef
338346
@tu lazy val RuntimeNothingModuleRef: TermRef = ctx.requiredModuleRef("scala.runtime.Nothing")
339-
@tu lazy val NullClass: ClassSymbol = enterCompleteClassSymbol(
340-
ScalaPackageClass, tpnme.Null, AbstractFinal, List(ObjectClass.typeRef))
347+
@tu lazy val NullClass: ClassSymbol = {
348+
val parent = if (ctx.explicitNulls) AnyType else ObjectType
349+
enterCompleteClassSymbol(ScalaPackageClass, tpnme.Null, AbstractFinal, parent :: Nil)
350+
}
341351
def NullType: TypeRef = NullClass.typeRef
342352
@tu lazy val RuntimeNullModuleRef: TermRef = ctx.requiredModuleRef("scala.runtime.Null")
343353

354+
/** An alias for null values that originate in Java code.
355+
* This type gets special treatment in the Typer. Specifically, `JavaNull` can be selected through:
356+
* e.g.
357+
* ```
358+
* // x: String|Null
359+
* x.length // error: `Null` has no `length` field
360+
* // x2: String|JavaNull
361+
* x2.length // allowed by the Typer, but unsound (might throw NPE)
362+
* ```
363+
*/
364+
lazy val JavaNullAlias: TypeSymbol = {
365+
assert(ctx.explicitNulls)
366+
enterAliasType(tpnme.JavaNull, NullType)
367+
}
368+
def JavaNullAliasType: TypeRef = JavaNullAlias.typeRef
369+
344370
@tu lazy val ImplicitScrutineeTypeSym =
345371
newSymbol(ScalaPackageClass, tpnme.IMPLICITkw, EmptyFlags, TypeBounds.empty).entered
346372
def ImplicitScrutineeTypeRef: TypeRef = ImplicitScrutineeTypeSym.typeRef
@@ -508,12 +534,16 @@ class Definitions {
508534
@tu lazy val BoxedNumberClass: ClassSymbol = ctx.requiredClass("java.lang.Number")
509535
@tu lazy val ClassCastExceptionClass: ClassSymbol = ctx.requiredClass("java.lang.ClassCastException")
510536
@tu lazy val ClassCastExceptionClass_stringConstructor: TermSymbol = ClassCastExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match {
511-
case List(pt) => (pt isRef StringClass)
537+
case List(pt) =>
538+
val pt1 = if (ctx.explicitNulls) pt.stripNull else pt
539+
pt1 isRef StringClass
512540
case _ => false
513541
}).symbol.asTerm
514542
@tu lazy val ArithmeticExceptionClass: ClassSymbol = ctx.requiredClass("java.lang.ArithmeticException")
515543
@tu lazy val ArithmeticExceptionClass_stringConstructor: TermSymbol = ArithmeticExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match {
516-
case List(pt) => (pt isRef StringClass)
544+
case List(pt) =>
545+
val pt1 = if (ctx.explicitNulls) pt.stripNull else pt
546+
pt1 isRef StringClass
517547
case _ => false
518548
}).symbol.asTerm
519549

@@ -783,10 +813,29 @@ class Definitions {
783813
@tu lazy val InfixAnnot: ClassSymbol = ctx.requiredClass("scala.annotation.infix")
784814
@tu lazy val AlphaAnnot: ClassSymbol = ctx.requiredClass("scala.annotation.alpha")
785815

816+
// A list of annotations that are commonly used to indicate that a field/method argument or return
817+
// type is not null. These annotations are used by the nullification logic in JavaNullInterop to
818+
// improve the precision of type nullification.
819+
// We don't require that any of these annotations be present in the class path, but we want to
820+
// create Symbols for the ones that are present, so they can be checked during nullification.
821+
@tu lazy val NotNullAnnots: List[ClassSymbol] = ctx.getClassesIfDefined(
822+
"javax.annotation.Nonnull" ::
823+
"edu.umd.cs.findbugs.annotations.NonNull" ::
824+
"androidx.annotation.NonNull" ::
825+
"android.support.annotation.NonNull" ::
826+
"android.annotation.NonNull" ::
827+
"com.android.annotations.NonNull" ::
828+
"org.eclipse.jdt.annotation.NonNull" ::
829+
"org.checkerframework.checker.nullness.qual.NonNull" ::
830+
"org.checkerframework.checker.nullness.compatqual.NonNullDecl" ::
831+
"org.jetbrains.annotations.NotNull" ::
832+
"lombok.NonNull" ::
833+
"io.reactivex.annotations.NonNull" :: Nil map PreNamedString)
834+
786835
// convenient one-parameter method types
787836
def methOfAny(tp: Type): MethodType = MethodType(List(AnyType), tp)
788837
def methOfAnyVal(tp: Type): MethodType = MethodType(List(AnyValType), tp)
789-
def methOfAnyRef(tp: Type): MethodType = MethodType(List(ObjectType), tp)
838+
def methOfAnyRefOrNull(tp: Type): MethodType = MethodType(List(ObjectType.maybeNullable), tp)
790839

791840
// Derived types
792841

@@ -947,8 +996,16 @@ class Definitions {
947996
name.drop(prefix.length).forall(_.isDigit))
948997

949998
def isBottomClass(cls: Symbol): Boolean =
950-
cls == NothingClass || cls == NullClass
999+
if (ctx.explicitNulls && !ctx.phase.erasedTypes) cls == NothingClass
1000+
else isBottomClassAfterErasure(cls)
1001+
1002+
def isBottomClassAfterErasure(cls: Symbol): Boolean = cls == NothingClass || cls == NullClass
1003+
9511004
def isBottomType(tp: Type): Boolean =
1005+
if (ctx.explicitNulls && !ctx.phase.erasedTypes) tp.derivesFrom(NothingClass)
1006+
else isBottomTypeAfterErasure(tp)
1007+
1008+
def isBottomTypeAfterErasure(tp: Type): Boolean =
9521009
tp.derivesFrom(NothingClass) || tp.derivesFrom(NullClass)
9531010

9541011
/** Is a function class.
@@ -1292,18 +1349,22 @@ class Definitions {
12921349
// ----- Initialization ---------------------------------------------------
12931350

12941351
/** Lists core classes that don't have underlying bytecode, but are synthesized on-the-fly in every reflection universe */
1295-
@tu lazy val syntheticScalaClasses: List[TypeSymbol] = List(
1296-
AnyClass,
1297-
AnyRefAlias,
1298-
AnyKindClass,
1299-
andType,
1300-
orType,
1301-
RepeatedParamClass,
1302-
ByNameParamClass2x,
1303-
AnyValClass,
1304-
NullClass,
1305-
NothingClass,
1306-
SingletonClass)
1352+
@tu lazy val syntheticScalaClasses: List[TypeSymbol] = {
1353+
val synth = List(
1354+
AnyClass,
1355+
AnyRefAlias,
1356+
AnyKindClass,
1357+
andType,
1358+
orType,
1359+
RepeatedParamClass,
1360+
ByNameParamClass2x,
1361+
AnyValClass,
1362+
NullClass,
1363+
NothingClass,
1364+
SingletonClass)
1365+
1366+
if (ctx.explicitNulls) synth :+ JavaNullAlias else synth
1367+
}
13071368

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

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -450,8 +450,12 @@ object Flags {
450450
* is completed)
451451
*/
452452
val AfterLoadFlags: FlagSet = commonFlags(
453-
FromStartFlags, AccessFlags, Final, AccessorOrSealed, LazyOrTrait, SelfName, JavaDefined)
454-
453+
FromStartFlags, AccessFlags, Final, AccessorOrSealed, LazyOrTrait, SelfName, JavaDefined,
454+
// We would like to add JavaEnumValue to this set so that we can correctly
455+
// detect it in JavaNullInterop. However, JavaEnumValue is not initialized at this
456+
// point, so we just make sure that all the "primitive" flags contained in JavaEnumValue
457+
// are mentioned here as well.
458+
Enum, StableRealizable)
455459

456460
/** A value that's unstable unless complemented with a Stable flag */
457461
val UnstableValueFlags: FlagSet = Mutable | Method
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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)(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 (sym.name == nme.TYPE_ || sym.isAllOf(Flags.JavaEnumValue))
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 = {
118+
// Fast version of Type::toJavaNullableUnion that doesn't check whether the type
119+
// is already a union.
120+
def toJavaNullableUnion(tpe: Type): Type = OrType(tpe, defn.JavaNullAliasType)
121+
122+
tp match {
123+
case tp: TypeRef if needsNull(tp) => toJavaNullableUnion(tp)
124+
case appTp @ AppliedType(tycon, targs) =>
125+
val oldOutermostNullable = outermostLevelAlreadyNullable
126+
// We don't make the outmost levels of type arguements nullable if tycon is Java-defined.
127+
// This is because Java classes are _all_ nullified, so both `java.util.List[String]` and
128+
// `java.util.List[String|Null]` contain nullable elements.
129+
outermostLevelAlreadyNullable = tp.classSymbol.is(JavaDefined)
130+
val targs2 = targs map this
131+
outermostLevelAlreadyNullable = oldOutermostNullable
132+
val appTp2 = derivedAppliedType(appTp, tycon, targs2)
133+
if (needsNull(tycon)) toJavaNullableUnion(appTp2) else appTp2
134+
case ptp: PolyType =>
135+
derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType))
136+
case mtp: MethodType =>
137+
val oldOutermostNullable = outermostLevelAlreadyNullable
138+
outermostLevelAlreadyNullable = false
139+
val paramInfos2 = mtp.paramInfos map this
140+
outermostLevelAlreadyNullable = oldOutermostNullable
141+
derivedLambdaType(mtp)(paramInfos2, this(mtp.resType))
142+
case tp: TypeAlias => mapOver(tp)
143+
case tp: AndType =>
144+
// nullify(A & B) = (nullify(A) & nullify(B)) | JavaNull, but take care not to add
145+
// duplicate `JavaNull`s at the outermost level inside `A` and `B`.
146+
outermostLevelAlreadyNullable = true
147+
toJavaNullableUnion(derivedAndType(tp, this(tp.tp1), this(tp.tp2)))
148+
case tp: TypeParamRef if needsNull(tp) => toJavaNullableUnion(tp)
149+
// In all other cases, return the type unchanged.
150+
// In particular, if the type is a ConstantType, then we don't nullify it because it is the
151+
// type of a final non-nullable field.
152+
case _ => tp
153+
}
154+
}
155+
}
156+
}

0 commit comments

Comments
 (0)