diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 4bd0b591a249..bf331e2d1440 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -425,6 +425,9 @@ object Contexts { def useColors: Boolean = base.settings.color.value == "always" + /** Is the explicit nulls option set? */ + def explicitNulls: Boolean = base.settings.YexplicitNulls.value + protected def init(outer: Context, origin: Context): this.type = { util.Stats.record("Context.fresh") _outer = outer diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index d057817c9b79..5a535071f4b3 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -4,7 +4,7 @@ package core import scala.annotation.{threadUnsafe => tu} import Types._, Contexts._, Symbols._, SymDenotations._, StdNames._, Names._ -import Flags._, Scopes._, Decorators._, NameOps._, Periods._ +import Flags._, Scopes._, Decorators._, NameOps._, Periods._, NullOpsDecorator._ import unpickleScala2.Scala2Unpickler.ensureConstructor import scala.collection.mutable import collection.mutable @@ -269,7 +269,7 @@ class Definitions { @tu lazy val Any_asInstanceOf: TermSymbol = enterT1ParameterlessMethod(AnyClass, nme.asInstanceOf_, _.paramRefs(0), Final) @tu lazy val Any_typeTest: TermSymbol = enterT1ParameterlessMethod(AnyClass, nme.isInstanceOfPM, _ => BooleanType, Final | Synthetic | Artifact) @tu lazy val Any_typeCast: TermSymbol = enterT1ParameterlessMethod(AnyClass, nme.asInstanceOfPM, _.paramRefs(0), Final | Synthetic | Artifact | StableRealizable) - // generated by pattern matcher, eliminated by erasure + // generated by pattern matcher and explicit nulls, eliminated by erasure /** def getClass[A >: this.type](): Class[? <: A] */ @tu lazy val Any_getClass: TermSymbol = @@ -347,11 +347,29 @@ class Definitions { ScalaPackageClass, tpnme.Nothing, AbstractFinal, List(AnyClass.typeRef)) def NothingType: TypeRef = NothingClass.typeRef @tu lazy val RuntimeNothingModuleRef: TermRef = ctx.requiredModuleRef("scala.runtime.Nothing") - @tu lazy val NullClass: ClassSymbol = enterCompleteClassSymbol( - ScalaPackageClass, tpnme.Null, AbstractFinal, List(ObjectClass.typeRef)) + @tu lazy val NullClass: ClassSymbol = { + val parent = if (ctx.explicitNulls) AnyType else ObjectType + enterCompleteClassSymbol(ScalaPackageClass, tpnme.Null, AbstractFinal, parent :: Nil) + } def NullType: TypeRef = NullClass.typeRef @tu lazy val RuntimeNullModuleRef: TermRef = ctx.requiredModuleRef("scala.runtime.Null") + /** An alias for null values that originate in Java code. + * This type gets special treatment in the Typer. Specifically, `JavaNull` can be selected through: + * e.g. + * ``` + * // x: String|Null + * x.length // error: `Null` has no `length` field + * // x2: String|JavaNull + * x2.length // allowed by the Typer, but unsound (might throw NPE) + * ``` + */ + lazy val JavaNullAlias: TypeSymbol = { + assert(ctx.explicitNulls) + enterAliasType(tpnme.JavaNull, NullType) + } + def JavaNullAliasType: TypeRef = JavaNullAlias.typeRef + @tu lazy val ImplicitScrutineeTypeSym = newSymbol(ScalaPackageClass, tpnme.IMPLICITkw, EmptyFlags, TypeBounds.empty).entered def ImplicitScrutineeTypeRef: TypeRef = ImplicitScrutineeTypeSym.typeRef @@ -441,12 +459,12 @@ class Definitions { @tu lazy val Boolean_|| : Symbol = BooleanClass.requiredMethod(nme.ZOR) @tu lazy val Boolean_== : Symbol = BooleanClass.info.member(nme.EQ).suchThat(_.info.firstParamTypes match { - case List(pt) => (pt isRef BooleanClass) + case List(pt) => pt.isRef(BooleanClass) case _ => false }).symbol @tu lazy val Boolean_!= : Symbol = BooleanClass.info.member(nme.NE).suchThat(_.info.firstParamTypes match { - case List(pt) => (pt isRef BooleanClass) + case List(pt) => pt.isRef(BooleanClass) case _ => false }).symbol @@ -509,7 +527,7 @@ class Definitions { @tu lazy val StringModule: Symbol = StringClass.linkedClass @tu lazy val String_+ : TermSymbol = enterMethod(StringClass, nme.raw.PLUS, methOfAny(StringType), Final) @tu lazy val String_valueOf_Object: Symbol = StringModule.info.member(nme.valueOf).suchThat(_.info.firstParamTypes match { - case List(pt) => (pt isRef AnyClass) || (pt isRef ObjectClass) + case List(pt) => pt.isRef(AnyClass) || pt.isRef(ObjectClass) case _ => false }).symbol @@ -520,12 +538,16 @@ class Definitions { @tu lazy val BoxedNumberClass: ClassSymbol = ctx.requiredClass("java.lang.Number") @tu lazy val ClassCastExceptionClass: ClassSymbol = ctx.requiredClass("java.lang.ClassCastException") @tu lazy val ClassCastExceptionClass_stringConstructor: TermSymbol = ClassCastExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match { - case List(pt) => (pt isRef StringClass) + case List(pt) => + val pt1 = if (ctx.explicitNulls) pt.stripNull() else pt + pt1.isRef(StringClass) case _ => false }).symbol.asTerm @tu lazy val ArithmeticExceptionClass: ClassSymbol = ctx.requiredClass("java.lang.ArithmeticException") @tu lazy val ArithmeticExceptionClass_stringConstructor: TermSymbol = ArithmeticExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match { - case List(pt) => (pt isRef StringClass) + case List(pt) => + val pt1 = if (ctx.explicitNulls) pt.stripNull() else pt + pt1.isRef(StringClass) case _ => false }).symbol.asTerm @@ -793,6 +815,31 @@ class Definitions { @tu lazy val InfixAnnot: ClassSymbol = ctx.requiredClass("scala.annotation.infix") @tu lazy val AlphaAnnot: ClassSymbol = ctx.requiredClass("scala.annotation.alpha") + // A list of annotations that are commonly used to indicate that a field/method argument or return + // type is not null. These annotations are used by the nullification logic in JavaNullInterop to + // improve the precision of type nullification. + // We don't require that any of these annotations be present in the class path, but we want to + // create Symbols for the ones that are present, so they can be checked during nullification. + @tu lazy val NotNullAnnots: List[ClassSymbol] = ctx.getClassesIfDefined( + "javax.annotation.Nonnull" :: + "javax.validation.constraints.NotNull" :: + "androidx.annotation.NonNull" :: + "android.support.annotation.NonNull" :: + "android.annotation.NonNull" :: + "com.android.annotations.NonNull" :: + "org.eclipse.jdt.annotation.NonNull" :: + "edu.umd.cs.findbugs.annotations.NonNull" :: + "org.checkerframework.checker.nullness.qual.NonNull" :: + "org.checkerframework.checker.nullness.compatqual.NonNullDecl" :: + "org.jetbrains.annotations.NotNull" :: + "org.springframework.lang.NonNull" :: + "org.springframework.lang.NonNullApi" :: + "org.springframework.lang.NonNullFields" :: + "lombok.NonNull" :: + "reactor.util.annotation.NonNull" :: + "reactor.util.annotation.NonNullApi" :: + "io.reactivex.annotations.NonNull" :: Nil map PreNamedString) + // convenient one-parameter method types def methOfAny(tp: Type): MethodType = MethodType(List(AnyType), tp) def methOfAnyVal(tp: Type): MethodType = MethodType(List(AnyValType), tp) @@ -845,7 +892,7 @@ class Definitions { if (ctx.erasedTypes) JavaArrayType(elem) else ArrayType.appliedTo(elem :: Nil) def unapply(tp: Type)(implicit ctx: Context): Option[Type] = tp.dealias match { - case AppliedType(at, arg :: Nil) if at isRef ArrayType.symbol => Some(arg) + case AppliedType(at, arg :: Nil) if at.isRef(ArrayType.symbol) => Some(arg) case _ => None } } @@ -957,8 +1004,16 @@ class Definitions { name.drop(prefix.length).forall(_.isDigit)) def isBottomClass(cls: Symbol): Boolean = - cls == NothingClass || cls == NullClass + if (ctx.explicitNulls && !ctx.phase.erasedTypes) cls == NothingClass + else isBottomClassAfterErasure(cls) + + def isBottomClassAfterErasure(cls: Symbol): Boolean = cls == NothingClass || cls == NullClass + def isBottomType(tp: Type): Boolean = + if (ctx.explicitNulls && !ctx.phase.erasedTypes) tp.derivesFrom(NothingClass) + else isBottomTypeAfterErasure(tp) + + def isBottomTypeAfterErasure(tp: Type): Boolean = tp.derivesFrom(NothingClass) || tp.derivesFrom(NullClass) /** Is a function class. @@ -1089,7 +1144,7 @@ class Definitions { def isTupleType(tp: Type)(implicit ctx: Context): Boolean = { val arity = tp.dealias.argInfos.length - arity <= MaxTupleArity && TupleType(arity) != null && (tp isRef TupleType(arity).symbol) + arity <= MaxTupleArity && TupleType(arity) != null && tp.isRef(TupleType(arity).symbol) } def tupleType(elems: List[Type]): Type = { @@ -1302,18 +1357,22 @@ class Definitions { // ----- Initialization --------------------------------------------------- /** Lists core classes that don't have underlying bytecode, but are synthesized on-the-fly in every reflection universe */ - @tu lazy val syntheticScalaClasses: List[TypeSymbol] = List( - AnyClass, - AnyRefAlias, - AnyKindClass, - andType, - orType, - RepeatedParamClass, - ByNameParamClass2x, - AnyValClass, - NullClass, - NothingClass, - SingletonClass) + @tu lazy val syntheticScalaClasses: List[TypeSymbol] = { + val synth = List( + AnyClass, + AnyRefAlias, + AnyKindClass, + andType, + orType, + RepeatedParamClass, + ByNameParamClass2x, + AnyValClass, + NullClass, + NothingClass, + SingletonClass) + + if (ctx.explicitNulls) synth :+ JavaNullAlias else synth + } @tu lazy val syntheticCoreClasses: List[Symbol] = syntheticScalaClasses ++ List( EmptyPackageVal, diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index e0d60cd5d7f5..dc6cea67be51 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -452,7 +452,6 @@ object Flags { val AfterLoadFlags: FlagSet = commonFlags( FromStartFlags, AccessFlags, Final, AccessorOrSealed, LazyOrTrait, SelfName, JavaDefined) - /** A value that's unstable unless complemented with a Stable flag */ val UnstableValueFlags: FlagSet = Mutable | Method diff --git a/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala b/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala new file mode 100644 index 000000000000..52ba2ac462e4 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala @@ -0,0 +1,150 @@ +package dotty.tools.dotc.core + +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Flags.JavaDefined +import dotty.tools.dotc.core.StdNames.{jnme, nme} +import dotty.tools.dotc.core.Symbols._ +import dotty.tools.dotc.core.Types._ +import NullOpsDecorator._ + +/** This module defines methods to interpret types of Java symbols, which are implicitly nullable in Java, + * as Scala types, which are explicitly nullable. + * + * The transformation is (conceptually) a function `n` that adheres to the following rules: + * (1) n(T) = T|JavaNull if T is a reference type + * (2) n(T) = T if T is a value type + * (3) n(C[T]) = C[T]|JavaNull if C is Java-defined + * (4) n(C[T]) = C[n(T)]|JavaNull if C is Scala-defined + * (5) n(A|B) = n(A)|n(B)|JavaNull + * (6) n(A&B) = n(A) & n(B) + * (7) n((A1, ..., Am)R) = (n(A1), ..., n(Am))n(R) for a method with arguments (A1, ..., Am) and return type R + * (8) n(T) = T otherwise + * + * Treatment of generics (rules 3 and 4): + * - if `C` is Java-defined, then `n(C[T]) = C[T]|JavaNull`. That is, we don't recurse + * on the type argument, and only add JavaNull on the outside. This is because + * `C` itself will be nullified, and in particular so will be usages of `C`'s type argument within C's body. + * e.g. calling `get` on a `java.util.List[String]` already returns `String|Null` and not `String`, so + * we don't need to write `java.util.List[String|Null]`. + * - if `C` is Scala-defined, however, then we want `n(C[T]) = C[n(T)]|JavaNull`. This is because + * `C` won't be nullified, so we need to indicate that its type argument is nullable. + * + * Notice that since the transformation is only applied to types attached to Java symbols, it doesn't need + * to handle the full spectrum of Scala types. Additionally, some kinds of symbols like constructors and + * enum instances get special treatment. + */ +object JavaNullInterop { + + /** Transforms the type `tp` of Java member `sym` to be explicitly nullable. + * `tp` is needed because the type inside `sym` might not be set when this method is called. + * + * e.g. given a Java method + * String foo(String arg) { return arg; } + * + * After calling `nullifyMember`, Scala will see the method as + * + * def foo(arg: String|JavaNull): String|JavaNull + * + * This nullability function uses `JavaNull` instead of vanilla `Null`, for usability. + * This means that we can select on the return of `foo`: + * + * val len = foo("hello").length + * + * But the selection can throw an NPE if the returned value is `null`. + */ + def nullifyMember(sym: Symbol, tp: Type, isEnumValueDef: Boolean)(implicit ctx: Context): Type = { + assert(ctx.explicitNulls) + assert(sym.is(JavaDefined), "can only nullify java-defined members") + + // Some special cases when nullifying the type + if (isEnumValueDef || sym.name == nme.TYPE_) + // Don't nullify the `TYPE` field in every class and Java enum instances + tp + else if (sym.name == nme.toString_ || sym.isConstructor || hasNotNullAnnot(sym)) + // Don't nullify the return type of the `toString` method. + // Don't nullify the return type of constructors. + // Don't nullify the return type of methods with a not-null annotation. + nullifyExceptReturnType(tp) + else + // Otherwise, nullify everything + nullifyType(tp) + } + + private def hasNotNullAnnot(sym: Symbol)(implicit ctx: Context): Boolean = + ctx.definitions.NotNullAnnots.exists(nna => sym.unforcedAnnotation(nna).isDefined) + + /** If tp is a MethodType, the parameters and the inside of return type are nullified, + * but the result return type is not nullable. + * If tp is a type of a field, the inside of the type is nullified, + * but the result type is not nullable. + */ + private def nullifyExceptReturnType(tp: Type)(implicit ctx: Context): Type = + new JavaNullMap(true)(ctx)(tp) + + /** Nullifies a Java type by adding `| JavaNull` in the relevant places. */ + private def nullifyType(tp: Type)(implicit ctx: Context): Type = + new JavaNullMap(false)(ctx)(tp) + + /** A type map that implements the nullification function on types. Given a Java-sourced type, this adds `| JavaNull` + * in the right places to make the nulls explicit in Scala. + * + * @param outermostLevelAlreadyNullable whether this type is already nullable at the outermost level. + * For example, `Array[String]|JavaNull` is already nullable at the + * outermost level, but `Array[String|JavaNull]` isn't. + * If this parameter is set to true, then the types of fields, and the return + * types of methods will not be nullified. + * This is useful for e.g. constructors, and also so that `A & B` is nullified + * to `(A & B) | JavaNull`, instead of `(A|JavaNull & B|JavaNull) | JavaNull`. + */ + private class JavaNullMap(var outermostLevelAlreadyNullable: Boolean)(implicit ctx: Context) extends TypeMap { + /** Should we nullify `tp` at the outermost level? */ + def needsNull(tp: Type): Boolean = + !outermostLevelAlreadyNullable && (tp match { + case tp: TypeRef => + // We don't modify value types because they're non-nullable even in Java. + !tp.symbol.isValueClass && + // We don't modify `Any` because it's already nullable. + !tp.isRef(defn.AnyClass) && + // We don't nullify Java varargs at the top level. + // Example: if `setNames` is a Java method with signature `void setNames(String... names)`, + // then its Scala signature will be `def setNames(names: (String|JavaNull)*): Unit`. + // This is because `setNames(null)` passes as argument a single-element array containing the value `null`, + // and not a `null` array. + !tp.isRef(defn.RepeatedParamClass) + case _ => true + }) + + override def apply(tp: Type): Type = tp match { + case tp: TypeRef if needsNull(tp) => OrJavaNull(tp) + case appTp @ AppliedType(tycon, targs) => + val oldOutermostNullable = outermostLevelAlreadyNullable + // We don't make the outmost levels of type arguements nullable if tycon is Java-defined. + // This is because Java classes are _all_ nullified, so both `java.util.List[String]` and + // `java.util.List[String|Null]` contain nullable elements. + outermostLevelAlreadyNullable = tp.classSymbol.is(JavaDefined) + val targs2 = targs map this + outermostLevelAlreadyNullable = oldOutermostNullable + val appTp2 = derivedAppliedType(appTp, tycon, targs2) + if (needsNull(tycon)) OrJavaNull(appTp2) else appTp2 + case ptp: PolyType => + derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType)) + case mtp: MethodType => + val oldOutermostNullable = outermostLevelAlreadyNullable + outermostLevelAlreadyNullable = false + val paramInfos2 = mtp.paramInfos map this + outermostLevelAlreadyNullable = oldOutermostNullable + derivedLambdaType(mtp)(paramInfos2, this(mtp.resType)) + case tp: TypeAlias => mapOver(tp) + case tp: AndType => + // nullify(A & B) = (nullify(A) & nullify(B)) | JavaNull, but take care not to add + // duplicate `JavaNull`s at the outermost level inside `A` and `B`. + outermostLevelAlreadyNullable = true + OrJavaNull(derivedAndType(tp, this(tp.tp1), this(tp.tp2))) + case tp: TypeParamRef if needsNull(tp) => OrJavaNull(tp) + // In all other cases, return the type unchanged. + // In particular, if the type is a ConstantType, then we don't nullify it because it is the + // type of a final non-nullable field. + case _ => tp + } + } +} diff --git a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala new file mode 100644 index 000000000000..401cce87241d --- /dev/null +++ b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala @@ -0,0 +1,86 @@ +package dotty.tools.dotc.core + +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Symbols.defn +import dotty.tools.dotc.core.Types._ + +/** Defines operations on nullable types. */ +object NullOpsDecorator { + + implicit class NullOps(val self: Type) { + /** Is this type exactly `JavaNull` (no vars, aliases, refinements etc allowed)? */ + def isJavaNullType(implicit ctx: Context): Boolean = { + assert(ctx.explicitNulls) + // We can't do `self == defn.JavaNull` because when trees are unpickled new references + // to `JavaNull` could be created that are different from `defn.JavaNull`. + // Instead, we compare the symbol. + self.isDirectRef(defn.JavaNullAlias) + } + + /** Syntactically strips the nullability from this type. + * If the type is `T1 | ... | Tn`, and `Ti` references to `Null` (or `JavaNull`), + * then return `T1 | ... | Ti-1 | Ti+1 | ... | Tn`. + * If this type isn't (syntactically) nullable, then returns the type unchanged. + * + * @param onlyJavaNull whether we only remove `JavaNull`, the default value is false + */ + def stripNull(onlyJavaNull: Boolean = false)(implicit ctx: Context): Type = { + assert(ctx.explicitNulls) + + def isNull(tp: Type) = + if (onlyJavaNull) tp.isJavaNullType + else tp.isNullType + + def strip(tp: Type): Type = tp match { + case tp @ OrType(lhs, rhs) => + val llhs = strip(lhs) + val rrhs = strip(rhs) + if (isNull(rrhs)) llhs + else if (isNull(llhs)) rrhs + else tp.derivedOrType(llhs, rrhs) + case tp @ AndType(tp1, tp2) => + // We cannot `tp.derivedAndType(strip(tp1), strip(tp2))` directly, + // since `stripNull((A | Null) & B)` would produce the wrong + // result `(A & B) | Null`. + val tp1s = strip(tp1) + val tp2s = strip(tp2) + if((tp1s ne tp1) && (tp2s ne tp2)) + tp.derivedAndType(tp1s, tp2s) + else tp + case _ => tp + } + + val self1 = self.widenDealias + val stripped = strip(self1) + if (stripped ne self1) stripped else self + } + + /** Like `stripNull`, but removes only the `JavaNull`s. */ + def stripJavaNull(implicit ctx: Context): Type = self.stripNull(true) + + /** Collapses all `JavaNull` unions within this type, and not just the outermost ones (as `stripJavaNull` does). + * e.g. (Array[String|JavaNull]|JavaNull).stripJavaNull => Array[String|JavaNull] + * (Array[String|JavaNull]|JavaNull).stripAllJavaNull => Array[String] + * If no `JavaNull` unions are found within the type, then returns the input type unchanged. + */ + def stripAllJavaNull(implicit ctx: Context): Type = { + object RemoveNulls extends TypeMap { + override def apply(tp: Type): Type = mapOver(tp.stripNull(true)) + } + val rem = RemoveNulls(self) + if (rem ne self) rem else self + } + + /** Is self (after widening and dealiasing) a type of the form `T | Null`? */ + def isNullableUnion(implicit ctx: Context): Boolean = { + val stripped = self.stripNull() + stripped ne self + } + + /** Is self (after widening and dealiasing) a type of the form `T | JavaNull`? */ + def isJavaNullableUnion(implicit ctx: Context): Boolean = { + val stripped = self.stripNull(true) + stripped ne self + } + } +} diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index f3654e1ab53b..afb7d66f0672 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -197,6 +197,7 @@ object StdNames { final val Nothing: N = "Nothing" final val NotNull: N = "NotNull" final val Null: N = "Null" + final val JavaNull: N = "JavaNull" final val Object: N = "Object" final val Product: N = "Product" final val PartialFunction: N = "PartialFunction" diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 5a919d48467f..62a408209c20 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -797,8 +797,16 @@ object SymDenotations { // after Erasure and to avoid cyclic references caused by forcing denotations } - /** Is this symbol a class references to which that are supertypes of null? */ + /** Is this symbol a class of which `null` is a value? */ final def isNullableClass(implicit ctx: Context): Boolean = + if (ctx.explicitNulls && !ctx.phase.erasedTypes) symbol == defn.NullClass || symbol == defn.AnyClass + else isNullableClassAfterErasure + + /** Is this symbol a class of which `null` is a value after erasure? + * For example, if `-Yexplicit-nulls` is set, `String` is not nullable before erasure, + * but it becomes nullable after erasure. + */ + final def isNullableClassAfterErasure(implicit ctx: Context): Boolean = isClass && !isValueClass && !is(ModuleClass) && symbol != defn.NothingClass /** Is this definition accessible as a member of tree with type `pre`? diff --git a/compiler/src/dotty/tools/dotc/core/Symbols.scala b/compiler/src/dotty/tools/dotc/core/Symbols.scala index 3ee8751a1f19..d783780f593d 100644 --- a/compiler/src/dotty/tools/dotc/core/Symbols.scala +++ b/compiler/src/dotty/tools/dotc/core/Symbols.scala @@ -383,6 +383,12 @@ trait Symbols { this: Context => .requiredSymbol("class", name, generateStubs = false)(_.isClass) } + /** Get a List of ClassSymbols which are either defined in current compilation + * run or present on classpath. + */ + def getClassesIfDefined(paths: List[PreName]): List[ClassSymbol] = + paths.map(getClassIfDefined).filter(_.exists).map(_.asInstanceOf[ClassSymbol]) + /** Get ClassSymbol if package is either defined in current compilation run * or present on classpath. * Returns NoSymbol otherwise. */ diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 582c41a2a950..dc112a59403a 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -18,6 +18,7 @@ import scala.util.control.NonFatal import typer.ProtoTypes.constrained import typer.Applications.productSelectorTypes import reporting.trace +import NullOpsDecorator.NullOps final class AbsentContext object AbsentContext { @@ -1611,9 +1612,24 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] w formals2 match { case formal2 :: rest2 => val formal2a = if (tp2.isParamDependent) formal2.subst(tp2, tp1) else formal2 + // The next two definitions handle the special case mentioned above, where + // the Java argument has type 'Any', and the Scala argument has type 'Object' or + // 'Object|Null', depending on whether explicit nulls are enabled. + def formal1IsObject = + if (ctx.explicitNulls) formal1 match { + case OrNull(formal1b) => formal1b.isRef(ObjectClass) + case _ => false + } + else formal1.isRef(ObjectClass) + def formal2IsObject = + if (ctx.explicitNulls) formal2 match { + case OrNull(formal2b) => formal2b.isRef(ObjectClass) + case _ => false + } + else formal2.isRef(ObjectClass) (isSameTypeWhenFrozen(formal1, formal2a) - || tp1.isJavaMethod && (formal2 isRef ObjectClass) && (formal1 isRef AnyClass) - || tp2.isJavaMethod && (formal1 isRef ObjectClass) && (formal2 isRef AnyClass)) && + || tp1.isJavaMethod && formal2IsObject && (formal1 isRef AnyClass) + || tp2.isJavaMethod && formal1IsObject && (formal2 isRef AnyClass)) && loop(rest1, rest2) case nil => false diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 700fe6134940..394e9a58fe5e 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -289,8 +289,8 @@ object TypeErasure { // We need to short-circuit this case here because the regular lub logic below // relies on the class hierarchy, which doesn't properly capture `Null`s subtyping // behaviour. - if (defn.isBottomType(tp1) && tp2.derivesFrom(defn.ObjectClass)) return tp2 - if (defn.isBottomType(tp2) && tp1.derivesFrom(defn.ObjectClass)) return tp1 + if (defn.isBottomTypeAfterErasure(tp1) && tp2.derivesFrom(defn.ObjectClass)) return tp2 + if (defn.isBottomTypeAfterErasure(tp2) && tp1.derivesFrom(defn.ObjectClass)) return tp1 tp1 match { case JavaArrayType(elem1) => import dotty.tools.dotc.transform.TypeUtils._ diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index c834c929b8eb..68e8b91f4f72 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -7,6 +7,7 @@ import Symbols._ import Flags._ import Names._ import StdNames._, NameOps._ +import NullOpsDecorator._ import NameKinds.SkolemName import Scopes._ import Constants._ @@ -16,6 +17,7 @@ import SymDenotations._ import Decorators._ import Denotations._ import Periods._ +import CheckRealizable._ import util.Stats._ import util.SimpleIdentitySet import reporting.diagnostic.Message @@ -162,6 +164,9 @@ object Types { case tp: RefinedOrRecType => tp.parent.isStable case tp: ExprType => tp.resultType.isStable case tp: AnnotatedType => tp.parent.isStable + case tp: AndType => + tp.tp1.isStable && (realizability(tp.tp2) eq Realizable) || + tp.tp2.isStable && (realizability(tp.tp1) eq Realizable) case _ => false } @@ -600,11 +605,7 @@ object Types { case AndType(l, r) => goAnd(l, r) case tp: OrType => - // we need to keep the invariant that `pre <: tp`. Branch `union-types-narrow-prefix` - // achieved that by narrowing `pre` to each alternative, but it led to merge errors in - // lots of places. The present strategy is instead of widen `tp` using `join` to be a - // supertype of `pre`. - go(tp.join) + goOr(tp) case tp: JavaArrayType => defn.ObjectType.findMember(name, pre, required, excluded) case err: ErrorType => @@ -710,6 +711,21 @@ object Types { def goAnd(l: Type, r: Type) = go(l) & (go(r), pre, safeIntersection = ctx.base.pendingMemberSearches.contains(name)) + def goOr(tp: OrType) = tp match { + case OrJavaNull(tp1) => + // Selecting `name` from a type `T|JavaNull` is like selecting `name` from `T`. + // This can throw at runtime, but we trade soundness for usability. + // We need to strip `JavaNull` from both the type and the prefix so that + // `pre <: tp` continues to hold. + tp1.findMember(name, pre.stripJavaNull, required, excluded) + case _ => + // we need to keep the invariant that `pre <: tp`. Branch `union-types-narrow-prefix` + // achieved that by narrowing `pre` to each alternative, but it led to merge errors in + // lots of places. The present strategy is instead of widen `tp` using `join` to be a + // supertype of `pre`. + go(tp.join) + } + val recCount = ctx.base.findMemberCount if (recCount >= Config.LogPendingFindMemberThreshold) ctx.base.pendingMemberSearches = name :: ctx.base.pendingMemberSearches @@ -1089,16 +1105,28 @@ object Types { * * is approximated by constraining `A` to be =:= to `Int` and returning `ArrayBuffer[Int]` * instead of `ArrayBuffer[? >: Int | A <: Int & A]` + * + * Exception (if `-YexplicitNulls` is set): if this type is a nullable union (i.e. of the form `T | Null`), + * then the top-level union isn't widened. This is needed so that type inference can infer nullable types. */ def widenUnion(implicit ctx: Context): Type = widen match { - case tp @ OrType(tp1, tp2) => - if tp1.isNull || tp2.isNull then tp - else ctx.typeComparer.lub(tp1.widenUnion, tp2.widenUnion, canConstrain = true) match { + case tp @ OrNull(tp1): OrType => + // Don't widen `T|Null`, since otherwise we wouldn't be able to infer nullable unions. + val tp1Widen = tp1.widenUnionWithoutNull + if (tp1Widen.isRef(defn.AnyClass)) tp1Widen + else tp.derivedOrType(tp1Widen, defn.NullType) + case tp => + tp.widenUnionWithoutNull + } + + def widenUnionWithoutNull(implicit ctx: Context): Type = widen match { + case tp @ OrType(lhs, rhs) => + ctx.typeComparer.lub(lhs.widenUnionWithoutNull, rhs.widenUnionWithoutNull, canConstrain = true) match { case union: OrType => union.join case res => res } case tp @ AndType(tp1, tp2) => - tp derived_& (tp1.widenUnion, tp2.widenUnion) + tp derived_& (tp1.widenUnionWithoutNull, tp2.widenUnionWithoutNull) case tp: RefinedType => tp.derivedRefinedType(tp.parent.widenUnion, tp.refinedName, tp.refinedInfo) case tp: RecType => @@ -1424,9 +1452,7 @@ object Types { } /** Is this (an alias of) the `scala.Null` type? */ - final def isNull(given Context) = - isRef(defn.NullClass) - || classSymbol.name == tpnme.Null // !!! temporary kludge for being able to test without the explicit nulls PR + final def isNullType(given Context) = isRef(defn.NullClass) /** The resultType of a LambdaType, or ExprType, the type itself for others */ def resultType(implicit ctx: Context): Type = this @@ -2915,23 +2941,41 @@ object Types { else apply(tp1, tp2) } - /** An extractor for `T | Null` or `Null | T`, returning the `T` */ - object OrNull with - private def stripNull(tp: Type)(given Context): Type = tp match - case tp @ OrType(tp1, tp2) => - if tp1.isNull then tp2 - else if tp2.isNull then tp1 - else tp.derivedOrType(stripNull(tp1), stripNull(tp2)) - case tp @ AndType(tp1, tp2) => - tp.derivedAndType(stripNull(tp1), stripNull(tp2)) - case _ => - tp + /** An extractor object to pattern match against a nullable union. + * e.g. + * + * (tp: Type) match + * case OrNull(tp1) => // tp had the form `tp1 | Null` + * case _ => // tp was not a nullable union + */ + object OrNull { def apply(tp: Type)(given Context) = OrType(tp, defn.NullType) - def unapply(tp: Type)(given Context): Option[Type] = - val tp1 = stripNull(tp) - if tp1 ne tp then Some(tp1) else None - end OrNull + def unapply(tp: Type)(given ctx: Context): Option[Type] = + if (ctx.explicitNulls) { + val tp1 = tp.stripNull() + if tp1 ne tp then Some(tp1) else None + } + else None + } + + /** An extractor object to pattern match against a Java-nullable union. + * e.g. + * + * (tp: Type) match + * case OrJavaNull(tp1) => // tp had the form `tp1 | JavaNull` + * case _ => // tp was not a Java-nullable union + */ + object OrJavaNull { + def apply(tp: Type)(given Context) = + OrType(tp, defn.JavaNullAliasType) + def unapply(tp: Type)(given ctx: Context): Option[Type] = + if (ctx.explicitNulls) { + val tp1 = tp.stripJavaNull + if tp1 ne tp then Some(tp1) else None + } + else None + } // ----- ExprType and LambdaTypes ----------------------------------- diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index 9ca55f762ff0..2813a57e5acb 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -288,6 +288,8 @@ class ClassfileParser( if (denot.is(Flags.Method) && (jflags & JAVA_ACC_VARARGS) != 0) denot.info = arrayToRepeated(denot.info) + if (ctx.explicitNulls) denot.info = JavaNullInterop.nullifyMember(denot.symbol, denot.info, isEnum) + // seal java enums if (isEnum) { val enumClass = sym.owner.linkedClass diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala index ef573fb7a1df..1785164eff8f 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Completion.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -207,7 +207,7 @@ object Completion { def addMemberCompletions(qual: Tree)(implicit ctx: Context): Unit = if (!qual.tpe.widenDealias.isBottomType) { addAccessibleMembers(qual.tpe) - if (!mode.is(Mode.Import) && !qual.tpe.isNull) + if (!mode.is(Mode.Import) && !qual.tpe.isNullType) // Implicit conversions do not kick in when importing // and for `NullClass` they produce unapplicable completions (for unclear reasons) implicitConversionTargets(qual)(ctx.fresh.setExploreTyperState()) diff --git a/compiler/src/dotty/tools/dotc/transform/CollectNullableFields.scala b/compiler/src/dotty/tools/dotc/transform/CollectNullableFields.scala index 28cf0924a017..0fd3bdde12d3 100644 --- a/compiler/src/dotty/tools/dotc/transform/CollectNullableFields.scala +++ b/compiler/src/dotty/tools/dotc/transform/CollectNullableFields.scala @@ -34,7 +34,7 @@ object CollectNullableFields { * - belongs to a non trait-class * - is private[this] * - is not lazy - * - its type is nullable + * - its type is nullable after erasure * - is only used in a lazy val initializer * - defined in the same class as the lazy val */ @@ -65,7 +65,9 @@ class CollectNullableFields extends MiniPhase { !sym.is(Lazy) && !sym.owner.is(Trait) && sym.initial.isAllOf(PrivateLocal) && - sym.info.widenDealias.typeSymbol.isNullableClass + // We need `isNullableClassAfterErasure` and not `isNullable` because + // we care about the values as present in the JVM. + sym.info.widenDealias.typeSymbol.isNullableClassAfterErasure if (isNullablePrivateField) nullability.get(sym) match { diff --git a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala index f35507a15133..ccbf079c33cb 100644 --- a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala +++ b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala @@ -17,6 +17,7 @@ import DenotTransformers._ import NameOps._ import NameKinds.OuterSelectName import StdNames._ +import NullOpsDecorator._ object FirstTransform { val name: String = "firstTransform" @@ -50,10 +51,25 @@ class FirstTransform extends MiniPhase with InfoTransformer { thisPhase => override def checkPostCondition(tree: Tree)(implicit ctx: Context): Unit = tree match { case Select(qual, name) if !name.is(OuterSelectName) && tree.symbol.exists => + val qualTpe = if (ctx.explicitNulls) { + // `JavaNull` is already special-cased in the Typer, but needs to be handled here as well. + // We need `stripAllJavaNull` and not `stripJavaNull` because of the following case: + // + // val s: (String|JavaNull)&(String|JavaNull) = "hello" + // val l = s.length + // + // The invariant below is that the type of `s`, which isn't a top-level JavaNull union, + // must derive from the type of the owner of `length`, which is `String`. Because we don't + // know which `JavaNull`s were used to find the `length` member, we conservatively remove + // all of them. + qual.tpe.stripAllJavaNull + } else { + qual.tpe + } assert( - qual.tpe.derivesFrom(tree.symbol.owner) || - tree.symbol.is(JavaStatic) && qual.tpe.derivesFrom(tree.symbol.enclosingClass), - i"non member selection of ${tree.symbol.showLocated} from ${qual.tpe} in $tree") + qualTpe.derivesFrom(tree.symbol.owner) || + tree.symbol.is(JavaStatic) && qualTpe.derivesFrom(tree.symbol.enclosingClass), + i"non member selection of ${tree.symbol.showLocated} from ${qualTpe} in $tree") case _: TypeTree => case _: Import | _: NamedArg | _: TypTree => assert(false, i"illegal tree: $tree") diff --git a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala index c41c6e7729c1..186bc9521041 100644 --- a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala +++ b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala @@ -16,7 +16,6 @@ import dotty.tools.dotc.core.Types._ import dotty.tools.dotc.core.{Names, StdNames} import dotty.tools.dotc.transform.MegaPhase.MiniPhase import dotty.tools.dotc.transform.SymUtils._ - import scala.collection.mutable class LazyVals extends MiniPhase with IdentityDenotTransformer { diff --git a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala index 928d965025e6..2bcf4088f1e4 100644 --- a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala +++ b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala @@ -14,6 +14,7 @@ import ValueClasses.isDerivedValueClass import SymUtils._ import util.Property import config.Printers.derive +import NullOpsDecorator._ object SyntheticMembers { @@ -187,7 +188,9 @@ class SyntheticMembers(thisPhase: DenotTransformer) { val ioob = defn.IndexOutOfBoundsException.typeRef // Second constructor of ioob that takes a String argument def filterStringConstructor(s: Symbol): Boolean = s.info match { - case m: MethodType if s.isConstructor => m.paramInfos == List(defn.StringType) + case m: MethodType if s.isConstructor && m.paramInfos.size == 1 => + val pinfo = if (ctx.explicitNulls) m.paramInfos.head.stripJavaNull else m.paramInfos.head + pinfo == defn.StringType case _ => false } val constructor = ioob.typeSymbol.info.decls.find(filterStringConstructor _).asTerm diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index b1fdf12aaf50..6120ed356805 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -22,6 +22,7 @@ import reporting.diagnostic.messages._ import reporting.trace import config.Printers.{exhaustivity => debug} import util.SourcePosition +import NullOpsDecorator._ /** Space logic for checking exhaustivity and unreachability of pattern matching * @@ -293,14 +294,28 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic { private val scalaNilType = ctx.requiredModuleRef("scala.collection.immutable.Nil") private val scalaConsType = ctx.requiredClassRef("scala.collection.immutable.::") - private val nullType = ConstantType(Constant(null)) - private val nullSpace = Typ(nullType) + private val constantNullType = ConstantType(Constant(null)) + private val constantNullSpace = Typ(constantNullType) - override def intersectUnrelatedAtomicTypes(tp1: Type, tp2: Type): Space = trace(s"atomic intersection: ${AndType(tp1, tp2).show}", debug) { - // Precondition: !isSubType(tp1, tp2) && !isSubType(tp2, tp1) + /** Does the given tree stand for the literal `null`? */ + def isNullLit(tree: Tree): Boolean = tree match { + case Literal(Constant(null)) => true + case _ => false + } - // Since projections of types don't include null, intersection with null is empty. - if (tp1 == nullType || tp2 == nullType) Empty + /** Does the given space contain just the value `null`? */ + def isNullSpace(space: Space): Boolean = space match { + case Typ(tpe, _) => tpe.dealias == constantNullType || tpe.isNullType + case Or(spaces) => spaces.forall(isNullSpace) + case _ => false + } + + override def intersectUnrelatedAtomicTypes(tp1: Type, tp2: Type): Space = trace(s"atomic intersection: ${AndType(tp1, tp2).show}", debug) { + // Precondition: !isSubType(tp1, tp2) && !isSubType(tp2, tp1). + if (!ctx.explicitNulls && (tp1.isNullType || tp2.isNullType)) { + // Since projections of types don't include null, intersection with null is empty. + return Empty + } else { val res = ctx.typeComparer.provablyDisjoint(tp1, tp2) @@ -320,7 +335,7 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic { Typ(ConstantType(c), false) case pat: Ident if isBackquoted(pat) => Typ(pat.tpe, false) case Ident(nme.WILDCARD) => - Or(Typ(pat.tpe.stripAnnots, false) :: nullSpace :: Nil) + Or(Typ(pat.tpe.stripAnnots, false) :: constantNullSpace :: Nil) case Ident(_) | Select(_, _) => Typ(erase(pat.tpe.stripAnnots), false) case Alternative(trees) => Or(trees.map(project(_))) @@ -437,7 +452,11 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic { /** Is `tp1` a subtype of `tp2`? */ def isSubType(tp1: Type, tp2: Type): Boolean = { debug.println(TypeComparer.explained(tp1 <:< tp2)) - val res = (tp1 != nullType || tp2 == nullType) && tp1 <:< tp2 + val res = if (ctx.explicitNulls) { + tp1 <:< tp2 + } else { + (tp1 != constantNullType || tp2 == constantNullType) && tp1 <:< tp2 + } res } @@ -763,10 +782,10 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic { if (!redundancyCheckable(sel)) return val targetSpace = - if (selTyp.classSymbol.isPrimitiveValueClass) + if (ctx.explicitNulls || selTyp.classSymbol.isPrimitiveValueClass) Typ(selTyp, true) else - Or(Typ(selTyp, true) :: nullSpace :: Nil) + Or(Typ(selTyp, true) :: constantNullSpace :: Nil) // in redundancy check, take guard as false in order to soundly approximate def projectPrevCases(cases: List[CaseDef]): Space = @@ -775,11 +794,6 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic { else Empty }.reduce((a, b) => Or(List(a, b))) - def isNull(tree: Tree): Boolean = tree match { - case Literal(Constant(null)) => true - case _ => false - } - (1 until cases.length).foreach { i => val prevs = projectPrevCases(cases.take(i)) @@ -796,16 +810,18 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic { // `covered == Empty` may happen for primitive types with auto-conversion // see tests/patmat/reader.scala tests/patmat/byte.scala - if (covered == Empty) covered = curr + if (covered == Empty && !isNullLit(pat)) covered = curr if (isSubspace(covered, prevs)) { ctx.warning(MatchCaseUnreachable(), pat.sourcePos) } // if last case is `_` and only matches `null`, produce a warning - if (i == cases.length - 1 && !isNull(pat) ) { + // If explicit nulls are enabled, this check isn't needed because most of the cases + // that would trigger it would also trigger unreachability warnings. + if (!ctx.explicitNulls && i == cases.length - 1 && !isNullLit(pat) ) { simplify(minus(covered, prevs)) match { - case Typ(`nullType`, _) => + case Typ(`constantNullType`, _) => ctx.warning(MatchCaseOnlyNullWarning(), pat.sourcePos) case _ => } diff --git a/compiler/src/dotty/tools/dotc/typer/ConstFold.scala b/compiler/src/dotty/tools/dotc/typer/ConstFold.scala index 0fd7174dc774..6e95cb2714e3 100644 --- a/compiler/src/dotty/tools/dotc/typer/ConstFold.scala +++ b/compiler/src/dotty/tools/dotc/typer/ConstFold.scala @@ -11,7 +11,6 @@ import Constants._ import Names._ import StdNames._ import Contexts._ -import Nullables.{CompareNull, TrackedRef} object ConstFold { @@ -20,10 +19,6 @@ object ConstFold { /** If tree is a constant operation, replace with result. */ def apply[T <: Tree](tree: T)(implicit ctx: Context): T = finish(tree) { tree match { - case CompareNull(TrackedRef(ref), testEqual) - if ctx.settings.YexplicitNulls.value && ctx.notNullInfos.impliesNotNull(ref) => - // TODO maybe drop once we have general Nullability? - Constant(!testEqual) case Apply(Select(xt, op), yt :: Nil) => xt.tpe.widenTermRefExpr.normalized match case ConstantType(x) => diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 451c107f98ac..0d8111fbd642 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -807,6 +807,16 @@ trait Implicits { self: Typer => cmpWithBoxed(cls1, cls2) else if (cls2.isPrimitiveValueClass) cmpWithBoxed(cls2, cls1) + else if (ctx.explicitNulls) + // If explicit nulls is enabled, we want to disallow comparison between Object and Null. + // If a nullable value has a non-nullable type, we can still cast it to nullable type + // then compare. + // + // Example: + // val x: String = null.asInstanceOf[String] + // if (x == null) {} // error: x is non-nullable + // if (x.asInstanceOf[String|Null] == null) {} // ok + cls1 == defn.NullClass && cls1 == cls2 else if (cls1 == defn.NullClass) cls1 == cls2 || cls2.derivesFrom(defn.ObjectClass) else if (cls2 == defn.NullClass) diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index da6d6f344bb4..c36ef250b122 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1446,7 +1446,10 @@ class Namer { typer: Typer => case _ => WildcardType } - paramFn(checkSimpleKinded(typedAheadType(mdef.tpt, tptProto)).tpe) + val memTpe = paramFn(checkSimpleKinded(typedAheadType(mdef.tpt, tptProto)).tpe) + if (ctx.explicitNulls && mdef.mods.is(JavaDefined)) + JavaNullInterop.nullifyMember(sym, memTpe, mdef.mods.isAllOf(JavaEnumValue)) + else memTpe } /** The type signature of a DefDef with given symbol */ diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 1f6fe39b2b1d..0142b23dcdc0 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -9,7 +9,8 @@ import StdNames.nme import util.Property import Names.Name import util.Spans.Span -import Flags.Mutable +import Flags._ +import NullOpsDecorator._ import collection.mutable /** Operations for implementing a flow analysis for nullability */ @@ -100,17 +101,37 @@ object Nullables with case _ => None end TrackedRef - /** Is given reference tracked for nullability? - * This is the case if the reference is a path to an immutable val, or if it refers - * to a local mutable variable where all assignments to the variable are _reachable_ - * (in the sense of how it is defined in assignmentSpans). + /** Is the given reference tracked for nullability? + * + * This is the case if one of the following holds: + * 1) The reference is a path to an immutable `val`. + * 2) The reference is to a mutable variable, in which case all assignments to it must be + * reachable (in the sense of how it is defined in assignmentSpans) _and_ the variable + * must not be used "out of order" (in the sense specified by `usedOutOfOrder`). + * + * Whether to track a local mutable variable during flow typing? + * We track a local mutable variable iff the variable is not assigned in a closure. + * For example, in the following code `x` is assigned to by the closure `y`, so we do not + * do flow typing on `x`. + * + * ```scala + * var x: String|Null = ??? + * def y = { + * x = null + * } + * if (x != null) { + * // y can be called here, which break the fact + * val a: String = x // error: x is captured and mutated by the closure, not trackable + * } + * ``` + * + * Check `usedOutOfOrder` to see the explaination and example of "out of order". + * See more examples in `tests/explicit-nulls/neg/var-ref-in-closure.scala`. */ def isTracked(ref: TermRef)(given Context) = ref.isStable || { val sym = ref.symbol - sym.is(Mutable) - && sym.owner.isTerm - && sym.owner.enclosingMethod == curCtx.owner.enclosingMethod + !ref.usedOutOfOrder && sym.span.exists && curCtx.compilationUnit != null // could be null under -Ytest-pickler && curCtx.compilationUnit.assignmentSpans.contains(sym.span.start) @@ -161,6 +182,60 @@ object Nullables with then infos else info :: infos + given refOps: extension (ref: TermRef) with + + /** Is the use of a mutable variable out of order + * + * Whether to generate and use flow typing on a specific _use_ of a local mutable variable? + * We only want to do flow typing on a use that belongs to the same method as the definition + * of the local variable. + * For example, in the following code, even `x` is not assigned to by a closure, but we can only + * use flow typing in one of the occurrences (because the other occurrence happens within a nested + * closure). + * ```scala + * var x: String|Null = ??? + * def y = { + * if (x != null) { + * // not safe to use the fact (x != null) here + * // since y can be executed at the same time as the outer block + * val _: String = x + * } + * } + * if (x != null) { + * val a: String = x // ok to use the fact here + * x = null + * } + * ``` + * + * Another example: + * ```scala + * var x: String|Null = ??? + * if (x != null) { + * def f: String = { + * val y: String = x // error: the use of x is out of order + * y + * } + * x = null + * val y: String = f // danger + * } + * ``` + */ + def usedOutOfOrder(given Context): Boolean = + val refSym = ref.symbol + val refOwner = refSym.owner + + @tailrec def recur(s: Symbol): Boolean = + s != NoSymbol + && s != refOwner + && (s.isOneOf(Lazy | Method) // not at the rhs of lazy ValDef or in a method (or lambda) + || s.isClass // not in a class + // TODO: need to check by-name paramter + || recur(s.owner)) + + refSym.is(Mutable) // if it is immutable, we don't need to check the rest conditions + && refOwner.isTerm + && recur(curCtx.owner) + given treeOps: extension (tree: Tree) with /* The `tree` with added nullability attachment */ @@ -254,7 +329,18 @@ object Nullables with given assignOps: extension (tree: Assign) with def computeAssignNullable()(given Context): tree.type = tree.lhs match case TrackedRef(ref) => - tree.withNotNullInfo(NotNullInfo(Set(), Set(ref))) // TODO: refine with nullability type info + val rhstp = tree.rhs.typeOpt + if curCtx.explicitNulls && ref.isNullableUnion then + if rhstp.isNullType || rhstp.isNullableUnion then + // If the type of rhs is nullable (`T|Null` or `Null`), then the nullability of the + // lhs variable is no longer trackable. We don't need to check whether the type `T` + // is correct here, as typer will check it. + tree.withNotNullInfo(NotNullInfo(Set(), Set(ref))) + else + // If the initial type is nullable and the assigned value is non-null, + // we add it to the NotNull. + tree.withNotNullInfo(NotNullInfo(Set(ref), Set())) + else tree case _ => tree private val analyzedOps = Set(nme.EQ, nme.NE, nme.eq, nme.ne, nme.ZAND, nme.ZOR, nme.UNARY_!) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 74a8c350bdab..b795ab296467 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -41,6 +41,7 @@ import transform.SymUtils._ import transform.TypeUtils._ import reporting.trace import Nullables.{NotNullInfo, given} +import NullOpsDecorator._ object Typer { @@ -347,6 +348,24 @@ class Typer extends Namer findRefRecur(NoType, BindingPrec.NothingBound, NoContext) } + /** If `tree`'s type is a `TermRef` identified by flow typing to be non-null, then + * cast away `tree`s nullability. Otherwise, `tree` remains unchanged. + * + * Example: + * If x is a trackable reference and we know x is not null at this point, + * (x: T | Null) => x.$asInstanceOf$[x.type & T] + */ + def toNotNullTermRef(tree: Tree, pt: Type)(implicit ctx: Context): Tree = tree.tpe match + case ref @ OrNull(tpnn) : TermRef + if pt != AssignProto && // Ensure it is not the lhs of Assign + ctx.notNullInfos.impliesNotNull(ref) && + // If a reference is in the context, it is already trackable at the point we add it. + // Hence, we don't use isTracked in the next line, because checking use out of order is enough. + !ref.usedOutOfOrder => + tree.select(defn.Any_typeCast).appliedToType(AndType(ref, tpnn)) + case _ => + tree + /** Attribute an identifier consisting of a simple name or wildcard * * @param tree The tree representing the identifier. @@ -417,7 +436,9 @@ class Typer extends Namer tree.withType(ownType) } - checkStableIdentPattern(tree1, pt) + val tree2 = toNotNullTermRef(tree1, pt) + + checkStableIdentPattern(tree2, pt) } /** Check that a stable identifier pattern is indeed stable (SLS 8.1.5) @@ -442,8 +463,11 @@ class Typer extends Namer case qual => if (tree.name.isTypeName) checkStable(qual.tpe, qual.sourcePos) val select = assignType(cpy.Select(tree)(qual, tree.name), qual) - if (select.tpe ne TryDynamicCallType) ConstFold(checkStableIdentPattern(select, pt)) - else if (pt.isInstanceOf[FunOrPolyProto] || pt == AssignProto) select + + val select1 = toNotNullTermRef(select, pt) + + if (select1.tpe ne TryDynamicCallType) ConstFold(checkStableIdentPattern(select1, pt)) + else if (pt.isInstanceOf[FunOrPolyProto] || pt == AssignProto) select1 else typedDynamicSelect(tree, Nil, pt) } @@ -1556,16 +1580,6 @@ class Typer extends Namer typed(annot, defn.AnnotationClass.typeRef) def typedValDef(vdef: untpd.ValDef, sym: Symbol)(implicit ctx: Context): Tree = { - sym.infoOrCompleter match - case completer: Namer#Completer - if completer.creationContext.notNullInfos ne ctx.notNullInfos => - // The RHS of a val def should know about not null facts established - // in preceding statements (unless the ValDef is completed ahead of time, - // then it is impossible). - vdef.symbol.info = Completer(completer.original)( - given completer.creationContext.withNotNullInfos(ctx.notNullInfos)) - case _ => - val ValDef(name, tpt, _) = vdef completeAnnotations(vdef, sym) if (sym.isOneOf(GivenOrImplicit)) checkImplicitConversionDefOK(sym) @@ -2222,14 +2236,9 @@ class Typer extends Namer case Some(xtree) => traverse(xtree :: rest) case none => - val defCtx = mdef match - // Keep preceding not null facts in the current context only if `mdef` - // cannot be executed out-of-sequence. - case _: ValDef if !mdef.mods.is(Lazy) && ctx.owner.isTerm => - ctx // all preceding statements will have been executed in this case - case _ => - ctx.withNotNullInfos(initialNotNullInfos) - typed(mdef)(given defCtx) match { + val newCtx = if (ctx.owner.isTerm && adaptCreationContext(mdef)) ctx + else ctx.withNotNullInfos(initialNotNullInfos) + typed(mdef)(given newCtx) match { case mdef1: DefDef if !Inliner.bodyToInline(mdef1.symbol).isEmpty => buf += inlineExpansion(mdef1) // replace body with expansion, because it will be used as inlined body @@ -2279,6 +2288,34 @@ class Typer extends Namer (stats1, finalCtx) } + /** Tries to adapt NotNullInfos from creation context to the DefTree, + * returns whether the adaption took place. An adaption only takes place if the + * DefTree has a symbol and it has not been completed (is not forward referenced). + */ + def adaptCreationContext(mdef: untpd.DefTree)(implicit ctx: Context): Boolean = + // Keep preceding not null facts in the current context only if `mdef` + // cannot be executed out-of-sequence. + // We have to check the Completer of symbol befor typedValDef, + // otherwise the symbol is already completed using creation context. + mdef.getAttachment(SymOfTree) match { + case Some(sym) => sym.infoOrCompleter match { + case completer: Namer#Completer => + if (completer.creationContext.notNullInfos ne ctx.notNullInfos) + // The RHS of a val def should know about not null facts established + // in preceding statements (unless the DefTree is completed ahead of time, + // then it is impossible). + sym.info = Completer(completer.original)( + given completer.creationContext.withNotNullInfos(ctx.notNullInfos)) + true + case _ => + // If it has been completed, then it must be because there is a forward reference + // to the definition in the program. Hence, we don't Keep preceding not null facts + // in the current context. + false + } + case _ => false + } + /** Given an inline method `mdef`, the method rewritten so that its body * uses accessors to access non-public members. * Overwritten in Retyper to return `mdef` unchanged. diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index cf50445a170b..5b2c86f2bc09 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -57,7 +57,7 @@ class CompilationTests extends ParallelTesting { compileFile("tests/pos-special/typeclass-scaling.scala", defaultOptions.and("-Xmax-inlines", "40")), compileFile("tests/pos-special/indent-colons.scala", defaultOptions.and("-Yindent-colons")), compileFile("tests/pos-special/i7296.scala", defaultOptions.and("-strict", "-deprecation", "-Xfatal-warnings")), - compileFile("tests/pos-special/nullable.scala", defaultOptions.and("-Yexplicit-nulls")), + compileFile("tests/pos-special/notNull.scala", defaultOptions.and("-Yexplicit-nulls")), compileDir("tests/pos-special/adhoc-extension", defaultOptions.and("-strict", "-feature", "-Xfatal-warnings")), compileFile("tests/pos-special/i7575.scala", defaultOptions.and("-language:dynamics")), ).checkCompile() @@ -257,6 +257,28 @@ class CompilationTests extends ParallelTesting { tests.foreach(_.delete()) } + + // Explicit nulls tests + @Test def explicitNullsNeg: Unit = { + implicit val testGroup: TestGroup = TestGroup("explicitNullsNeg") + aggregateTests( + compileFilesInDir("tests/explicit-nulls/neg", explicitNullsOptions), + compileFilesInDir("tests/explicit-nulls/neg-patmat", explicitNullsOptions and "-Xfatal-warnings") + ) + }.checkExpectedErrors() + + @Test def explicitNullsPos: Unit = { + implicit val testGroup: TestGroup = TestGroup("explicitNullsPos") + aggregateTests( + compileFilesInDir("tests/explicit-nulls/pos", explicitNullsOptions), + compileFilesInDir("tests/explicit-nulls/pos-separate", explicitNullsOptions) + ) + }.checkCompile() + + @Test def explicitNullsRun: Unit = { + implicit val testGroup: TestGroup = TestGroup("explicitNullsRun") + compileFilesInDir("tests/explicit-nulls/run", explicitNullsOptions) + }.checkRuns() } object CompilationTests { diff --git a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala index 931f56d21048..fdcd4f83f44f 100644 --- a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala +++ b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala @@ -67,4 +67,7 @@ object TestConfiguration { val scala2CompatMode = defaultOptions and "-language:Scala2Compat" val explicitUTF8 = defaultOptions and ("-encoding", "UTF8") val explicitUTF16 = defaultOptions and ("-encoding", "UTF16") + + /** Enables explicit nulls */ + val explicitNullsOptions = defaultOptions and "-Yexplicit-nulls" } diff --git a/docs/docs/internals/explicit-nulls.md b/docs/docs/internals/explicit-nulls.md new file mode 100644 index 000000000000..73d7fb9e7178 --- /dev/null +++ b/docs/docs/internals/explicit-nulls.md @@ -0,0 +1,141 @@ +--- +layout: doc-page +title: "Explicit Nulls" +--- + +The explicit nulls feature (enabled via a flag) changes the Scala type hierarchy +so that reference types (e.g. `String`) are non-nullable. We can still express nullability +with union types: e.g. `val x: String|Null = null`. + +The implementation of the feature in dotty can be conceptually divided in several parts: + 1. changes to the type hierarchy so that `Null` is only a subtype of `Any` + 2. a "translation layer" for Java interop that exposes the nullability in Java APIs + 3. a "magic" `JavaNull` type (an alias for `Null`) that is recognized by the compiler and + allows unsound member selections (trading soundness for usability) + +## Feature Flag + +Explicit nulls are disabled by default. They can be enabled via `-Yexplicit-nulls` defined in +`ScalaSettings.scala`. All of the explicit-nulls-related changes should be gated behind the flag. + +## Type Hierarchy + +We change the type hierarchy so that `Null` is only a subtype of `Any` by: + - modifying the notion of what is a nullable class (`isNullableClass`) in `SymDenotations` + to include _only_ `Null` and `Any` + - changing the parent of `Null` in `Definitions` to point to `Any` and not `AnyRef` + - changing `isBottomType` and `isBottomClass` in `Definitions` + +## Java Interop + +The problem we're trying to solve here is: if we see a Java method `String foo(String)`, +what should that method look like to Scala? + - since we should be able to pass `null` into Java methods, the argument type should be `String|JavaNull` + - since Java methods might return `null`, the return type should be `String|JavaNull` + +`JavaNull` here is a type alias for `Null` with "magic" properties (see below). + +At a high-level: + - we track the loading of Java fields and methods as they're loaded by the compiler + - we do this in two places: `Namer` (for Java sources) and `ClassFileParser` (for bytecode) + - whenever we load a Java member, we "nullify" its argument and return types + +The nullification logic lives in `compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala`. + +The entry point is the function +`def nullifyMember(sym: Symbol, tp: Type, isEnumValueDef: Boolean)(implicit ctx: Context): Type` +which, given a symbol, its "regular" type, and a boolean whether it is a Enum value definition, +produces what the type of the symbol should be in the explicit nulls world. + +1. If the symbol is a Enum value definition or a `TYPE_` field, we don't nullify the type +2. If it is `toString()` method or the constructor, or it has a `@NotNull` annotation, + we nullify the type, without a `JavaNull` at the outmost level. +3. Otherwise, we nullify the type in regular way. + +See `JavaNullMap` in `JavaNullInterop.scala` for more details about how we nullify different types. + +## JavaNull + +`JavaNull` is just an alias for `Null`, but with magic power. `JavaNull`'s magic (anti-)power is that +it's unsound. + +```scala +val s: String|JavaNull = "hello" +s.length // allowed, but might throw NPE +``` + +`JavaNull` is defined as `JavaNullAlias` in `Definitions.scala`. +The logic to allow member selections is defined in `findMember` in `Types.scala`: + - if we're finding a member in a type union + - and the union contains `JavaNull` on the r.h.s. after normalization (see below) + - then we can continue with `findMember` on the l.h.s of the union (as opposed to failing) + +## Working with Nullable Unions + +Within `Types.scala`, we defined some extractors to work with nullable unions: +`OrNull` and `OrJavaNull`. + +```scala +(tp: Type) match { + case OrNull(tp1) => // if tp is a nullable union: tp1 | Null + case _ => // otherwise +} +``` + +These extractor will call utility methods in `NullOpsDecorator.scala`. All of these +are methods of the `Type` class, so call them with `this` as a receiver: + +- `stripNull` syntactically strips all `Null` types in the union: + e.g. `String|Null => String`. +- `stripJavaNull` is like `stripNull` but only removes `JavaNull` from the union. + This is needed when we want to "revert" the Java nullification function. +- `stripAllJavaNull` collapses all `JavaNull` unions within this type, and not just the outermost + ones (as `stripJavaNull` does). +- `isNullableUnion` determines whether `this` is a nullable union. +- `isJavaNullableUnion` determines whether `this` is syntactically a union of the form + `T|JavaNull`. + +## Flow Typing + +As typing happens, we accumulate a set of `NotNullInfo`s in the `Context` (see +`Contexts.scala`). A `NotNullInfo` contains the set of `TermRef`s that are known to +be non-null at the current program point. See `Nullables.scala` for how `NotNullInfo`s +are computed. + +During type-checking, when we type an identity or a select tree (in `typedIdent` and +`typedSelect`), we will call `toNotNullTermRef` on the tree before return the typed tree. +If the tree `x` has nullable type `T|Null` and it is known to be not null according to +the `NotNullInfo` and it is not on the lhs of assignment, then we cast it to `x.type & T` +using `defn.Any_typeCast`. + +The reason for casting to `x.type & T`, as opposed to just `T`, is that it allows us to +support flow typing for paths of length greater than one. + +```scala +abstract class Node { + val x: String + val next: Node | Null +} + +def f = { + val l: Node|Null = ??? + if (l != null && l.next != null) { + val third: l.next.next.type = l.next.next + } +} +``` + +After typing, `f` becomes: + +```scala +def f = { + val l: Node|Null = ??? + if (l != null && l.$asInstanceOf$[l.type & Node].next != null) { + val third: + l.$asInstanceOf$[l.type & Node].next.$asInstanceOf$[(l.type & Node).next.type & Node].next.type = + l.$asInstanceOf$[l.type & Node].next.$asInstanceOf$[(l.type & Node).next.type & Node].next + } +} +``` +Notice that in the example above `(l.type & Node).next.type & Node` is still a stable path, so +we can use it in the type and track it for flow typing. diff --git a/docs/docs/reference/other-new-features/explicit-nulls.md b/docs/docs/reference/other-new-features/explicit-nulls.md new file mode 100644 index 000000000000..907b6d71835f --- /dev/null +++ b/docs/docs/reference/other-new-features/explicit-nulls.md @@ -0,0 +1,458 @@ +--- +layout: doc-page +title: "Explicit Nulls" +--- + +Explicit nulls is an opt-in feature that modifies the Scala type system, which makes reference types +(anything that extends `AnyRef`) _non-nullable_. + +This means the following code will no longer typecheck: +``` +val x: String = null // error: found `Null`, but required `String` +``` + +Instead, to mark a type as nullable we use a [type union](https://dotty.epfl.ch/docs/reference/new-types/union-types.html) + +``` +val x: String|Null = null // ok +``` + +Explicit nulls are enabled via a `-Yexplicit-nulls` flag, so they're an opt-in feature. + +Read on for details. + +## New Type Hierarchy + +When explicit nulls are enabled, the type hierarchy changes so that `Null` is subtype only of +`Any`, as opposed to every reference type. + +This is the new type hierarchy: +![](../../images/explicit-nulls/explicit-nulls-type-hierarchy.png "Type Hierarchy for Explicit Nulls") + +After erasure, `Null` remains a subtype of all reference types (as forced by the JVM). + +## Unsoundness + +The new type system is unsound with respect to `null`. This means there are still instances where an expressions has a non-nullable type like `String`, but its value is `null`. + +The unsoundness happens because uninitialized fields in a class start out as `null`: +```scala +class C { + val f: String = foo(f) + def foo(f2: String): String = if (f2 == null) "field is null" else f2 +} +val c = new C() +// c.f == "field is null" +``` + +Enforcing sound initialization is a non-goal of this proposal. However, once we have a type +system where nullability is explicit, we can use a sound initialization scheme like the one +proposed by @liufengyun and @biboudis in [https://github.com/lampepfl/dotty/pull/4543](https://github.com/lampepfl/dotty/pull/4543) to eliminate this particular source of unsoundness. + +## Equality + +We don't allow the double-equal (`==` and `!=`) and reference (`eq` and `ne`) comparison between +`AnyRef` and `Null` anymore, since a variable with non-nullable type shouldn't have null value. +`null` can only be compared with `Null`, nullable union (`T | Null`), or `Any` type. + +For some reason, if we really want to compare `null` with non-null values, we can use cast. + +```scala +val x: String = ??? +val y: String | Null = ??? + +x == null // error: Values of types String and Null cannot be compared with == or != +x eq null // error +"hello" == null // error + +y == null // ok +y == x // ok + +(x: String | Null) == null // ok +(x: Any) == null // ok +``` + +## Working with Null + +To make working with nullable values easier, we propose adding a few utilities to the standard library. +So far, we have found the following useful: + + - An extension method `.nn` to "cast away" nullability + + ```scala + def[T] (x: T|Null) nn: x.type & T = + if (x == null) throw new NullPointerException("tried to cast away nullability, but value is null") + else x.asInstanceOf[x.type & T] + ``` + + This means that given `x: String|Null`, `x.nn` has type `String`, so we can call all the + usual methods on it. Of course, `x.nn` will throw a NPE if `x` is `null`. + + Don't use `.nn` on mutable variables directly, which may introduce unknown value into the type. + +## Java Interop + +The compiler can load Java classes in two ways: from source or from bytecode. In either case, +when a Java class is loaded, we "patch" the type of its members to reflect that Java types +remain implicitly nullable. + +Specifically, we patch +* the type of fields +* the argument type and return type of methods + +`JavaNull` is an alias for `Null` with magic properties (see below). We illustrate the rules with following examples: + + * The first two rules are easy: we nullify reference types but not value types. + + ```java + class C { + String s; + int x; + } + ``` + ==> + ```scala + class C { + val s: String|JavaNull + val x: Int + } + ``` + + * We nullify type parameters because in Java a type parameter is always nullable, so the following code compiles. + + ```java + class C { T foo() { return null; } } + ``` + ==> + ```scala + class C[T] { def foo(): T|JavaNull } + ``` + + Notice this is rule is sometimes too conservative, as witnessed by + + ```scala + class InScala { + val c: C[Bool] = ??? // C as above + val b: Bool = c.foo() // no longer typechecks, since foo now returns Bool|Null + } + ``` + + * This reduces the number of redundant nullable types we need to add. Consider + + ```java + class Box { T get(); } + class BoxFactory { Box makeBox(); } + ``` + ==> + ```scala + class Box[T] { def get(): T|JavaNull } + class BoxFactory[T] { def makeBox(): Box[T]|JavaNull } + ``` + + Suppose we have a `BoxFactory[String]`. Notice that calling `makeBox()` on it returns a + `Box[String]|JavaNull`, not a `Box[String|JavaNull]|JavaNull`. This seems at first + glance unsound ("What if the box itself has `null` inside?"), but is sound because calling + `get()` on a `Box[String]` returns a `String|JavaNull`. + + Notice that we need to patch _all_ Java-defined classes that transitively appear in the + argument or return type of a field or method accessible from the Scala code being compiled. + Absent crazy reflection magic, we think that all such Java classes _must_ be visible to + the Typer in the first place, so they will be patched. + + * We will append `JavaNull` to the type arguments if the generic class is defined in Scala. + + ```java + class BoxFactory { + Box makeBox(); // Box is Scala-defined + List>> makeCrazyBoxes(); // List is Java-defined + } + ``` + ==> + ```scala + class BoxFactory[T] { + def makeBox(): Box[T | JavaNull] | JavaNull + def makeCrazyBoxes(): List[Box[List[T] | JavaNull]] | JavaNull + } + ``` + + In this case, since `Box` is Scala-defined, and we will get `Box[T|JavaNull]|JavaNull`. + This is needed because our nullability function is only applied (modularly) to the Java + classes, but not to the Scala ones, so we need a way to tell `Box` that it contains a + nullable value. + + The `List` is Java-defined, so we don't append `JavaNull` to its type argument. But we + still need to nullify its inside. + + * We don't nullify _simple_ literal constant (`final`) fields, since they are known to be non-null + + ```java + class Constants { + final String NAME = "name"; + final int AGE = 0; + final char CHAR = 'a'; + + final String NAME_GENERATED = getNewName(); + } + ``` + ==> + ```scala + class Constants { + val NAME: String("name") = "name" + val AGE: Int(0) = 0 + val CHAR: Char('a') = 'a' + + val NAME_GENERATED: String | Null = ??? + } + ``` + + * We don't append `JavaNull` to a field and the return type of a method which is annotated with a + `NotNull` annotation. + + ```java + class C { + @NotNull String name; + @NotNull List getNames(String prefix); // List is Java-defined + @NotNull Box getBoxedName(); // Box is Scala-defined + } + ``` + ==> + ```scala + class C { + val name: String + def getNames(prefix: String | JavaNull): List[String] // we still need to nullify the paramter types + def getBoxedName(): Box[String | JavaNull] // we don't append `JavaNull` to the outmost level, but we still need to nullify inside + } + ``` + + The annotation must be from the list below to be recognized as NotNull by the compiler. + Check `Definitions.scala` for an updated list. + + ```scala + // A list of annotations that are commonly used to indicate that a field/method argument or return + // type is not null. These annotations are used by the nullification logic in JavaNullInterop to + // improve the precision of type nullification. + // We don't require that any of these annotations be present in the class path, but we want to + // create Symbols for the ones that are present, so they can be checked during nullification. + @tu lazy val NotNullAnnots: List[ClassSymbol] = ctx.getClassesIfDefined( + "javax.annotation.Nonnull" :: + "edu.umd.cs.findbugs.annotations.NonNull" :: + "androidx.annotation.NonNull" :: + "android.support.annotation.NonNull" :: + "android.annotation.NonNull" :: + "com.android.annotations.NonNull" :: + "org.eclipse.jdt.annotation.NonNull" :: + "org.checkerframework.checker.nullness.qual.NonNull" :: + "org.checkerframework.checker.nullness.compatqual.NonNullDecl" :: + "org.jetbrains.annotations.NotNull" :: + "lombok.NonNull" :: + "io.reactivex.annotations.NonNull" :: Nil map PreNamedString) + ``` + +### JavaNull + +To enable method chaining on Java-returned values, we have the special type alias for `Null`: + +```scala +type JavaNull = Null +``` + +`JavaNull` behaves just like `Null`, except it allows (unsound) member selections: + +```scala +// Assume someJavaMethod()'s original Java signature is +// String someJavaMethod() {} +val s2: String = someJavaMethod().trim().substring(2).toLowerCase() // unsound +``` + +Here, all of `trim`, `substring` and `toLowerCase` return a `String|JavaNull`. +The Typer notices the `JavaNull` and allows the member selection to go through. +However, if `someJavaMethod` were to return `null`, then the first member selection +would throw a `NPE`. + +Without `JavaNull`, the chaining becomes too cumbersome + +```scala +val ret = someJavaMethod() +val s2 = if (ret != null) { + val tmp = ret.trim() + if (tmp != null) { + val tmp2 = tmp.substring(2) + if (tmp2 != null) { + tmp2.toLowerCase() + } + } +} +// Additionally, we need to handle the `else` branches. +``` + +## Flow Typing + +We added a simple form of flow-sensitive type inference. The idea is that if `p` is a +stable path or a trackable variable, then we can know that `p` is non-null if it's compared +with the `null`. This information can then be propagated to the `then` and `else` branches +of an if-statement (among other places). + +Example: + +```scala +val s: String|Null = ??? +if (s != null) { + // s: String +} +// s: String|Null + +assert(x != null) +// s: String +``` + +A similar inference can be made for the `else` case if the test is `p == null` + +```scala +if (s == null) { + // s: String|Null +} else { + // s: String +} +``` + +`==` and `!=` is considered a comparison for the purposes of the flow inference. + +### Logical Operators + +We also support logical operators (`&&`, `||`, and `!`): + +```scala +val s: String|Null = ??? +val s2: String|Null = ??? +if (s != null && s2 != null) { + // s: String + // s2: String +} + +if (s == null || s2 == null) { + // s: String|Null + // s2: String|Null +} else { + // s: String + // s2: String +} +``` + +### Inside Conditions + +We also support type specialization _within_ the condition, taking into account that `&&` and `||` are short-circuiting: + +```scala +val s: String|Null = ??? + +if (s != null && s.length > 0) { // s: String in `s.length > 0` + // s: String +} + +if (s == null || s.length > 0) // s: String in `s.length > 0` { + // s: String|Null +} else { + // s: String|Null +} +``` + +### Match Case + +The non-null cases can be detected in match statements. + +```scala +val s: String|Null = ??? + +s match { + case _: String => // s: String + case _ => +} +``` + +### Mutable Variable + +We are able to detect the nullability of some local mutable variables. A simple example is: + +```scala +class C(val x: Int, val next: C|Null) + +var xs: C|Null = C(1, C(2, null)) +// xs is trackable, since all assignments are in the same mathod +while (xs != null) { + // xs: C + val xsx: Int = xs.x + val xscpy: C = xs + xs = xscpy // since xscpy is non-null, xs still has type C after this line + // xs: C + xs = xs.next // after this assignment, xs can be null again + // xs: C | Null +} +``` + +When dealing with local mutable variables, there are two questions: + +1. Whether to track a local mutable variable during flow typing. + We track a local mutable variable iff the variable is not assigned in a closure. + For example, in the following code `x` is assigned to by the closure `y`, so we do not + do flow typing on `x`. + + ```scala + var x: String|Null = ??? + def y = { + x = null + } + if (x != null) { + // y can be called here, which break the fact + val a: String = x // error: x is captured and mutated by the closure, not trackable + } + ``` + +2. Whether to generate and use flow typing on a specific _use_ of a local mutable variable. + We only want to do flow typing on a use that belongs to the same method as the definition + of the local variable. + For example, in the following code, even `x` is not assigned to by a closure, but we can only + use flow typing in one of the occurrences (because the other occurrence happens within a nested + closure). + + ```scala + var x: String|Null = ??? + def y = { + if (x != null) { + // not safe to use the fact (x != null) here + // since y can be executed at the same time as the outer block + val _: String = x + } + } + if (x != null) { + val a: String = x // ok to use the fact here + x = null + } + ``` + +See more examples in `tests/explicit-nulls/neg/var-ref-in-closure.scala`. + +Currently, we are unable to track paths with a mutable variable prefix. +For example, `x.a` if `x` is mutable. + +### Unsupported Idioms + +We don't support: + +- flow facts not related to nullability (`if (x == 0) { // x: 0.type not inferred }`) +- tracking aliasing between non-nullable paths + ```scala + val s: String|Null = ??? + val s2: String|Null = ??? + if (s != null && s == s2) { + // s: String inferred + // s2: String not inferred + } + ``` + +## Binary Compatibility + +Our strategy for binary compatibility with Scala binaries that predate explicit nulls +and new libraries compiled without `-Yexplicit-nulls` is to leave the types unchanged +and be compatible but unsound. + +[More details](../../internals/explicit-nulls.md) diff --git a/docs/images/explicit-nulls/explicit-nulls-type-hierarchy.png b/docs/images/explicit-nulls/explicit-nulls-type-hierarchy.png new file mode 100644 index 000000000000..65179260c246 Binary files /dev/null and b/docs/images/explicit-nulls/explicit-nulls-type-hierarchy.png differ diff --git a/library/src/dotty/DottyPredef.scala b/library/src/dotty/DottyPredef.scala index c934bf0ddd67..69040aef4961 100644 --- a/library/src/dotty/DottyPredef.scala +++ b/library/src/dotty/DottyPredef.scala @@ -38,4 +38,17 @@ object DottyPredef { } inline def summon[T](given x: T): x.type = x + + // Extension methods for working with explicit nulls + + /** Strips away the nullability from a value. + * e.g. + * val s1: String|Null = "hello" + * val s: String = s1.nn + * + * Note that `.nn` performs a checked cast, so if invoked on a null value it'll throw an NPE. + */ + def[T] (x: T|Null) nn: x.type & T = + if (x == null) throw new NullPointerException("tried to cast away nullability, but value is null") + else x.asInstanceOf[x.type & T] } diff --git a/tests/explicit-nulls/neg-patmat/patmat1.scala b/tests/explicit-nulls/neg-patmat/patmat1.scala new file mode 100644 index 000000000000..6e9710a56dec --- /dev/null +++ b/tests/explicit-nulls/neg-patmat/patmat1.scala @@ -0,0 +1,38 @@ + +class Foo { + val s: String = ??? + s match { + case s: String => 100 // warning: type test will always succeed + case _ => 200 // error: unreachable + } + + s match { + case s: String => 100 // warning: type test will always succeed + case _ => 200 // error: unreachable + } + + sealed trait Animal + case class Dog(name: String) extends Animal + case object Cat extends Animal + + val a: Animal = ??? + a match { + case Dog(name) => 100 + case Cat => 200 + case _ => 300 // error: unreachable + } + + val a2: Animal|Null = ??? + a2 match { + case Dog(_) => 100 + case Cat => 200 + case _ => 300 + } + + val a3: Animal|Null = ??? + a3 match { + case Dog(_) => 100 + case Cat => 200 + case null => 300 // ok + } +} diff --git a/tests/explicit-nulls/neg/after-assign.scala b/tests/explicit-nulls/neg/after-assign.scala new file mode 100644 index 000000000000..79b7d675f3a5 --- /dev/null +++ b/tests/explicit-nulls/neg/after-assign.scala @@ -0,0 +1,13 @@ +class C(val x: Int, val next: C|Null) + +def f = { + var xs: C|Null = C(1, C(2, null)) + while (xs != null) { + val xsx: Int = xs.x + val xscpy: C = xs + xs = xscpy // Since xscpy is non-nullable, after the assign, xs is still non-nullable + val xscpyx: Int = xscpy.x + xs = xs.next // xs.next is nullable, after the assign, xs becomes nullable + val xsnx: Int = xs.x // error + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/neg/alias.scala b/tests/explicit-nulls/neg/alias.scala new file mode 100644 index 000000000000..f8dea4864027 --- /dev/null +++ b/tests/explicit-nulls/neg/alias.scala @@ -0,0 +1,24 @@ + +// Test that nullability is correctly detected +// in the presence of a type alias. +class Base { + type T >: Null <: AnyRef|Null +} + +object foo { + class Foo { + val length: Int = 42 + def doFoo(): Unit = () + } +} + +class Derived extends Base { + type Nullable[X] = X|Null + type Foo = Nullable[foo.Foo] + + def fun(foo: Foo): Unit = { + foo.length // error: foo is nullable + foo.doFoo() // error: foo is nullable + } +} + diff --git a/tests/explicit-nulls/neg/basic.scala b/tests/explicit-nulls/neg/basic.scala new file mode 100644 index 000000000000..7c652887590b --- /dev/null +++ b/tests/explicit-nulls/neg/basic.scala @@ -0,0 +1,11 @@ +// Test that reference types are no longer nullable. + +class Foo { + val s: String = null // error + val s1: String|Null = null // ok + val b: Boolean = null // error + val ar: AnyRef = null // error + val a: Any = null // ok + val n: Null = null // ok +} + diff --git a/tests/explicit-nulls/neg/default.scala b/tests/explicit-nulls/neg/default.scala new file mode 100644 index 000000000000..fe115861e926 --- /dev/null +++ b/tests/explicit-nulls/neg/default.scala @@ -0,0 +1,13 @@ + +class Foo { + val x: String = null // error: String is non-nullable + + def foo(x: String): String = "x" + + val y = foo(null) // error: String argument is non-nullable + + val z: String = foo("hello") + + class Bar + val b: Bar = null // error: user-created classes are also non-nullable +} diff --git a/tests/explicit-nulls/neg/eq.scala b/tests/explicit-nulls/neg/eq.scala new file mode 100644 index 000000000000..3fab8ba4bf8a --- /dev/null +++ b/tests/explicit-nulls/neg/eq.scala @@ -0,0 +1,44 @@ +// Test what can be compared for equality against null. +class Foo { + // Null itself + val x0: Null = null + x0 != x0 + x0 == null + x0 != null + null == x0 + null == null + null != null + + // Non-nullable types: error + val x1: String = "hello" + x1 != null // error + x1 == null // error + null == x1 // error + null != x1 // error + x1 == x0 // error + x0 != x1 // error + x1.asInstanceOf[String|Null] == null + x1.asInstanceOf[String|Null] == x0 + x1.asInstanceOf[Any] == null + x1.asInstanceOf[Any] == x0 + + // Nullable types: OK + val x2: String|Null = null + x2 == null + null == x2 + x2 == x0 + x2 != x0 + x0 == x2 + x2 == x1 + x2 != x1 + x1 == x2 + + // Value types: not allowed. + 1 == null // error + null != 0 // error + null == 0 // error + true == null // error + null == false // error + 'a' == null // error + null == 'b' // error +} diff --git a/tests/explicit-nulls/neg/eq2.scala b/tests/explicit-nulls/neg/eq2.scala new file mode 100644 index 000000000000..8c730407daa4 --- /dev/null +++ b/tests/explicit-nulls/neg/eq2.scala @@ -0,0 +1,18 @@ +// Test that we can't compare for equality `null` and +// classes that derive from AnyVal. +class Foo(x: Int) extends AnyVal + +class Bar { + val foo: Foo = new Foo(15) + if (foo == null) {} // error: Values of types Null and Foo cannot be compared + if (null == foo) {} // error + if (foo != null) {} // error + if (null != foo) {} // error + + // To test against null, make the type nullable. + val foo2: Foo|Null = foo + if (foo2 == null) {} + if (null == foo2) {} + if (foo2 != null) {} + if (null != foo2) {} +} diff --git a/tests/explicit-nulls/neg/erasure.scala b/tests/explicit-nulls/neg/erasure.scala new file mode 100644 index 000000000000..da896a0aa427 --- /dev/null +++ b/tests/explicit-nulls/neg/erasure.scala @@ -0,0 +1,6 @@ +// Check that T|Null is erased to T if T is a reference type. + +trait Foo { + def foo(s: String|Null): Unit + def foo(s: String): Unit // error: collision after erasure +} diff --git a/tests/explicit-nulls/neg/flow-conservative.scala b/tests/explicit-nulls/neg/flow-conservative.scala new file mode 100644 index 000000000000..4ed5713a08f8 --- /dev/null +++ b/tests/explicit-nulls/neg/flow-conservative.scala @@ -0,0 +1,55 @@ + +// Show that the static analysis behind flow typing is conservative. + +class Test { + + val x: String|Null = ??? + + // Why is the then branch ok, but the else problematic? + // The problem is that we're computing a "must not be null analysis". + // So we know that + // 1) if the condition x == null && x != null, then both sides of the + // and must be true. Then it must be the case that x != null, so we + // know that x cannot be null and x.length is allowed. + // Of course, the then branch will never execute, but the analysis doesn't + // know (so it's ok to say that x won't be null). + // 2) if the condition is false, then we only know that _one_ or more + // of the operands failed, but we don't know _which_. + // This means that we can only pick the flow facts that hold for _both_ + // operands. In particular, we look at x == null, and see that if the condition + // is false, then x must _not_ be null. But then we look at what happens if + // x != null is false, and we can't conclude that any variables must be non-null. + // When we intersect the two sets {x} and \empty, we get the empty set, which + // correctly approximates reality, which is that we can get to the else branch + // regardless of whether x is null. + + if (x == null && x != null) { + val y = x.length // ok + } else { + val y = x.length // error + } + + // Next we show how strengthening the condition can backfire in an + // unintuitive way. + if (x != null && 1 == 1) { + val y = x.length // ok + } + + if (x == null) { + } else { + val y = x.length // ok + } + + // But + if (x == null && 1 == 1) { // logically equivalent to `x == null`, but the + // analysis doesn't known + } else { + val y = x.length // error + } + + // The problem here is the same. If the condition is false + // then we know the l.h.s implies that x must not be null. + // But the r.h.s doesn't tell us anything about x, so we can't + // assume that x is non-null. Then the fact that x is non-null can't + // be propagated to the else branch. +} diff --git a/tests/explicit-nulls/neg/flow-implicitly.scala b/tests/explicit-nulls/neg/flow-implicitly.scala new file mode 100644 index 000000000000..33934cf1f70f --- /dev/null +++ b/tests/explicit-nulls/neg/flow-implicitly.scala @@ -0,0 +1,10 @@ + +// Test that flow typing works well with implicit resolution. +class Test { + implicit val x: String | Null = ??? + implicitly[x.type <:< String] // error: x.type is widened String|Null + + if (x != null) { + implicitly[x.type <:< String] // ok: x.type is widened to String + } +} diff --git a/tests/explicit-nulls/neg/flow.scala b/tests/explicit-nulls/neg/flow.scala new file mode 100644 index 000000000000..0feb037210c7 --- /dev/null +++ b/tests/explicit-nulls/neg/flow.scala @@ -0,0 +1,182 @@ + +// Flow-sensitive type inference +class Foo { + + def basic() = { + class Bar { + val s: String = ??? + } + + // Basic + val b: Bar|Null = ??? + if (b != null) { + val s = b.s // ok: type of `b` inferred as `Bar` + val s2: Bar = b + } else { + val s = b.s // error: `b` is `Bar|Null` + } + val s = b.s // error: `b` is `Bar|Null` + } + + def notStable() = { + class Bar { + var s: String = ??? + } + + var b2: Bar|Null = ??? + if (b2 != null) { + val s = b2.s + } + } + + def nested() = { + class Bar2 { + val x: Bar2|Null = ??? + } + + val bar2: Bar2|Null = ??? + if (bar2 != null) { + if (bar2.x != null) { + if (bar2.x.x != null) { + if (bar2.x.x.x != null) { + val b2: Bar2 = bar2.x.x.x + } + val b2: Bar2 = bar2.x.x + val b2err: Bar2 = bar2.x.x.x // error: expected Bar2 but got Bar2|Null + } + val b2: Bar2 = bar2.x + } + val b2: Bar2 = bar2 + } + } + + def ifThenElse() = { + val s: String|Null = ??? + if (s == null) { + } else { + val len: Int = s.length + val len2 = s.length + } + } + + def elseIf() = { + val s1: String|Null = ??? + val s2: String|Null = ??? + val s3: String|Null = ??? + if (s1 != null) { + val len = s1.length + val err1 = s2.length // error + val err2 = s3.length // error + } else if (s2 != null) { + val len = s2.length + val err1 = s1.length // error + val err2 = s3.length // error + } else if (s3 != null) { + val len = s3.length + val err1 = s1.length // error + val err2 = s2.length // error + } + + // Accumulation in elseif + if (s1 == null) { + } else if (s2 == null) { + val len = s1.length + } else if (s3 == null) { + val len1 = s1.length + val len2 = s2.length + } else { + val len1 = s1.length + val len2 = s2.length + val len3 = s3.length + } + } + + def commonIdioms() = { + val s1: String|Null = ??? + val s2: String|Null = ??? + val s3: String|Null = ??? + + if (s1 == null || s2 == null || s3 == null) { + } else { + val len1: Int = s1.length + val len2: Int = s2.length + val len3: Int = s3.length + } + + if (s1 != null && s2 != null && s3 != null) { + val len1: Int = s1.length + val len2: Int = s2.length + val len3: Int = s3.length + } + } + + def basicNegation() = { + val s1: String|Null = ??? + if (!(s1 != null)) { + val len = s1.length // error + } else { + val len = s1.length + } + + if (!(!(!(!(s1 != null))))) { + val len1 = s1.length + } + } + + def parens() = { + val s1: String|Null = ??? + val s2: String|Null = ??? + if ((((s1 == null))) || s2 == null) { + } else { + val len1 = s1.length + val len2 = s2.length + } + } + + def operatorPrec() = { + val s1: String|Null = ??? + val s2: String|Null = ??? + val s3: String|Null = ??? + + if (s1 != null || s2 != null && s3 != null) { + val len = s3.length // error + } + + if (s1 != null && s2 != null || s3 != null) { + val len1 = s1.length // error + val len2 = s2.length // error + val len3 = s3.length // error + } + + if (s1 != null && (s2 != null || s3 != null)) { + val len1 = s1.length + val len2 = s2.length // error + val len3 = s3.length // error + } + } + + def insideCond() = { + val x: String|Null = ??? + if (x != null && x.length > 0) { + val len = x.length + } else { + val len = x.length // error + } + + if (x == null || x.length > 0) { + val len = x.length // error + } else { + val len = x.length + } + + class Rec { + val r: Rec|Null = ??? + } + + val r: Rec|Null = ??? + if (r != null && r.r != null && (r.r.r == null || r.r.r.r == r)) { + val err = r.r.r.r // error + } + } +} + diff --git a/tests/explicit-nulls/neg/flow2.scala b/tests/explicit-nulls/neg/flow2.scala new file mode 100644 index 000000000000..7ac243f7fd36 --- /dev/null +++ b/tests/explicit-nulls/neg/flow2.scala @@ -0,0 +1,18 @@ + +// Test that flow inference can handle blocks. +class Foo { + val x: String|Null = "hello" + if ({val z = 10; {1 + 1 == 2; x != null}}) { + val l = x.length + } + + if ({x != null; true}) { + val l = x.length // error + } + + val x2: String|Null = "world" + if ({{{{1 + 1 == 2; x != null}}}} && x2 != null) { + val l = x.length + val l2 = x2.length + } +} diff --git a/tests/explicit-nulls/neg/flow5.scala b/tests/explicit-nulls/neg/flow5.scala new file mode 100644 index 000000000000..0d11e45c6d54 --- /dev/null +++ b/tests/explicit-nulls/neg/flow5.scala @@ -0,0 +1,66 @@ + +// Test that flow-sensitive type inference handles +// early exists from blocks. +class Foo(x: String|Null) { + + // Test within constructor + if (x == null) throw new NullPointerException() + val x2: String = x // error: flow inference for blocks doesn't work inside constructors + + def foo(): Unit = { + val y: String|Null = ??? + if (y == null) return () + val y2: String = y // ok + } + + def bar(): Unit = { + val y: String|Null = ??? + if (y != null) { + } else { + return () + } + val y2: String = y // ok + } + + def fooInExprPos(): String = { + val y: String|Null = ??? + if (y == null) return "foo" + y // ok + } + + def nonLocalInBlock(): String = { + val y: String|Null = ??? + if (y == null) { println("foo"); return "foo" } + y + } + + def barWrong(): Unit = { + val y: String|Null = ??? + if (y != null) { + return () + } else { + } + val y2: String = y // error: can't infer that y is non-null (actually, it's the opposite) + } + + def err(msg: String): Nothing = { + throw new RuntimeException(msg) + } + + def retTypeNothing(): String = { + val y: String|Null = ??? + if (y == null) err("y is null!") + y + } + + def errRetUnit(msg: String): Unit = { + throw new RuntimeException(msg) + () + } + + def retTypeUnit(): String = { + val y: String|Null = ??? + if (y == null) errRetUnit("y is null!") + y // error: previous statement returned unit so can't infer non-nullability + } +} diff --git a/tests/explicit-nulls/neg/flow6.scala b/tests/explicit-nulls/neg/flow6.scala new file mode 100644 index 000000000000..6890a43018dd --- /dev/null +++ b/tests/explicit-nulls/neg/flow6.scala @@ -0,0 +1,58 @@ +// Test forward references handled with flow typing +// Currently, the flow typing will not be applied to definitions forwardly referred. +class Foo { + + def test0(): Unit = { + def z: Int = y + val x: String|Null = ??? + if (x == null) return () + def y: Int = x.length // error: x: String|Null inferred + } + + + def test1(): Unit = { + val x: String|Null = ??? + if (x == null) return () + def y = x.length // ok: x: String inferred + () + } + + // This test is similar to test1, but a forward DefDef referring + // to y is added after x. y is completed before knowing the + // fact "x != null", hence, the type x: String|Null is used. + def test2(): Unit = { + val x: String|Null = ??? + def z: Int = y + if (x == null) return () + def y: Int = x.length // error: x: String|Null is inferred + () + } + + // Since y is referred before definition, flow typing is not used here. + def test3(): Unit = { + val x: String|Null = ??? + lazy val z = y + if (x == null) return () + lazy val y = x.length // error: x: String|Null is inferred + () + } + + // This case is invalid because z has an implicit forward reference to y, + // but x, y and z aren't lazy (only forward references to lazy vals are allowed). + // Since y is referred (by z) before definition, flow typing is not used here. + // Only the typing error is shown because reference check is after typing. + def test4(): Unit = { + val z = implicitly[Int] + val x: String|Null = ??? + if (x == null) return () + implicit val y: Int = x.length // error: x: String|Null inferred + } + + // Since z is referred before definition, flow typing is not used here. + def test5(): Unit = { + val x: String|Null = ??? + if (x == null) return () + def y = z + def z = x.length // error: x: String|Null inferred + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/neg/flow7.scala b/tests/explicit-nulls/neg/flow7.scala new file mode 100644 index 000000000000..e0fa5b79464c --- /dev/null +++ b/tests/explicit-nulls/neg/flow7.scala @@ -0,0 +1,11 @@ + +class Foo(x: String|Null) { + if (x == null) throw new NullPointerException("x is null") + val y: String = x // error: flow inference for blocks only works inside methods + + def foo(x: String|Null): Unit = { + if (x == null) throw new NullPointerException("x is null") + val y: String = x + () + } +} diff --git a/tests/explicit-nulls/neg/interop-array-src/J.java b/tests/explicit-nulls/neg/interop-array-src/J.java new file mode 100644 index 000000000000..80fda83e89d7 --- /dev/null +++ b/tests/explicit-nulls/neg/interop-array-src/J.java @@ -0,0 +1,3 @@ +class J { + void foo(String[] ss) {} +} diff --git a/tests/explicit-nulls/neg/interop-array-src/S.scala b/tests/explicit-nulls/neg/interop-array-src/S.scala new file mode 100644 index 000000000000..3796bab79970 --- /dev/null +++ b/tests/explicit-nulls/neg/interop-array-src/S.scala @@ -0,0 +1,10 @@ +class S { + + val j = new J() + val x: Array[String] = ??? + j.foo(x) // error: expected Array[String|Null] but got Array[String] + + val x2: Array[String|Null] = ??? + j.foo(x2) // ok + j.foo(null) // ok +} diff --git a/tests/explicit-nulls/neg/interop-java-enum-src/Planet.java b/tests/explicit-nulls/neg/interop-java-enum-src/Planet.java new file mode 100644 index 000000000000..7a6cb097f565 --- /dev/null +++ b/tests/explicit-nulls/neg/interop-java-enum-src/Planet.java @@ -0,0 +1,27 @@ +public enum Planet { + MERCURY (3.303e+23, 2.4397e6), + VENUS (4.869e+24, 6.0518e6), + EARTH (5.976e+24, 6.37814e6), + MARS (6.421e+23, 3.3972e6), + JUPITER (1.9e+27, 7.1492e7), + SATURN (5.688e+26, 6.0268e7), + URANUS (8.686e+25, 2.5559e7), + NEPTUNE (1.024e+26, 2.4746e7); + + private final double mass; // in kilograms + private final double radius; // in meters + Planet(double mass, double radius) { + this.mass = mass; + this.radius = radius; + } + private double mass() { return mass; } + private double radius() { return radius; } + + // This method returns a `Planet`, but since `null` is a valid + // return value, the return type should be nullified. + // Contrast with accessing the static member corresponding to the enum + // _instance_ (e.g. Planet.MERCURY) which shouldn't be nullified. + Planet next() { + return null; + } +} diff --git a/tests/explicit-nulls/neg/interop-java-enum-src/S.scala b/tests/explicit-nulls/neg/interop-java-enum-src/S.scala new file mode 100644 index 000000000000..8e4e228a5e76 --- /dev/null +++ b/tests/explicit-nulls/neg/interop-java-enum-src/S.scala @@ -0,0 +1,6 @@ + +// Verify that enum values aren't nullified. +class S { + val p: Planet = Planet.MARS // ok: accessing static member + val p2: Planet = p.next() // error: expected Planet but got Planet|Null +} diff --git a/tests/explicit-nulls/neg/interop-javanull.scala b/tests/explicit-nulls/neg/interop-javanull.scala new file mode 100644 index 000000000000..1a1924016491 --- /dev/null +++ b/tests/explicit-nulls/neg/interop-javanull.scala @@ -0,0 +1,8 @@ + +// Test that JavaNull can be assigned to Null. +class Foo { + import java.util.ArrayList + val l = new ArrayList[String]() + val s: String = l.get(0) // error: return type is nullable + val s2: String|Null = l.get(0) // ok +} diff --git a/tests/explicit-nulls/neg/interop-method-src/J.java b/tests/explicit-nulls/neg/interop-method-src/J.java new file mode 100644 index 000000000000..1b7ea514e4b2 --- /dev/null +++ b/tests/explicit-nulls/neg/interop-method-src/J.java @@ -0,0 +1,5 @@ + +class J { + String foo(String x) { return null; } + static String fooStatic(String x) { return null; } +} diff --git a/tests/explicit-nulls/neg/interop-method-src/S.scala b/tests/explicit-nulls/neg/interop-method-src/S.scala new file mode 100644 index 000000000000..403c86bc4c06 --- /dev/null +++ b/tests/explicit-nulls/neg/interop-method-src/S.scala @@ -0,0 +1,10 @@ + +class S { + + val j = new J() + j.foo(null) // ok: argument is nullable + val s: String = j.foo("hello") // error: return type is nullable + + J.fooStatic(null) // ok: argument is nullable + val s2: String = J.fooStatic("hello") // error: return type is nullable +} diff --git a/tests/explicit-nulls/neg/interop-polytypes.scala b/tests/explicit-nulls/neg/interop-polytypes.scala new file mode 100644 index 000000000000..5718e0fc564d --- /dev/null +++ b/tests/explicit-nulls/neg/interop-polytypes.scala @@ -0,0 +1,7 @@ +class Foo { + import java.util.ArrayList + // Test that return values in PolyTypes are marked as nullable. + val lstring = new ArrayList[String]() + val res: String = java.util.Collections.max(lstring) // error: missing |Null + val res2: String|Null = java.util.Collections.max(lstring) // ok +} diff --git a/tests/explicit-nulls/neg/interop-propagate.scala b/tests/explicit-nulls/neg/interop-propagate.scala new file mode 100644 index 000000000000..c21728fb7395 --- /dev/null +++ b/tests/explicit-nulls/neg/interop-propagate.scala @@ -0,0 +1,11 @@ + class Foo { + import java.util.ArrayList + + // Test that as we extract return values, we're missing the |JavaNull in the return type. + // i.e. test that the nullability is propagated to nested containers. + val ll = new ArrayList[ArrayList[ArrayList[String]]] + val level1: ArrayList[ArrayList[String]] = ll.get(0) // error + val level2: ArrayList[String] = ll.get(0).get(0) // error + val level3: String = ll.get(0).get(0).get(0) // error + val ok: String = ll.get(0).get(0).get(0) // error +} diff --git a/tests/explicit-nulls/neg/interop-return.scala b/tests/explicit-nulls/neg/interop-return.scala new file mode 100644 index 000000000000..677d9528e6fa --- /dev/null +++ b/tests/explicit-nulls/neg/interop-return.scala @@ -0,0 +1,14 @@ + +// Test that the return type of Java methods as well as the type of Java fields is marked as nullable. +class Foo { + + def foo = { + import java.util.ArrayList + val x = new ArrayList[String]() + val r: String = x.get(0) // error: got String|JavaNull instead of String + + val x2 = new ArrayList[Int]() + val r2: Int = x2.get(0) // error: even though Int is non-nullable in Scala, its counterpart + // (for purposes of generics) in Java (Integer) is. So we're missing |JavaNull + } +} diff --git a/tests/explicit-nulls/neg/java-null.scala b/tests/explicit-nulls/neg/java-null.scala new file mode 100644 index 000000000000..884cd43745db --- /dev/null +++ b/tests/explicit-nulls/neg/java-null.scala @@ -0,0 +1,10 @@ +// Test that `JavaNull` is see-through, but `Null` isn't. + +class Test { + val s: String|Null = "hello" + val l = s.length // error: `Null` isn't "see-through" + + val s2: String|JavaNull = "world" + val l2 = s2.length // ok +} + diff --git a/tests/explicit-nulls/neg/notnull/J.java b/tests/explicit-nulls/neg/notnull/J.java new file mode 100644 index 000000000000..6230e44eb828 --- /dev/null +++ b/tests/explicit-nulls/neg/notnull/J.java @@ -0,0 +1,15 @@ +import java.util.*; +import notnull.NotNull; + +public class J { + + private static String getK() { + return "k"; + } + + @NotNull + public static final String k = getK(); + + @NotNull + public static String l = "l"; +} diff --git a/tests/explicit-nulls/neg/notnull/NotNull.java b/tests/explicit-nulls/neg/notnull/NotNull.java new file mode 100644 index 000000000000..79c36de8504c --- /dev/null +++ b/tests/explicit-nulls/neg/notnull/NotNull.java @@ -0,0 +1,8 @@ +package notnull; + +import java.lang.annotation.*; + +// A NotNull Annotation not in the list +@Retention(value = RetentionPolicy.RUNTIME) +public @interface NotNull { +} diff --git a/tests/explicit-nulls/neg/notnull/S.scala b/tests/explicit-nulls/neg/notnull/S.scala new file mode 100644 index 000000000000..eada60eea6e7 --- /dev/null +++ b/tests/explicit-nulls/neg/notnull/S.scala @@ -0,0 +1,7 @@ +// Test that NotNull annotations not in the list are not working in Java files. + +class S { + def kk: String = J.k // error: k doesn't have a constant type and the NotNull annotation is not in the list + + def ll: String = J.l // error: the NotNull annotation is not in the list +} \ No newline at end of file diff --git a/tests/explicit-nulls/neg/null-subtype-any.scala b/tests/explicit-nulls/neg/null-subtype-any.scala new file mode 100644 index 000000000000..aa1ff441a601 --- /dev/null +++ b/tests/explicit-nulls/neg/null-subtype-any.scala @@ -0,0 +1,17 @@ +// Test that Null is a subtype of Any, but not of AnyRef. + +class Foo { + + val x1: Any = null + val x2: AnyRef = null // error + val x3: AnyRef|Null = null + val x4: Any|Null = null // Any|Null == Any + + { + def bar(a: Any): Unit = () + val s: String|Null = ??? + bar(s) + val s2: Int|Null = ??? + bar(s2) + } +} diff --git a/tests/explicit-nulls/neg/nullnull.scala b/tests/explicit-nulls/neg/nullnull.scala new file mode 100644 index 000000000000..1ebb25bf6238 --- /dev/null +++ b/tests/explicit-nulls/neg/nullnull.scala @@ -0,0 +1,18 @@ +// Test that `Null | Null | ... | Null` will not cause crash during typing. +// We want to strip `Null`s from the type after the `if` statement. +// After `normNullableUnion`, `Null | Null | ... | Null` should become +// `Null | Null`, and `stripNull` will return type `Null`. + +class Foo { + def foo1: Unit = { + val x: Null | Null | Null = ??? + if (x == null) return () + val y = x.length // error: x: Null is inferred + } + + def foo2: Unit = { + val x: JavaNull | String | Null = ??? + if (x == null) return () + val y = x.length // ok: x: String is inferred + } +} diff --git a/tests/explicit-nulls/neg/override-java-object-arg.scala b/tests/explicit-nulls/neg/override-java-object-arg.scala new file mode 100644 index 000000000000..7a45cb1d6199 --- /dev/null +++ b/tests/explicit-nulls/neg/override-java-object-arg.scala @@ -0,0 +1,26 @@ + +// Test that we can properly override Java methods where an argument has type 'Object'. +// See pos/override-java-object-arg.scala for context. + +import javax.management.{Notification, NotificationEmitter, NotificationListener} + +class Foo { + + def bar(): Unit = { + val listener = new NotificationListener() { + override def handleNotification(n: Notification|Null, emitter: Object): Unit = { // error: method handleNotification overrides nothing + } + } + + val listener2 = new NotificationListener() { + override def handleNotification(n: Notification|Null, emitter: Object|Null): Unit = { // ok + } + } + + val listener3 = new NotificationListener() { + override def handleNotification(n: Notification, emitter: Object|Null): Unit = { // error: method handleNotification overrides nothing + } + } + } +} + diff --git a/tests/explicit-nulls/neg/override-java-object-arg2.scala b/tests/explicit-nulls/neg/override-java-object-arg2.scala new file mode 100644 index 000000000000..5ef7373d0868 --- /dev/null +++ b/tests/explicit-nulls/neg/override-java-object-arg2.scala @@ -0,0 +1,13 @@ + +import javax.management.{Notification, NotificationEmitter, NotificationListener} + +class Foo { + + def bar(): Unit = { + val listener4 = new NotificationListener() { // error: duplicate symbol error + def handleNotification(n: Notification|Null, emitter: Object): Unit = { + } + } + } + +} diff --git a/tests/explicit-nulls/neg/simple-var.scala b/tests/explicit-nulls/neg/simple-var.scala new file mode 100644 index 000000000000..66ac053a4fbb --- /dev/null +++ b/tests/explicit-nulls/neg/simple-var.scala @@ -0,0 +1,45 @@ +// Test simple var track + +class SimpleVar { + + def nullable[T](x: T): T|Null = x + + locally { + var x: String|Null = ??? + x = "" // x is assigned to a non-null value + val l: Int = x.length // ok, we know x is not null + } + + locally { + var x: String|Null = ??? + if (x != null) { + val a: String = x + x = "" + val b: String = x + } + + assert(x != null) + val a: String = x + x = nullable(x) + val b: String = x // error: x might be null + } + + locally { + var x: String|Null = ??? + if (x != null) { + val a: String = x + x = null + val b: String = x // error: x is null + } + } + + locally { + var x: String|Null = ??? + if (x != null) { + val a: String = x + val b: String | String = a + x = b + val _: String = x // ok + } + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/neg/strip.scala b/tests/explicit-nulls/neg/strip.scala new file mode 100644 index 000000000000..997d272d008f --- /dev/null +++ b/tests/explicit-nulls/neg/strip.scala @@ -0,0 +1,25 @@ +// Test we are correctly striping nulls from nullable unions. + +class Foo { + + class B1 + class B2 + locally { + val x: (Null | String) | Null | (B1 | (Null | B2)) = ??? + if (x != null) { + val _: String | B1 | B2 = x // ok: can remove all nullable unions + } + } + + locally { + val x: (Null | String) & (Null | B1) = ??? + if (x != null) { + val _: String & B1 = x // ok: can remove null from embedded intersection + } + } + + locally { + val x: (Null | B1) & B2 = ??? + if (x != null) {} // error: the type of x is not a nullable union, so we cannot remove the Null + } +} diff --git a/tests/explicit-nulls/neg/throw-null.scala b/tests/explicit-nulls/neg/throw-null.scala new file mode 100644 index 000000000000..1fc3d4721155 --- /dev/null +++ b/tests/explicit-nulls/neg/throw-null.scala @@ -0,0 +1,14 @@ +// `throws null` is valid program in dotty but not valid with explicit null, +// since this statement will throw `NullPointerException` during runtime. +// https://stackoverflow.com/questions/17576922/why-can-i-throw-null-in-java + +class Foo { + def test1() = { + throw null // error: the expression cannot be `Null` + } + + def test2() = { + val t: Throwable | Null = ??? + throw t // error: the expression cannot be `Null` + } +} diff --git a/tests/explicit-nulls/neg/type-arg.scala b/tests/explicit-nulls/neg/type-arg.scala new file mode 100644 index 000000000000..c145ce562e6e --- /dev/null +++ b/tests/explicit-nulls/neg/type-arg.scala @@ -0,0 +1,13 @@ + +// Test that reference types being non-nullable +// is checked when lower bound of a type argument +// is Null. +object Test { + type Untyped = Null + class TreeInstances[T >: Untyped] + class Type + + object untpd extends TreeInstances[Null] + // There are two errors reported for the line below (don't know why). + object tpd extends TreeInstances[Type] // error // error +} diff --git a/tests/explicit-nulls/neg/var-ref-in-closure.scala b/tests/explicit-nulls/neg/var-ref-in-closure.scala new file mode 100644 index 000000000000..a33c32b5922e --- /dev/null +++ b/tests/explicit-nulls/neg/var-ref-in-closure.scala @@ -0,0 +1,172 @@ +// Test that we don't track variables which is refered in another closure. + +object VarRef { + locally { + var x: String|Null = ??? + val y = { + if (x != null) { + val _: String = x // ok: y doesn't create closure + } + } + if (x != null) { + val a: String = x // ok + } + } + + locally { + var x: String|Null = ??? + var y = { + if (x != null) { + val _: String = x // ok: y doesn't create closure + } + } + if (x != null) { + val a: String = x // ok + } + } + + locally { + var x: String|Null = ??? + lazy val y = { + if (x != null) { + x = null + } + x + } + if (x != null) { + val a: String = x // error: x exists in closure, no longer trackable + } + } + + locally { + var x: String|Null = ??? + def y = { + if (x != null) { + x = null + } + x + } + if (x != null) { + val a: String = x // error: x exists in closure, no longer trackable + } + } + + + locally { + var x: String|Null = ??? + lazy val y = { + if (x != null) { + val a: String = x // error: x exists in closure, no longer trackable + } + x + } + } + + locally { + var x: String|Null = ??? + def y = { + if (x != null) { + val a: String = x // error: x exists in closure, no longer trackable + } + x + } + } + + lazy val lazyblock = { + var x: String|Null = ??? + lazy val y = { + if (x != null) { + // The enclosingMethods of x definition and x reference hare are same + val a: String = x // error: x exists in closure, no longer trackable + } + x + } + } + + abstract class F { + def get(): String | Null + } + + locally { + var x: String|Null = ??? + val y: F = new F { + def get() = { + if (x != null) x = null + x + } + } + if (x != null) { + val a: String = x // error: x exists in closure, no longer trackable + } + } + + locally { + var x: String|Null = ??? + val y: F = new F { + def get() = { + if (x != null) { + val a: String = x // error: x exists in closure, no longer trackable + } + x + } + } + } + + def f(x: => String | Null): F = new F { + def get() = x + } + + locally { + var x: String|Null = ??? + val y: F = f { + if (x != null) { + x = null + } + x + } + if (x != null) { + val a: String = x // error: x exists in closure, no longer trackable + } + } + + // TODO: not working now + // locally { + // var x: String|Null = ??? + // val y: F = f { + // if (x != null) { + // val a: String = x // err: x exists in closure, no longer trackable + // } + // x + // } + // } + + locally { + var x: String|Null = ??? + val y: String => String|Null = s => { + if (x != null) { + val a: String = x // error: x exists in closure, no longer trackable + } + x + } + } + + locally { + val x: String|Null = ??? + if (x != null) { + def f = { + val y: String = x // ok, x is a value definition + y + } + } + } + + locally { + var x: String|Null = ??? + if (x != null) { + def f = { + val y: String = x // error: the use of x is out of order + y + } + } + } +} diff --git a/tests/explicit-nulls/pos-separate/interop-enum-src/Day_1.java b/tests/explicit-nulls/pos-separate/interop-enum-src/Day_1.java new file mode 100644 index 000000000000..b5d96e446fa8 --- /dev/null +++ b/tests/explicit-nulls/pos-separate/interop-enum-src/Day_1.java @@ -0,0 +1,6 @@ + +public enum Day_1 { + SUN, + MON, + TUE +} diff --git a/tests/explicit-nulls/pos-separate/interop-enum-src/Planet_2.java b/tests/explicit-nulls/pos-separate/interop-enum-src/Planet_2.java new file mode 100644 index 000000000000..c46e92d13bea --- /dev/null +++ b/tests/explicit-nulls/pos-separate/interop-enum-src/Planet_2.java @@ -0,0 +1,19 @@ +public enum Planet_2 { + MERCURY (3.303e+23, 2.4397e6), + VENUS (4.869e+24, 6.0518e6), + EARTH (5.976e+24, 6.37814e6), + MARS (6.421e+23, 3.3972e6), + JUPITER (1.9e+27, 7.1492e7), + SATURN (5.688e+26, 6.0268e7), + URANUS (8.686e+25, 2.5559e7), + NEPTUNE (1.024e+26, 2.4746e7); + + private final double mass; // in kilograms + private final double radius; // in meters + Planet_2(double mass, double radius) { + this.mass = mass; + this.radius = radius; + } + private double mass() { return mass; } + private double radius() { return radius; } +} diff --git a/tests/explicit-nulls/pos-separate/interop-enum-src/S_3.scala b/tests/explicit-nulls/pos-separate/interop-enum-src/S_3.scala new file mode 100644 index 000000000000..3c5c8cd451ae --- /dev/null +++ b/tests/explicit-nulls/pos-separate/interop-enum-src/S_3.scala @@ -0,0 +1,6 @@ + +// Verify that enum values aren't nullified. +class S { + val d: Day_1 = Day_1.MON + val p: Planet_2 = Planet_2.MARS +} diff --git a/tests/explicit-nulls/pos-separate/notnull/J_2.java b/tests/explicit-nulls/pos-separate/notnull/J_2.java new file mode 100644 index 000000000000..b8837a41f966 --- /dev/null +++ b/tests/explicit-nulls/pos-separate/notnull/J_2.java @@ -0,0 +1,52 @@ +package javax.annotation; +import java.util.*; + +public class J_2 { + + // Since the value of the constant field is not null, + // the type of the field is ConstantType("k"), which we + // don't need to nullify + public static final String k = "k"; + + @Nonnull + public static String l = "l"; + + @Nonnull + // Since the value of the constant field is null, + // the type of the field before nullifying is TypeRef(String). + // With the Nonnull annotation, the result of nullifying would + // be TypeRef(String). + public final String m = null; + + @Nonnull + public String n = "n"; + + @Nonnull + public static final String f(int i) { + return "f: " + i; + } + + @Nonnull + public static String g(int i) { + return "g: " + i; + } + + @Nonnull + public String h(int i) { + return "h: " + i; + } + + @Nonnull + public String[] genericf(T a) { + String[] as = new String[1]; + as[0] = "" + a; + return as; + } + + @Nonnull + public List genericg(T a) { + List as = new ArrayList(); + as.add(a); + return as; + } +} diff --git a/tests/explicit-nulls/pos-separate/notnull/Nonnull_1.java b/tests/explicit-nulls/pos-separate/notnull/Nonnull_1.java new file mode 100644 index 000000000000..d30447e632cb --- /dev/null +++ b/tests/explicit-nulls/pos-separate/notnull/Nonnull_1.java @@ -0,0 +1,8 @@ +package javax.annotation; + +import java.lang.annotation.*; + +// A "fake" Nonnull Annotation for jsr305 +@Retention(value = RetentionPolicy.RUNTIME) +@interface Nonnull { +} diff --git a/tests/explicit-nulls/pos-separate/notnull/S_3.scala b/tests/explicit-nulls/pos-separate/notnull/S_3.scala new file mode 100644 index 000000000000..96705e36d185 --- /dev/null +++ b/tests/explicit-nulls/pos-separate/notnull/S_3.scala @@ -0,0 +1,15 @@ +// Test that NotNull annotations are working in class files. + +import javax.annotation.J_2 + +class S_3 { + def kk: String = J_2.k + def ll: String = J_2.l + def mm: String = (new J_2).m + def nn: String = (new J_2).n + def ff(i: Int): String = J_2.f(i) + def gg(i: Int): String = J_2.g(i) + def hh(i: Int): String = (new J_2).h(i) + def genericff(a: String | Null): Array[String | JavaNull] = (new J_2).genericf(a) + def genericgg(a: String | Null): java.util.List[String] = (new J_2).genericg(a) +} diff --git a/tests/explicit-nulls/pos/array.scala b/tests/explicit-nulls/pos/array.scala new file mode 100644 index 000000000000..f3146c8e8e2b --- /dev/null +++ b/tests/explicit-nulls/pos/array.scala @@ -0,0 +1,5 @@ +// Test that array contents are non-nullable. +class Foo { + val x: Array[String] = Array("hello") + val s: String = x(0) +} diff --git a/tests/explicit-nulls/pos/dont-widen-src/J.java b/tests/explicit-nulls/pos/dont-widen-src/J.java new file mode 100644 index 000000000000..c957a1f307b6 --- /dev/null +++ b/tests/explicit-nulls/pos/dont-widen-src/J.java @@ -0,0 +1,3 @@ +class J { + String foo() { return "hello"; } +} diff --git a/tests/explicit-nulls/pos/dont-widen-src/S.scala b/tests/explicit-nulls/pos/dont-widen-src/S.scala new file mode 100644 index 000000000000..0fbca30fac0a --- /dev/null +++ b/tests/explicit-nulls/pos/dont-widen-src/S.scala @@ -0,0 +1,7 @@ +class S { + val j = new J() + val x = j.foo() + // Check that the type of `x` is inferred to be `String|Null`. + // i.e. the union isn't collapsed. + val y: String|Null = x +} diff --git a/tests/explicit-nulls/pos/dont-widen.scala b/tests/explicit-nulls/pos/dont-widen.scala new file mode 100644 index 000000000000..e35615f7079a --- /dev/null +++ b/tests/explicit-nulls/pos/dont-widen.scala @@ -0,0 +1,8 @@ + +class S { + def foo[T](x: T): T = x + // Check that the type argument to `foo` is inferred to be + // `String|Null`: i.e. it isn't collapsed. + val x = foo(if (1 == 2) "hello" else null) + val y: String|Null = x +} diff --git a/tests/explicit-nulls/pos/flow-singleton.scala b/tests/explicit-nulls/pos/flow-singleton.scala new file mode 100644 index 000000000000..b329a25370b0 --- /dev/null +++ b/tests/explicit-nulls/pos/flow-singleton.scala @@ -0,0 +1,9 @@ +// Test that flow typing works well with singleton types. + +class Test { + val x : String | Null = ??? + if (x != null) { + val y: x.type = x + y.toLowerCase // ok: y should have type `String` in this branch + } +} diff --git a/tests/explicit-nulls/pos/flow.scala b/tests/explicit-nulls/pos/flow.scala new file mode 100644 index 000000000000..bbe42a923b4d --- /dev/null +++ b/tests/explicit-nulls/pos/flow.scala @@ -0,0 +1,160 @@ + +// Flow-sensitive type inference +class Foo { + + def basic() = { + class Bar { + val s: String = ??? + } + + val b: Bar|Null = ??? + if (b != null) { + val s = b.s // ok: type of `b` inferred as `Bar` + val s2: Bar = b + } else { + } + } + + def nestedAndSelection() = { + class Bar2 { + val x: Bar2|Null = ??? + } + + val bar2: Bar2|Null = ??? + if (bar2 != null) { + if (bar2.x != null) { + if (bar2.x.x != null) { + if (bar2.x.x.x != null) { + val b2: Bar2 = bar2.x.x.x + } + val b2: Bar2 = bar2.x.x + } + val b2: Bar2 = bar2.x + } + val b2: Bar2 = bar2 + } + } + + def ifThenElse() = { + val s: String|Null = ??? + if (s == null) { + } else { + val len: Int = s.length + val len2 = s.length + } + } + + + def elseIf() = { + val s1: String|Null = ??? + val s2: String|Null = ??? + val s3: String|Null = ??? + if (s1 != null) { + val len = s1.length + } else if (s2 != null) { + val len = s2.length + } else if (s3 != null) { + val len = s3.length + } + + // Accumulation in elseif + if (s1 == null) { + } else if (s2 == null) { + val len = s1.length + } else if (s3 == null) { + val len1 = s1.length + val len2 = s2.length + } else { + val len1 = s1.length + val len2 = s2.length + val len3 = s3.length + } + } + + def commonIdioms() = { + val s1: String|Null = ??? + val s2: String|Null = ??? + val s3: String|Null = ??? + + if (s1 == null || s2 == null || s3 == null) { + } else { + val len1: Int = s1.length + val len2: Int = s2.length + val len3: Int = s3.length + } + + if (s1 != null && s2 != null && s3 != null) { + val len1: Int = s1.length + val len2: Int = s2.length + val len3: Int = s3.length + } + } + + def basicNegation() = { + val s1: String|Null = ??? + + if (!(s1 != null)) { + } else { + val len = s1.length + } + + if (!(!(!(!(s1 != null))))) { + val len1 = s1.length + } + } + + def parens() = { + val s1: String|Null = ??? + val s2: String|Null = ??? + + if ((((s1 == null))) || s2 == null) { + } else { + val len1 = s1.length + val len2 = s2.length + } + } + + def operatorPrecedence() = { + val s1: String|Null = ??? + val s2: String|Null = ??? + val s3: String|Null = ??? + + if (s1 != null && (s2 != null || s3 != null)) { + val len1 = s1.length + } + } + + def propInsideCond() = { + val s: String|Null = ??? + if (s != null && s.length > 0) { + val len: Int = s.length + } + + if (s == null || s.length == 0) { + } else { + val len: Int = s.length + } + + class Rec { + val r: Rec|Null = ??? + } + + val r: Rec|Null = ??? + if (r != null && r.r != null && r.r.r != null && (r.r.r.r != null) && r.r.r.r.r != null) { + val r6: Rec|Null = r.r.r.r.r.r + } + + if (r == null || r.r == null || r.r.r == null || (r.r.r.r == null) || r.r.r.r.r == null) { + } else { + val r6: Rec|Null = r.r.r.r.r.r + } + + if (!(r == null) && r.r != null) { + val r3: Rec|Null = r.r.r + } + } + + def interactWithTypeInference() = { + val f: String|Null => Int = (x) => if (x != null) x.length else 0 + } +} diff --git a/tests/explicit-nulls/pos/flow2.scala b/tests/explicit-nulls/pos/flow2.scala new file mode 100644 index 000000000000..2391da60b3be --- /dev/null +++ b/tests/explicit-nulls/pos/flow2.scala @@ -0,0 +1,11 @@ + +class Foo { + + val x: String|Null = ??? + val y: String|Null = ??? + val z: String|Null = ??? + + if ((x != null && z != null) || (y != null && z != null)) { + val z2: String = z + } +} diff --git a/tests/explicit-nulls/pos/flow4.scala b/tests/explicit-nulls/pos/flow4.scala new file mode 100644 index 000000000000..8327606bcd13 --- /dev/null +++ b/tests/explicit-nulls/pos/flow4.scala @@ -0,0 +1,23 @@ +// This test is based on tests/pos/rbtree.scala +// and it tests that we can use an inline method to "abstract" a more complicated +// isInstanceOf check, while at the same time getting the flow inference to know +// that `isRedTree(tree) => tree ne null`. +class TreeOps { + abstract class Tree[A, B](val key: A, val value: B) + class RedTree[A, B](override val key: A, override val value: B) extends Tree[A, B](key, value) + + private[this] inline def isRedTree(tree: Tree[_, _] | Null) = (tree != null) && tree.isInstanceOf[RedTree[_, _]] + + def foo[A, B](tree: Tree[A, B] | Null): Unit = { + if (isRedTree(tree)) { + val key = tree.key + val value = tree.value + } + + if (!isRedTree(tree)) { + } else { + val key = tree.key + val value = tree.value + } + } +} diff --git a/tests/explicit-nulls/pos/flow6.scala b/tests/explicit-nulls/pos/flow6.scala new file mode 100644 index 000000000000..555e24335c26 --- /dev/null +++ b/tests/explicit-nulls/pos/flow6.scala @@ -0,0 +1,74 @@ + +// Test that flow inference behaves soundly within blocks. +// This means that flow facts are propagated to all ValDef and DefDef. +class Foo { + + def test1(): Unit = { + val x: String|Null = ??? + if (x == null) return () + val y = x.length // ok: x: String inferred + () + } + + def test2(): Unit = { + val x: String|Null = ??? + if (x == null) return () + lazy val y = x.length // ok: x: String inferred + () + } + + def test3(): Unit = { + val x: String|Null = ??? + if (x == null) return () + implicit val y = x.length // ok: x: String inferred + () + } + + def test4(): Unit = { + val x: String|Null = ??? + if (x == null) return () + def y = x.length // ok: x: String inferred + () + } + + // This case is different from #3 because the type of y doesn't need + // to be inferred, which triggers a different codepath within the completer. + def test5(): Unit = { + val x: String|Null = ??? + if (x == null) return () + implicit val y: Int = x.length // ok: x: String inferred + } + + def test6(): Unit = { + val x: String|Null = ??? + if (x == null) return () + lazy val y: Int = x.length // ok: x: String inferred + () + } + + def test7(): Unit = { + val x: String|Null = ??? + if (x == null) return () + def y: Int = x.length // ok: x: String inferred + () + } + + def test8(): Unit = { + lazy val x: String|Null = ??? + if (x == null) return () + val y = x.length // ok: x: String inferred + () + } + + // This test checks that flow facts are forgotten for defs, but only + // the facts gathered within the current block are forgotten. + // Other facts from outer blocks are remembered. + def test9(): Unit = { + val x: String|Null = ??? + if (x == null) { + } else { + def f = x.length // ok + def f2: Int = x.length // ok + } + } +} diff --git a/tests/explicit-nulls/pos/interop-constructor-src/J.java b/tests/explicit-nulls/pos/interop-constructor-src/J.java new file mode 100644 index 000000000000..b1590d50023e --- /dev/null +++ b/tests/explicit-nulls/pos/interop-constructor-src/J.java @@ -0,0 +1,6 @@ +class J { + private String s; + + J(String x) { this.s = x; } + J(String x, String y, String z) {} +} diff --git a/tests/explicit-nulls/pos/interop-constructor-src/S.scala b/tests/explicit-nulls/pos/interop-constructor-src/S.scala new file mode 100644 index 000000000000..6cbfea9b57b1 --- /dev/null +++ b/tests/explicit-nulls/pos/interop-constructor-src/S.scala @@ -0,0 +1,6 @@ + +class S { + val x: J = new J("hello") + val x2: J = new J(null) + val x3: J = new J(null, null, null) +} diff --git a/tests/explicit-nulls/pos/interop-constructor.scala b/tests/explicit-nulls/pos/interop-constructor.scala new file mode 100644 index 000000000000..1f631e6efff6 --- /dev/null +++ b/tests/explicit-nulls/pos/interop-constructor.scala @@ -0,0 +1,7 @@ + +// Test that constructors have a non-nullab.e return type. +class Foo { + val x: java.lang.String = new java.lang.String() + val y: java.util.Date = new java.util.Date() + val v = new java.util.Vector[String](null /*stands for Collection*/) +} diff --git a/tests/explicit-nulls/pos/interop-enum-src/Day.java b/tests/explicit-nulls/pos/interop-enum-src/Day.java new file mode 100644 index 000000000000..55dca0783931 --- /dev/null +++ b/tests/explicit-nulls/pos/interop-enum-src/Day.java @@ -0,0 +1,6 @@ + +public enum Day { + SUN, + MON, + TUE +} diff --git a/tests/explicit-nulls/pos/interop-enum-src/Planet.java b/tests/explicit-nulls/pos/interop-enum-src/Planet.java new file mode 100644 index 000000000000..287aed6aecc5 --- /dev/null +++ b/tests/explicit-nulls/pos/interop-enum-src/Planet.java @@ -0,0 +1,19 @@ +public enum Planet { + MERCURY (3.303e+23, 2.4397e6), + VENUS (4.869e+24, 6.0518e6), + EARTH (5.976e+24, 6.37814e6), + MARS (6.421e+23, 3.3972e6), + JUPITER (1.9e+27, 7.1492e7), + SATURN (5.688e+26, 6.0268e7), + URANUS (8.686e+25, 2.5559e7), + NEPTUNE (1.024e+26, 2.4746e7); + + private final double mass; // in kilograms + private final double radius; // in meters + Planet(double mass, double radius) { + this.mass = mass; + this.radius = radius; + } + private double mass() { return mass; } + private double radius() { return radius; } +} diff --git a/tests/explicit-nulls/pos/interop-enum-src/S.scala b/tests/explicit-nulls/pos/interop-enum-src/S.scala new file mode 100644 index 000000000000..75e4654869a4 --- /dev/null +++ b/tests/explicit-nulls/pos/interop-enum-src/S.scala @@ -0,0 +1,6 @@ + +// Verify that enum values aren't nullified. +class S { + val d: Day = Day.MON + val p: Planet = Planet.MARS +} diff --git a/tests/explicit-nulls/pos/interop-generics/J.java b/tests/explicit-nulls/pos/interop-generics/J.java new file mode 100644 index 000000000000..b8eab374844b --- /dev/null +++ b/tests/explicit-nulls/pos/interop-generics/J.java @@ -0,0 +1,9 @@ + +class I {} + +class J { + I foo(T x) { + return new I(); + } + // TODO(abeln): test returning a Scala generic from Java +} diff --git a/tests/explicit-nulls/pos/interop-generics/S.scala b/tests/explicit-nulls/pos/interop-generics/S.scala new file mode 100644 index 000000000000..8c33ba3f0368 --- /dev/null +++ b/tests/explicit-nulls/pos/interop-generics/S.scala @@ -0,0 +1,7 @@ +class ReturnedFromJava[T] {} + +class S { + val j = new J() + // Check that the inside of a Java generic isn't nullified + val i: I[String]|Null = j.foo("hello") +} diff --git a/tests/explicit-nulls/pos/interop-javanull-src/J.java b/tests/explicit-nulls/pos/interop-javanull-src/J.java new file mode 100644 index 000000000000..a85afa17c859 --- /dev/null +++ b/tests/explicit-nulls/pos/interop-javanull-src/J.java @@ -0,0 +1,8 @@ + +class J1 { + J2 getJ2() { return new J2(); } +} + +class J2 { + J1 getJ1() { return new J1(); } +} diff --git a/tests/explicit-nulls/pos/interop-javanull-src/S.scala b/tests/explicit-nulls/pos/interop-javanull-src/S.scala new file mode 100644 index 000000000000..0f5c51a18ccc --- /dev/null +++ b/tests/explicit-nulls/pos/interop-javanull-src/S.scala @@ -0,0 +1,6 @@ + +// Test that JavaNull is "see through" +class S { + val j: J2 = new J2() + j.getJ1().getJ2().getJ1().getJ2().getJ1().getJ2() +} diff --git a/tests/explicit-nulls/pos/interop-javanull.scala b/tests/explicit-nulls/pos/interop-javanull.scala new file mode 100644 index 000000000000..636475166cbf --- /dev/null +++ b/tests/explicit-nulls/pos/interop-javanull.scala @@ -0,0 +1,10 @@ + +// Tests that the "JavaNull" type added to Java types is "see through" w.r.t member selections. +class Foo { + import java.util.ArrayList + import java.util.Iterator + + // Test that we can select through "|JavaNull" (unsoundly). + val x3 = new ArrayList[ArrayList[ArrayList[String]]]() + val x4: Int = x3.get(0).get(0).get(0).length() +} diff --git a/tests/explicit-nulls/pos/interop-nn-src/J.java b/tests/explicit-nulls/pos/interop-nn-src/J.java new file mode 100644 index 000000000000..96ac77a528f5 --- /dev/null +++ b/tests/explicit-nulls/pos/interop-nn-src/J.java @@ -0,0 +1,4 @@ +class J { + String foo() { return "hello"; } + String[] bar() { return null; } +} diff --git a/tests/explicit-nulls/pos/interop-nn-src/S.scala b/tests/explicit-nulls/pos/interop-nn-src/S.scala new file mode 100644 index 000000000000..819f080eab0c --- /dev/null +++ b/tests/explicit-nulls/pos/interop-nn-src/S.scala @@ -0,0 +1,15 @@ +class S { + val j = new J() + // Test that the `nn` extension method can be used to strip away + // nullability from a type. + val s: String = j.foo.nn + val a: Array[String|Null] = j.bar.nn + + // We can also call .nn on non-nullable types. + val x: String = ??? + val y: String = x.nn + + // And on other Scala code. + val x2: String|Null = null + val y2: String = x2.nn +} diff --git a/tests/explicit-nulls/pos/interop-poly-src/J.java b/tests/explicit-nulls/pos/interop-poly-src/J.java new file mode 100644 index 000000000000..a0d5c109605e --- /dev/null +++ b/tests/explicit-nulls/pos/interop-poly-src/J.java @@ -0,0 +1,29 @@ +import java.util.*; + +class JavaCat { + T prop; +} + +class J { + static ScalaCat getScalaCat() { + return null; + } + + static JavaCat getJavaCat() { + return null; + } + + static List getListOfStringArray() { + List as = new ArrayList(); + as.add(new String[1]); + return as; + } + + static List[] getArrayOfStringList() { + return (List[]) new List[1]; + } + + static List[]> getComplexStrings() { + return new ArrayList[]>(); + } +} diff --git a/tests/explicit-nulls/pos/interop-poly-src/S.scala b/tests/explicit-nulls/pos/interop-poly-src/S.scala new file mode 100644 index 000000000000..1fea277efe90 --- /dev/null +++ b/tests/explicit-nulls/pos/interop-poly-src/S.scala @@ -0,0 +1,20 @@ +// Test the handling of generics by the nullability transform. +// There are two classes here: JavaCat is Java-defined, and ScalaCat +// is Scala-defined. + +class ScalaCat[T] {} + +class Test { + // It's safe to return a JavaCat[String]|Null (no inner |Null), + // because JavaCat, being a Java class, _already_ nullifies its + // fields. + val jc: JavaCat[String]|Null = J.getJavaCat[String]() + // ScalaCat is Scala-defined, so we need the inner |Null. + val sc: ScalaCat[String|Null]|Null = J.getScalaCat[String]() + + import java.util.List + + val las: List[Array[String|Null]]|Null = J.getListOfStringArray() + val als: Array[List[String]|Null]|Null = J.getArrayOfStringList() + val css: List[Array[List[Array[String|Null]]|Null]]|Null = J.getComplexStrings() +} diff --git a/tests/explicit-nulls/pos/interop-static-src/J.java b/tests/explicit-nulls/pos/interop-static-src/J.java new file mode 100644 index 000000000000..10965aa9ef4c --- /dev/null +++ b/tests/explicit-nulls/pos/interop-static-src/J.java @@ -0,0 +1,4 @@ + +class J { + static int foo(String s) { return 42; } +} diff --git a/tests/explicit-nulls/pos/interop-static-src/S.scala b/tests/explicit-nulls/pos/interop-static-src/S.scala new file mode 100644 index 000000000000..e54a33cd175b --- /dev/null +++ b/tests/explicit-nulls/pos/interop-static-src/S.scala @@ -0,0 +1,6 @@ + +class S { + + J.foo(null) // Java static methods are also nullified + +} diff --git a/tests/explicit-nulls/pos/interop-tostring.scala b/tests/explicit-nulls/pos/interop-tostring.scala new file mode 100644 index 000000000000..75c90150dd05 --- /dev/null +++ b/tests/explicit-nulls/pos/interop-tostring.scala @@ -0,0 +1,9 @@ +// Test that `toString` has been special-cased to +// return a non-nullable value. + +class Foo { + val x: java.lang.Integer = 42 + val y: String = x.toString // would fail if toString returns nullable value + val y2 = x.toString // test interaction with type inference + val z: String = y2 +} diff --git a/tests/explicit-nulls/pos/interop-valuetypes.scala b/tests/explicit-nulls/pos/interop-valuetypes.scala new file mode 100644 index 000000000000..595a7de8917a --- /dev/null +++ b/tests/explicit-nulls/pos/interop-valuetypes.scala @@ -0,0 +1,6 @@ + +// Tests that value (non-reference) types aren't nullified by the Java transform. +class Foo { + val x: java.lang.String = "" + val len: Int = x.length() // type is Int and not Int|JavaNull +} diff --git a/tests/explicit-nulls/pos/java-null.scala b/tests/explicit-nulls/pos/java-null.scala new file mode 100644 index 000000000000..3739ddc138a1 --- /dev/null +++ b/tests/explicit-nulls/pos/java-null.scala @@ -0,0 +1,16 @@ +// Test that `JavaNull`able unions are transparent +// w.r.t member selections. + +class Test { + val s: String|JavaNull = "hello" + val l: Int = s.length // ok: `JavaNull` allows (unsound) member selections. + + val s2: JavaNull|String = "world" + val l2: Int = s2.length + + val s3: JavaNull|String|JavaNull = "hello" + val l3: Int = s3.length + + val s4: (String|JavaNull)&(JavaNull|String) = "hello" + val l4 = s4.length +} diff --git a/tests/explicit-nulls/pos/java-varargs-src/Names.java b/tests/explicit-nulls/pos/java-varargs-src/Names.java new file mode 100644 index 000000000000..e46b406749ce --- /dev/null +++ b/tests/explicit-nulls/pos/java-varargs-src/Names.java @@ -0,0 +1,4 @@ + +class Names { + static void setNames(String... names) {} +} diff --git a/tests/explicit-nulls/pos/java-varargs-src/S.scala b/tests/explicit-nulls/pos/java-varargs-src/S.scala new file mode 100644 index 000000000000..5c180fcca400 --- /dev/null +++ b/tests/explicit-nulls/pos/java-varargs-src/S.scala @@ -0,0 +1,19 @@ + +// Test that nullification can handle Java varargs. +// For varargs, the element type is nullified, but the top level argument isn't. +class S { + // Pass an empty array. + Names.setNames() + + // Pass a singleton array with null as an element. + Names.setNames(null) + + // Pass a singleton array. + Names.setNames("name1") + + // Multiple arguments. + Names.setNames("name1", "name2", "name3", "name4") + + // Multiple arguments, some null. + Names.setNames(null, null, "hello", "world", null) +} diff --git a/tests/explicit-nulls/pos/java-varargs.scala b/tests/explicit-nulls/pos/java-varargs.scala new file mode 100644 index 000000000000..79d0bcb7cbfa --- /dev/null +++ b/tests/explicit-nulls/pos/java-varargs.scala @@ -0,0 +1,22 @@ + +import java.nio.file._ +import java.nio.file.Paths + + +class S { + + // Paths.get is a Java method with two arguments, where the second one + // is a varargs: https://docs.oracle.com/javase/8/docs/api/java/nio/file/Paths.html + // static Path get(String first, String... more) + // The Scala compiler converts this signature into + // def get(first: String|JavaNUll, more: (String|JavaNull)*) + + // Test that we can avoid providing the varargs argument altogether. + Paths.get("out").toAbsolutePath + + // Test with one argument in the varargs. + Paths.get("home", "src") + + // Test multiple arguments in the varargs. + Paths.get("home", "src", "compiler", "src") +} diff --git a/tests/explicit-nulls/pos/match.scala b/tests/explicit-nulls/pos/match.scala new file mode 100644 index 000000000000..0e65b9584328 --- /dev/null +++ b/tests/explicit-nulls/pos/match.scala @@ -0,0 +1,11 @@ +// Test NotNullInfo from non-null cases + +object MatchTest { + locally { + val s: String|Null = ??? + s match { + case _: String => println(s.length) + case _ => println(0) + } + } +} diff --git a/tests/explicit-nulls/pos/nn.scala b/tests/explicit-nulls/pos/nn.scala new file mode 100644 index 000000000000..9682999f9fdc --- /dev/null +++ b/tests/explicit-nulls/pos/nn.scala @@ -0,0 +1,20 @@ +// Check that the `.nn` extension method strips away nullability. + +class Test { + val s1: String|Null = ??? + val s2: String = s1.nn + + type NString = String|Null + val s3: NString = ??? + val s4: String = s3.nn + + // `.nn` is a no-op when called on value types + val b1: Boolean = true + val b2: Boolean = b1.nn + + // Check that `.nn` interacts well with type inference. + def foo(s: String): String = s + val s5: String|Null = "hello" + val s6 = s5.nn + foo(s6) +} diff --git a/tests/explicit-nulls/pos/nn2.scala b/tests/explicit-nulls/pos/nn2.scala new file mode 100644 index 000000000000..417d8855e405 --- /dev/null +++ b/tests/explicit-nulls/pos/nn2.scala @@ -0,0 +1,10 @@ + +// Test that is fixed when explicit nulls are enabled. +// https://github.com/lampepfl/dotty/issues/6247 + +class Foo { + val x1: String|Null = null + x1.nn.length + val x2: String = x1.nn + x1.nn.length +} diff --git a/tests/explicit-nulls/pos/notnull/J.java b/tests/explicit-nulls/pos/notnull/J.java new file mode 100644 index 000000000000..58351827b862 --- /dev/null +++ b/tests/explicit-nulls/pos/notnull/J.java @@ -0,0 +1,50 @@ +package javax.annotation; +import java.util.*; + +public class J { + + private static String getK() { + return "k"; + } + + @Nonnull + public static final String k = getK(); + + @Nonnull + public static String l = "l"; + + @Nonnull + public final String m = null; + + @Nonnull + public String n = "n"; + + @Nonnull + public static final String f(int i) { + return "f: " + i; + } + + @Nonnull + public static String g(int i) { + return "g: " + i; + } + + @Nonnull + public String h(int i) { + return "h: " + i; + } + + @Nonnull + public String[] genericf(T a) { + String[] as = new String[1]; + as[0] = "" + a; + return as; + } + + @Nonnull + public List genericg(T a) { + List as = new ArrayList(); + as.add(a); + return as; + } +} diff --git a/tests/explicit-nulls/pos/notnull/Nonnull.java b/tests/explicit-nulls/pos/notnull/Nonnull.java new file mode 100644 index 000000000000..d30447e632cb --- /dev/null +++ b/tests/explicit-nulls/pos/notnull/Nonnull.java @@ -0,0 +1,8 @@ +package javax.annotation; + +import java.lang.annotation.*; + +// A "fake" Nonnull Annotation for jsr305 +@Retention(value = RetentionPolicy.RUNTIME) +@interface Nonnull { +} diff --git a/tests/explicit-nulls/pos/notnull/S.scala b/tests/explicit-nulls/pos/notnull/S.scala new file mode 100644 index 000000000000..5e99c45c8547 --- /dev/null +++ b/tests/explicit-nulls/pos/notnull/S.scala @@ -0,0 +1,15 @@ +// Test that NotNull annotations are working in Java files. + +import javax.annotation.J + +class S_3 { + def kk: String = J.k + def ll: String = J.l + def mm: String = (new J).m + def nn: String = (new J).n + def ff(i: Int): String = J.f(i) + def gg(i: Int): String = J.g(i) + def hh(i: Int): String = (new J).h(i) + def genericff(a: String | Null): Array[String | JavaNull] = (new J).genericf(a) + def genericgg(a: String | Null): java.util.List[String] = (new J).genericg(a) +} diff --git a/tests/explicit-nulls/pos/nullable-union.scala b/tests/explicit-nulls/pos/nullable-union.scala new file mode 100644 index 000000000000..5e63b5adef45 --- /dev/null +++ b/tests/explicit-nulls/pos/nullable-union.scala @@ -0,0 +1,14 @@ +// Test that nullable types can be represented via unions. + +class Bar + +class Foo { + val x: String|Null = null + val y: Array[String]|Null = null + val b: Null|Bar = null + + def foo(p: Bar|Null): String|Null = null + + foo(null) + foo(b) +} diff --git a/tests/explicit-nulls/pos/opaque-nullable.scala b/tests/explicit-nulls/pos/opaque-nullable.scala new file mode 100644 index 000000000000..4b6f4f3f88aa --- /dev/null +++ b/tests/explicit-nulls/pos/opaque-nullable.scala @@ -0,0 +1,25 @@ +// Unboxed option type using unions + null + opaque. +// Relies on the fact that Null is not a subtype of AnyRef. +// Test suggested by Sébastien Doeraene. + +opaque type Nullable[+A <: AnyRef] = A | Null // disjoint by construction! + +object Nullable { + def apply[A <: AnyRef](x: A | Null): Nullable[A] = x + + def some[A <: AnyRef](x: A): Nullable[A] = x + def none: Nullable[Nothing] = null + + implicit class NullableOps[A <: AnyRef](x: Nullable[A]) { + def isEmpty: Boolean = x == null + def flatMap[B <: AnyRef](f: A => Nullable[B]): Nullable[B] = + if (x == null) null + else f(x) + } + + val s1: Nullable[String] = "hello" + val s2: Nullable[String] = null + + s1.isEmpty + s1.flatMap((x) => true) +} diff --git a/tests/explicit-nulls/pos/override-java-object-arg-src/J.java b/tests/explicit-nulls/pos/override-java-object-arg-src/J.java new file mode 100644 index 000000000000..efcb630b7b6c --- /dev/null +++ b/tests/explicit-nulls/pos/override-java-object-arg-src/J.java @@ -0,0 +1,10 @@ + +// Copy of https://docs.oracle.com/javase/7/docs/api/javax/management/NotificationListener.html + +class Notification {}; + +interface NotificationListener { + + void handleNotification(Notification notification, Object handback); + +} diff --git a/tests/explicit-nulls/pos/override-java-object-arg-src/S.scala b/tests/explicit-nulls/pos/override-java-object-arg-src/S.scala new file mode 100644 index 000000000000..333e6e710d57 --- /dev/null +++ b/tests/explicit-nulls/pos/override-java-object-arg-src/S.scala @@ -0,0 +1,20 @@ + +// This test is like tests/pos/override-java-object-arg.scala, except that +// here we load the Java code from source, as opposed to a class file. +// In this case, the Java 'Object' type is turned into 'AnyRef', not 'Any'. + +class S { + + def bar(): Unit = { + val listener = new NotificationListener() { + override def handleNotification(n: Notification|Null, emitter: Object|Null): Unit = { + } + } + + val listener2 = new NotificationListener() { + override def handleNotification(n: Notification|Null, emitter: AnyRef|Null): Unit = { + } + } + } + +} diff --git a/tests/explicit-nulls/pos/override-java-object-arg.scala b/tests/explicit-nulls/pos/override-java-object-arg.scala new file mode 100644 index 000000000000..7ab8a77a8b0f --- /dev/null +++ b/tests/explicit-nulls/pos/override-java-object-arg.scala @@ -0,0 +1,30 @@ + +// When we load a Java class file, if a java method has an argument with type +// 'Object', it (the method argument) gets loaded by Dotty as 'Any' (as opposed to 'AnyRef'). +// This is pre-explicit-nulls behaviour. +// There is special logic in the type comparer that allows that method to be overriden +// with a corresponding argument with type 'AnyRef'. +// This test verifies that we can continue to override such methods, except that in +// the explicit nulls world we override with 'AnyRef|Null'. + +import javax.management.{Notification, NotificationEmitter, NotificationListener} + +class Foo { + + def bar(): Unit = { + val listener = new NotificationListener() { + // The second argument in the base interface is loaded with type 'Any', but we override + // it with 'AnyRef|Null'. + override def handleNotification(n: Notification|Null, emitter: Object|Null): Unit = { + } + } + + val listener2 = new NotificationListener() { + // The second argument in the base interface is loaded with type 'Any', but we override + // it with 'AnyRef|Null'. + override def handleNotification(n: Notification|Null, emitter: AnyRef|Null): Unit = { + } + } + } + +} diff --git a/tests/explicit-nulls/pos/pattern-matching.scala b/tests/explicit-nulls/pos/pattern-matching.scala new file mode 100644 index 000000000000..7e84fb8cd513 --- /dev/null +++ b/tests/explicit-nulls/pos/pattern-matching.scala @@ -0,0 +1,38 @@ + +class Foo { + val s: String = ??? + s match { + case s: String => 100 // warning: type test will always succeed + case _ => 200 // warning: unreachable + } + + s match { + case s: String => 100 // warning: type test will always succeed + case _ => 200 // warning: unreachable + } + + sealed trait Animal + case class Dog(name: String) extends Animal + case object Cat extends Animal + + val a: Animal = ??? + a match { + case Dog(name) => 100 + case Cat => 200 + case _ => 300 // warning: unreachable + } + + val a2: Animal|Null = ??? + a2 match { + case Dog(_) => 100 + case Cat => 200 + case _ => 300 // warning: only matches null + } + + val a3: Animal|Null = ??? + a3 match { + case Dog(_) => 100 + case Cat => 200 + case null => 300 // ok + } +} diff --git a/tests/explicit-nulls/pos/stable-path.scala b/tests/explicit-nulls/pos/stable-path.scala new file mode 100644 index 000000000000..0d0dcd967a6a --- /dev/null +++ b/tests/explicit-nulls/pos/stable-path.scala @@ -0,0 +1,11 @@ +// Test stable path is still stable after notNull + +class C(val x: Int, val next: C|Null) + +def f() = { + val xs: C|Null = C(1, C(2, null)) + assert(xs != null) + val a: xs.x.type = xs.x + assert(xs.next != null) + val b: xs.next.x.type = xs.next.x +} diff --git a/tests/explicit-nulls/pos/tref-caching.scala b/tests/explicit-nulls/pos/tref-caching.scala new file mode 100644 index 000000000000..7a4c3bc412ea --- /dev/null +++ b/tests/explicit-nulls/pos/tref-caching.scala @@ -0,0 +1,19 @@ + +// Exercise code paths for different types of cached term refs. +// Specifically, `NonNullTermRef`s are cached separately from regular `TermRefs`. +// If the two kinds of trefs weren't cached separately, then the code below would +// error out, because every time `x` is accessed the nullable or non-null denotation +// would replace the other one, causing errors during -Ychecks. +class Test { + def foo(): Unit = { + val x: String|Null = ??? // regular tref `x` + if (x != null) { + val y = x.length // non-null tref `x` + x.length // 2nd access to non-null tref `x` + val z = x.length // 3rd access to non-null tref `x` + } else { + val y = x // regular tref `x` + } + val x2 = x // regular tref `x` + } +} diff --git a/tests/explicit-nulls/pos/while-loop.scala b/tests/explicit-nulls/pos/while-loop.scala new file mode 100644 index 000000000000..49b8be5ceef6 --- /dev/null +++ b/tests/explicit-nulls/pos/while-loop.scala @@ -0,0 +1,18 @@ +class C(val x: Int, val next: C|Null) + +def f = { + var xs: C|Null = C(1, C(2, null)) + while (xs != null) { + val xsx: Int = xs.x + // Currently, we can't track a path with a mutable variable prefix, + // even though the variable is trackable, like (xs.next != null). + val xscpy: C = xs + if (xscpy.next != null) { + val _: Int = xscpy.next.x + if (xscpy.next.next != null) { + val _: Int = xscpy.next.next.x + } + } + xs = xs.next + } +} diff --git a/tests/explicit-nulls/pos/widen-nullable-union.scala b/tests/explicit-nulls/pos/widen-nullable-union.scala new file mode 100644 index 000000000000..9ffa767b84e5 --- /dev/null +++ b/tests/explicit-nulls/pos/widen-nullable-union.scala @@ -0,0 +1,42 @@ +// Test that we correctly handle nullable unions when widening. +// We keep nullable after widening. +class Test { + class A + class B + class C extends A + + locally { + val x: String|Null = ??? + val y = x // String|Null is inferred, this used to crash the compiler + } + + locally { + val x: (Int | Null) | String = ??? + val y = x + val _: Any = y + } + + locally { + val x: (A | Null) | B = ??? + val y = x + val _: AnyRef | Null = y + } + + locally { + val x: A | (Null | C) = ??? + val y = x // after simplification before widenUnion, the type of x would become A | Null + val _: A | Null = y + } + + locally { + val x: (A | Null) | (Null | B) = ??? + val y = x + val _: AnyRef | Null = y + } + + locally { + val x: (A | Null) & (B | Null) = ??? + val y = x + val _: (A & B) | Null = y + } +} diff --git a/tests/explicit-nulls/run/eq.scala b/tests/explicit-nulls/run/eq.scala new file mode 100644 index 000000000000..f93cf4245ac9 --- /dev/null +++ b/tests/explicit-nulls/run/eq.scala @@ -0,0 +1,25 @@ + +object Test { + + def main(args: Array[String]): Unit = { + val x: String|Null = "" + val y: String|Null = null + val z: String = "" + + assert(x == x) + assert(x == "") + assert("" == x) + assert(x == z) + assert(z == x) + assert(x != "xx") + assert(x != y) + assert(y == y) + assert(z.asInstanceOf[String|Null] != null) + assert(z.asInstanceOf[Any] != null) + + assert(x != null) + assert(y == null) + assert(null == y) + assert(null == null) + } +} diff --git a/tests/explicit-nulls/run/flow.check b/tests/explicit-nulls/run/flow.check new file mode 100644 index 000000000000..02a42f1b5dd8 --- /dev/null +++ b/tests/explicit-nulls/run/flow.check @@ -0,0 +1 @@ +npe diff --git a/tests/explicit-nulls/run/flow.scala b/tests/explicit-nulls/run/flow.scala new file mode 100644 index 000000000000..7fa56e046c5a --- /dev/null +++ b/tests/explicit-nulls/run/flow.scala @@ -0,0 +1,30 @@ +// Test that flow-sensitive type inference handles +// early exists from blocks. +object Test { + def main(args: Array[String]): Unit = { + check("hello") + check("world") + check2("blocks") + try { + check(null) + } catch { + case npe: NullPointerException => + println("npe") + } + } + + def err(msg: String) = throw new NullPointerException(msg) + + def check(s: String|Null): String = { + if (s == null) err("null argument!") + s + } + + // Test that flow info is propagated to vals, but not to defs. + def check2(s: String|Null): String = { + if (s == null) err("null argument") + val s2 = s + def s3 = s.nn // need the "nn" + s2 ++ s3 + } +} diff --git a/tests/explicit-nulls/run/generic-java-array-src/JA.java b/tests/explicit-nulls/run/generic-java-array-src/JA.java new file mode 100644 index 000000000000..ccca309d4f49 --- /dev/null +++ b/tests/explicit-nulls/run/generic-java-array-src/JA.java @@ -0,0 +1,13 @@ +class JA { + public static T get(T[] arr) { + return arr[0]; + } + + public static int getInt(int[] arr) { + return arr[0]; + } + + public static boolean getBool(boolean[] arr) { + return arr[0]; + } +} diff --git a/tests/explicit-nulls/run/generic-java-array-src/Test.scala b/tests/explicit-nulls/run/generic-java-array-src/Test.scala new file mode 100644 index 000000000000..22cc5ea1eb91 --- /dev/null +++ b/tests/explicit-nulls/run/generic-java-array-src/Test.scala @@ -0,0 +1,21 @@ +object Test { + def main(args: Array[String]): Unit = { + // This test shows that if we have a Java method that takes a generic array, + // then on the Scala side we'll need to pass a nullable array. + // i.e. with explicit nulls the previously-implicit cast becomes an explicit + // type annotation. + val x = new Array[Int|Null](1) + x(0) = 10 + println(JA.get(x)) + + // However, if the Java method takes an array that's explicitly of a value type, + // then we can pass a non-nullable array from the Scala side. + val intArr = new Array[Int](1) + intArr(0) = 20 + println(JA.getInt(intArr)) + + val boolArr = new Array[Boolean](1) + boolArr(0) = true + println(JA.getBool(boolArr)) + } +} diff --git a/tests/explicit-nulls/run/instanceof-nothing.scala b/tests/explicit-nulls/run/instanceof-nothing.scala new file mode 100644 index 000000000000..e51aabc7fe00 --- /dev/null +++ b/tests/explicit-nulls/run/instanceof-nothing.scala @@ -0,0 +1,25 @@ +// Check that calling `asInstanceOf[Nothing]` throws a ClassCastException. +// In particular, the compiler needs access to the right method to throw +// the exception, and identifying the method uses some explicit nulls related +// logic (see ClassCastExceptionClass in Definitions.scala). +object Test { + def main(args: Array[String]): Unit = { + val x: String = "hello" + try { + val y: Nothing = x.asInstanceOf[Nothing] + assert(false) + } catch { + case e: ClassCastException => + // ok + } + + val n: Null = null + try { + val y: Nothing = n.asInstanceOf[Nothing] + assert(false) + } catch { + case e: ClassCastException => + // ok + } + } +} diff --git a/tests/explicit-nulls/run/interop-unsound-src/J.java b/tests/explicit-nulls/run/interop-unsound-src/J.java new file mode 100644 index 000000000000..e06b22c3bae2 --- /dev/null +++ b/tests/explicit-nulls/run/interop-unsound-src/J.java @@ -0,0 +1,17 @@ + +class JavaBox { + T contents; + + JavaBox(T contents) { this.contents = contents; } +} + +class Forwarder { + + static > void putInJavaBox(T box, String s) { + box.contents = s; + } + + static > void putInScalaBox(T box, String s) { + box.setContents(s); + } +} diff --git a/tests/explicit-nulls/run/interop-unsound-src/S.scala b/tests/explicit-nulls/run/interop-unsound-src/S.scala new file mode 100644 index 000000000000..2e5eca0c1e5b --- /dev/null +++ b/tests/explicit-nulls/run/interop-unsound-src/S.scala @@ -0,0 +1,33 @@ +// An example that shows that the nullability transform is unsound. + +class ScalaBox[T](init: T) { + var contents: T = init + + def setContents(c: T): Unit = { + contents = c + } +} + +object Test { + + def main(args: Array[String]): Unit = { + val jb: JavaBox[String] = new JavaBox("hello") + val sb: ScalaBox[String] = ScalaBox("world") + + Forwarder.putInJavaBox(jb, null) // not unsound, becase JavaBox is java-defined + // so the contents fields will have a nullable + // type + + Forwarder.putInScalaBox(sb, null) // this is unsound, because ScalaBox + // should contain only Strings, but we added + // a null + + try { + sb.contents.length + assert(false) + } catch { + case ex: NullPointerException => + // expected + } + } +} diff --git a/tests/explicit-nulls/run/java-null.scala b/tests/explicit-nulls/run/java-null.scala new file mode 100644 index 000000000000..39eb1668d9d5 --- /dev/null +++ b/tests/explicit-nulls/run/java-null.scala @@ -0,0 +1,17 @@ +// Check that selecting a member from a `JavaNull`able union is unsound. + +object Test { + def main(args: Array[String]): Unit = { + val s: String|JavaNull = "hello" + assert(s.length == 5) + + val s2: String|JavaNull = null + try { + s2.length // should throw + assert(false) + } catch { + case e: NullPointerException => + // ok: selecting on a JavaNull can throw + } + } +} diff --git a/tests/explicit-nulls/run/nn.scala b/tests/explicit-nulls/run/nn.scala new file mode 100644 index 000000000000..3ffff69649cf --- /dev/null +++ b/tests/explicit-nulls/run/nn.scala @@ -0,0 +1,21 @@ +// Test the `nn` extension method for removing nullability. +object Test { + def len(x: Array[String]|Null): Unit = x.nn.length + def load(x: Array[String]|Null): Unit = x.nn(0) + + def assertThrowsNPE(x: => Any) = try { + x; + assert(false) // failed to throw NPE + } catch { case _: NullPointerException => } + + def main(args: Array[String]): Unit = { + assert(42.nn == 42) + val x: String|Null = "hello" + assert(x.nn == "hello") + val y: String|Null = null + assertThrowsNPE(y.nn) + assertThrowsNPE(null.nn) + assertThrowsNPE(len(null)) + assertThrowsNPE(load(null)) + } +} diff --git a/tests/explicit-nulls/run/while-loop.check b/tests/explicit-nulls/run/while-loop.check new file mode 100644 index 000000000000..01e79c32a8c9 --- /dev/null +++ b/tests/explicit-nulls/run/while-loop.check @@ -0,0 +1,3 @@ +1 +2 +3 diff --git a/tests/explicit-nulls/run/while-loop.scala b/tests/explicit-nulls/run/while-loop.scala new file mode 100644 index 000000000000..f4b44d749c3f --- /dev/null +++ b/tests/explicit-nulls/run/while-loop.scala @@ -0,0 +1,12 @@ + +object Test { + class C(val x: Int, val next: C|Null) + + def main(args: Array[String]): Unit = { + var xs: C|Null = C(1, C(2, C(3, null))) + while (xs != null) { + println(xs.x) + xs = xs.next + } + } +} diff --git a/tests/pos/notNull.scala b/tests/pos-special/notNull.scala similarity index 96% rename from tests/pos/notNull.scala rename to tests/pos-special/notNull.scala index 3d46fe658948..bcc0154b0a80 100644 --- a/tests/pos/notNull.scala +++ b/tests/pos-special/notNull.scala @@ -1,4 +1,3 @@ -trait Null extends Any object Test with def notNull[A](x: A | Null): x.type & A = assert(x != null) diff --git a/tests/pos-special/nullable.scala b/tests/pos-special/nullable.scala deleted file mode 100644 index d2d68f0dcfd1..000000000000 --- a/tests/pos-special/nullable.scala +++ /dev/null @@ -1,66 +0,0 @@ -trait T { def f: Int } -def impossible(x: Any): Unit = - val y = x - -def test: Unit = - val x, x2, x3, x4 = "" - - if x != null then - if x == null then impossible(new T{}) - - if x == null then () - else - if x == null then impossible(new T{}) - - if x == null || { - if x == null then impossible(new T{}) - true - } - then () - - if x != null && { - if x == null then impossible(new T{}) - true - } - then () - - if !(x == null) && { - if x == null then impossible(new T{}) - true - } - then () - - x match - case _: String => - if x == null then impossible(new T{}) - - val y: Any = List(x) - y match - case y1 :: ys => if y == null then impossible(new T{}) - case Some(_) | Seq(_: _*) => if y == null then impossible(new T{}) - - x match - case null => - case _ => if x == null then impossible(new T{}) - - if x == null then return - if x == null then impossible(new T{}) - - if x2 == null then throw AssertionError() - if x2 == null then impossible(new T{}) - - if !(x3 != null) then throw AssertionError() - if x3 == null then impossible(new T{}) - - assert(x4 != null) - if x4 == null then impossible(new T{}) - - class C(val x: Int, val next: C) - var xs: C = C(1, C(2, null)) - while xs != null do - if xs == null then println("?") - // looking at this with -Xprint-frontend -Xprint-types shows that the - // type of `xs == null` is indeed `false`. We cannot currently use this in a test - // since `xs == null` is not technically a pure expression since `xs` is not a path. - // We should test variable tracking once this is integrated with explicit not null types. - xs = xs.next diff --git a/tests/pos/interop-tostring.scala b/tests/pos/interop-tostring.scala new file mode 100644 index 000000000000..6d4798badfa2 --- /dev/null +++ b/tests/pos/interop-tostring.scala @@ -0,0 +1,8 @@ + +// Check that the return type of toString() isn't nullable. +class Foo { + + val x: java.lang.Integer = 42 + val s: String = x.toString() + +} diff --git a/tests/pos/interop-type-field.scala b/tests/pos/interop-type-field.scala new file mode 100644 index 000000000000..69c5fadef819 --- /dev/null +++ b/tests/pos/interop-type-field.scala @@ -0,0 +1,5 @@ + +class S { + // verify that the special TYPE field is non-nullable + val x: Class[Integer] = java.lang.Integer.TYPE +}