diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 8b053263ed00..d78f1cbd8b3d 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -162,6 +162,7 @@ class ScalaSettings extends Settings.SettingGroup { // Extremely experimental language features val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting("-Yno-kind-polymorphism", "Enable kind polymorphism (see https://dotty.epfl.ch/docs/reference/kind-polymorphism.html). Potentially unsound.") + val YexplicitNulls: Setting[Boolean] = BooleanSetting("-Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.") /** Area-specific debug output */ val YexplainLowlevel: Setting[Boolean] = BooleanSetting("-Yexplain-lowlevel", "When explaining type errors, show types at a lower level.") diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index bbcfe509fd21..0a1adb9baf9c 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -37,6 +37,8 @@ import xsbti.AnalysisCallback import plugins._ import java.util.concurrent.atomic.AtomicInteger +import dotty.tools.dotc.core.FlowTyper.FlowFacts + object Contexts { private val (compilerCallbackLoc, store1) = Store.empty.newLocation[CompilerCallback]() @@ -47,7 +49,8 @@ object Contexts { private val (compilationUnitLoc, store6) = store5.newLocation[CompilationUnit]() private val (runLoc, store7) = store6.newLocation[Run]() private val (profilerLoc, store8) = store7.newLocation[Profiler]() - private val initialStore = store8 + private val (flowFactsLoc, store9) = store8.newLocation[FlowFacts](FlowTyper.emptyFlowFacts) + private val initialStore = store9 /** A context is passed basically everywhere in dotc. * This is convenient but carries the risk of captured contexts in @@ -207,6 +210,9 @@ object Contexts { /** The current compiler-run profiler */ def profiler: Profiler = store(profilerLoc) + /** The terms currently known to be non-null (in spite of their declared type) */ + def flowFacts: FlowFacts = store(flowFactsLoc) + /** The new implicit references that are introduced by this scope */ protected var implicitsCache: ContextualImplicits = null def implicits: ContextualImplicits = { @@ -421,6 +427,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 @@ -556,6 +565,10 @@ object Contexts { def setRun(run: Run): this.type = updateStore(runLoc, run) def setProfiler(profiler: Profiler): this.type = updateStore(profilerLoc, profiler) def setFreshNames(freshNames: FreshNameCreator): this.type = updateStore(freshNamesLoc, freshNames) + def addFlowFacts(facts: FlowFacts): this.type = { + assert(settings.YexplicitNulls.value) + updateStore(flowFactsLoc, store(flowFactsLoc) ++ facts) + } def setProperty[T](key: Key[T], value: T): this.type = setMoreProperties(moreProperties.updated(key, value)) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index df5f6194d8ed..6e405c186ca6 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 @@ -278,7 +278,7 @@ class Definitions { @tu lazy val ObjectClass: ClassSymbol = { val cls = ctx.requiredClass("java.lang.Object") assert(!cls.isCompleted, "race for completing java.lang.Object") - cls.info = ClassInfo(cls.owner.thisType, cls, AnyClass.typeRef :: Nil, newScope) + cls.info = ClassInfo(cls.owner.thisType, cls, AnyType :: Nil, newScope) cls.setFlag(NoInits | JavaDefined) // The companion object doesn't really exist, so it needs to be marked as @@ -295,8 +295,16 @@ class Definitions { @tu lazy val AnyRefAlias: TypeSymbol = enterAliasType(tpnme.AnyRef, ObjectType) def AnyRefType: TypeRef = AnyRefAlias.typeRef - @tu lazy val Object_eq: TermSymbol = enterMethod(ObjectClass, nme.eq, methOfAnyRef(BooleanType), Final) - @tu lazy val Object_ne: TermSymbol = enterMethod(ObjectClass, nme.ne, methOfAnyRef(BooleanType), Final) + @tu lazy val Object_eq: TermSymbol = { + // If explicit nulls is enabled, then we want to allow `(x: String).eq(null)`, so we need + // to adjust the signature of `eq` accordingly. + enterMethod(ObjectClass, nme.eq, methOfAnyRefOrNull(BooleanType), Final) + } + @tu lazy val Object_ne: TermSymbol = { + // If explicit nulls is enabled, then we want to allow `(x: String).ne(null)`, so we need + // to adjust the signature of `ne` accordingly. + enterMethod(ObjectClass, nme.ne, methOfAnyRefOrNull(BooleanType), Final) + } @tu lazy val Object_synchronized: TermSymbol = enterPolyMethod(ObjectClass, nme.synchronized_, 1, pt => MethodType(List(pt.paramRefs(0)), pt.paramRefs(0)), Final) @tu lazy val Object_clone: TermSymbol = enterMethod(ObjectClass, nme.clone_, MethodType(Nil, ObjectType), Protected) @@ -336,11 +344,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 @@ -508,12 +534,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 @@ -783,10 +813,29 @@ 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" :: + "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) + // convenient one-parameter method types def methOfAny(tp: Type): MethodType = MethodType(List(AnyType), tp) def methOfAnyVal(tp: Type): MethodType = MethodType(List(AnyValType), tp) - def methOfAnyRef(tp: Type): MethodType = MethodType(List(ObjectType), tp) + def methOfAnyRefOrNull(tp: Type): MethodType = MethodType(List(ObjectType.maybeNullable), tp) // Derived types @@ -947,8 +996,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. @@ -1292,18 +1349,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 6dcd33a15df5..4b1e30157005 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -450,8 +450,12 @@ object Flags { * is completed) */ val AfterLoadFlags: FlagSet = commonFlags( - FromStartFlags, AccessFlags, Final, AccessorOrSealed, LazyOrTrait, SelfName, JavaDefined) - + FromStartFlags, AccessFlags, Final, AccessorOrSealed, LazyOrTrait, SelfName, JavaDefined, + // We would like to add JavaEnumValue to this set so that we can correctly + // detect it in JavaNullInterop. However, JavaEnumValue is not initialized at this + // point, so we just make sure that all the "primitive" flags contained in JavaEnumValue + // are mentioned here as well. + Enum, StableRealizable) /** 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/FlowTyper.scala b/compiler/src/dotty/tools/dotc/core/FlowTyper.scala new file mode 100644 index 000000000000..bfd698fff643 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/core/FlowTyper.scala @@ -0,0 +1,235 @@ +package dotty.tools.dotc.core + +import dotty.tools.dotc.ast.tpd._ +import StdNames.nme +import dotty.tools.dotc.ast.Trees.{Apply, Block, If, Select, Ident} +import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.core.Constants.Constant +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Names.Name +import dotty.tools.dotc.core.Types.{NonNullTermRef, TermRef, Type} + +import scala.annotation.internal.sharable + +/** Flow-sensitive typer */ +object FlowTyper { + + /** A set of `TermRef`s known to be non-null at the current program point */ + type FlowFacts = Set[TermRef] + + /** The initial state where no `TermRef`s are known to be non-null */ + val emptyFlowFacts = Set.empty[TermRef] + + /** Tries to improve the precision of `tpe` using flow-sensitive type information. + * For nullability, is `tpe` is a `TermRef` declared as nullable but known to be non-nullable because of the + * contextual info, returns the non-nullable version of the type. + * If the precision of the type can't be improved, then returns the type unchanged. + */ + def refineType(tpe: Type)(implicit ctx: Context): Type = { + assert(ctx.explicitNulls) + tpe match { + case tref: TermRef if ctx.flowFacts.contains(tref) => NonNullTermRef(tref.prefix, tref.designator) + case _ => tpe + } + } + + /** Nullability facts inferred from a condition. + * @param ifTrue are the terms known to be non-null if the condition is true. + * @param ifFalse are the terms known to be non-null if the condition is false. + */ + case class Inferred(ifTrue: FlowFacts, ifFalse: FlowFacts) { + // Let `NN(e, true/false)` be the set of terms that are non-null if `e` evaluates to `true/false`. + // We can use De Morgan's laws to underapproximate `NN` via `Inferred`. + // e.g. say `e = e1 && e2`. Then if `e` is `false`, we know that either `!e1` or `!e2`. + // Let `t` be a term that is in both `NN(e1, false)` and `NN(e2, false)`. + // Then it follows that `t` must be in `NN(e, false)`. This means that if we set + // `Inferred(e1 && e2, false) = Inferred(e1, false) ∩ Inferred(e2, false)`, we'll have + // `Inferred(e1 && e2, false) ⊂ NN(e1 && e2, false)` (formally, we'd do a structural induction on `e`). + // This means that when we infer something we do so soundly. The methods below use this approach. + + /** If `this` corresponds to a condition `e1` and `other` to `e2`, calculate the inferred facts for `e1 && e2`. */ + def combineAnd(other: Inferred): Inferred = Inferred(ifTrue.union(other.ifTrue), ifFalse.intersect(other.ifFalse)) + + /** If `this` corresponds to a condition `e1` and `other` to `e2`, calculate the inferred facts for `e1 || e2`. */ + def combineOr(other: Inferred): Inferred = Inferred(ifTrue.intersect(other.ifTrue), ifFalse.union(other.ifFalse)) + + /** The inferred facts for the negation of this condition. */ + def negate: Inferred = Inferred(ifFalse, ifTrue) + } + + /** Analyze the tree for a condition `cond` to learn new flow facts. + * Supports ands, ors, and unary negation. + * + * Example: + * (1) + * ``` + * val x: String|Null = "foo" + * if (x != null) { + * // x: String in the "then" branch + * } + * ``` + * Notice that `x` must be stable for the above to work. + * + * Let NN(cond, true/false) be the set of paths (`TermRef`s) that we can infer to be non-null + * if `cond` is true/false, respectively. Then define NN by (basically De Morgan's laws): + * + * NN(p == null, true) = {} we also handle `eq` + * NN(p == null, false) = {p} if p is stable + * NN(p != null, true) = {p} if p is stable we also handle `ne` + * NN(p != null, false) = {} + * NN(p.isInstanceOf[Null], true) = {} + * NN(p.isInstanceOf[Null], false) = {p} if p is stable + * NN(A && B, true) = ∪(NN(A, true), NN(B, true)) + * NN(A && B, false) = ∩(NN(A, false), NN(B, false)) + * NN(A || B, true) = ∩(NN(A, true), NN(B, true)) + * NN(A || B, false) = ∪(NN(A, false), NN(B, false)) + * NN(!A, true) = NN(A, false) + * NN(!A, false) = NN(A, true) + * NN({S1; ...; Sn, cond}, true/false) = NN(cond, true/false) + * NN(cond, _) = {} otherwise + */ + def inferFromCond(cond: Tree)(implicit ctx: Context): Inferred = { + assert(ctx.explicitNulls) + /** Combine two sets of facts according to `op`. */ + def combine(lhs: Inferred, op: Name, rhs: Inferred): Inferred = { + op match { + case _ if op == nme.ZAND => lhs.combineAnd(rhs) + case _ if op == nme.ZOR => lhs.combineOr(rhs) + } + } + + val emptyFacts = Inferred(emptyFlowFacts, emptyFlowFacts) + val nullLit = tpd.Literal(Constant(null)) + + /** Recurse over a conditional to extract flow facts. */ + def recur(tree: Tree): Inferred = { + tree match { + // `==` and `!=` are methods in `Any`, so `x == y` gets desugared to + // `x.==(y)` + case Apply(Select(lhs, op), List(rhs)) => + if (op == nme.ZAND || op == nme.ZOR) combine(recur(lhs), op, recur(rhs)) + else if (op == nme.EQ || op == nme.NE) newFact(lhs, isEq = (op == nme.EQ), rhs) + else emptyFacts + // `eq` and `ne` are extension methods if the receiver is a nullable union, + // so `(x: String|Null) eq null` gets desugared to `eq(x)(null)`. + case Apply(Apply(Ident(op), List(lhs)), List(rhs)) => + if (op == nme.eq || op == nme.ne) newFact(lhs, isEq = (op == nme.eq), rhs) + else emptyFacts + // TODO(abeln): handle type test with argument that's not a subtype of `Null`. + // We could infer "non-null" in that case: e.g. `if (x.isInstanceOf[String]) { // x can't be null }` + // case TypeApply(Select(lhs, op), List(tArg)) if op == nme.isInstanceOf_ && tArg.tpe.isNullType => + // newFact(lhs, isEq = true, nullLit) + case Select(lhs, op) if op == nme.UNARY_! => recur(lhs).negate + case Block(_, expr) => recur(expr) + case inline: Inlined => recur(inline.expansion) + case typed: Typed => recur(typed.expr) // TODO(abeln): check that the type is `Boolean`? + case _ => emptyFacts + } + } + + /** Extract new facts from an expression `lhs = rhs` or `lhs != rhs` + * if either the lhs or rhs is the `null` literal. + */ + def newFact(lhs: Tree, isEq: Boolean, rhs: Tree): Inferred = { + def isNullLit(tree: Tree): Boolean = tree match { + case lit: Literal if lit.const.tag == Constants.NullTag => true + case _ => false + } + + def isStableTermRef(tree: Tree): Boolean = asStableTermRef(tree).isDefined + + def asStableTermRef(tree: Tree): Option[TermRef] = tree.tpe match { + case tref: TermRef if tref.isStable => Some(tref) + case _ => None + } + + val trefOpt = + if (isNullLit(lhs) && isStableTermRef(rhs)) asStableTermRef(rhs) + else if (isStableTermRef(lhs) && isNullLit(rhs)) asStableTermRef(lhs) + else None + + trefOpt match { + case Some(tref) => + // If `isEq`, then the condition is of the form `lhs == null`, + // in which case we know `lhs` is non-null if the condition is false. + if (!isEq) Inferred(Set(tref), emptyFlowFacts) + else Inferred(emptyFlowFacts, Set(tref)) + case _ => emptyFacts + } + } + + recur(cond) + } + + /** Infer flow-sensitive type information inside a condition. + * + * Specifically, if `cond` is of the form `lhs &&` or `lhs ||`, where the lhs has already been typed + * (and the rhs hasn't been typed yet), compute the non-null facts that must hold so that the rhs can + * execute. These facts can then be soundly assumed when typing the rhs, because boolean operators are + * short-circuiting. + * + * This is useful in e.g. + * ``` + * val x: String|Null = ??? + * if (x != null && x.length > 0) ... + * ``` + */ + def inferWithinCond(cond: Tree)(implicit ctx: Context): FlowFacts = { + assert(ctx.explicitNulls) + cond match { + case Select(lhs, op) if op == nme.ZAND || op == nme.ZOR => + val Inferred(ifTrue, ifFalse) = inferFromCond(lhs) + if (op == nme.ZAND) ifTrue + else ifFalse + case _ => emptyFlowFacts + } + } + + /** Infer flow-sensitive type information within a block. + * + * More precisely, if `s1; s2` are consecutive statements in a block, this returns + * a context with nullability facts that hold once `s1` has executed. + * The new facts can then be used to type `s2`. + * + * This is useful for e.g. + * ``` + * val x: String|Null = ??? + * if (x == null) return "foo" + * val y = x.length // x: String inferred + * ``` + * + * How can we obtain additional facts just from the fact that `s1` executed? + * This can happen if `s1` is of the form `If(cond, then, else)`, where `then` or + * `else` have non-local control flow. + * + * The following qualify as non-local: + * 1) a return + * 2) an expression of type `Nothing` (in particular, usages of `throw`) + * 3) a block where the last expression is non-local + * + * So, for example, if we know that `x` must be non-null if `cond` is true, and `else` is non-local, + * then in order for `s2` to execute `cond` must be true. We can thus soundly add `x` to our + * flow facts. + */ + def inferWithinBlock(stat: Tree)(implicit ctx: Context): FlowFacts = { + def isNonLocal(s: Tree): Boolean = s match { + case _: Return => true + case Block(_, expr) => isNonLocal(expr) + case _ => + // If the type is bottom (like the result of a `throw`), then we assume the statement + // won't finish executing. + s.tpe.isBottomType + } + + assert(ctx.explicitNulls) + stat match { + case If(cond, thenExpr, elseExpr) => + val Inferred(ifTrue, ifFalse) = inferFromCond(cond) + if (isNonLocal(thenExpr) && isNonLocal(elseExpr)) ifTrue ++ ifFalse + else if (isNonLocal(thenExpr)) ifFalse + else if (isNonLocal(elseExpr)) ifTrue + else emptyFlowFacts + case _ => emptyFlowFacts + } + } +} 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..5137fe213828 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala @@ -0,0 +1,156 @@ +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)(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 (sym.name == nme.TYPE_ || sym.isAllOf(Flags.JavaEnumValue)) + // 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 = { + // Fast version of Type::toJavaNullableUnion that doesn't check whether the type + // is already a union. + def toJavaNullableUnion(tpe: Type): Type = OrType(tpe, defn.JavaNullAliasType) + + tp match { + case tp: TypeRef if needsNull(tp) => toJavaNullableUnion(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)) toJavaNullableUnion(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 + toJavaNullableUnion(derivedAndType(tp, this(tp.tp1), this(tp.tp2))) + case tp: TypeParamRef if needsNull(tp) => toJavaNullableUnion(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..7b6e55efe47c --- /dev/null +++ b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala @@ -0,0 +1,144 @@ +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.{AndType, ClassInfo, ConstantType, OrType, Type, TypeBounds, TypeMap, TypeProxy} + +/** Defines operations on nullable types. */ +object NullOpsDecorator { + + implicit class NullOps(val self: Type) { + /** Is this type a reference to `Null`, possibly after aliasing? */ + def isNullType(implicit ctx: Context): Boolean = self.isRef(defn.NullClass) + + /** 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) + } + + /** Normalizes unions so that all `Null`s (or aliases to `Null`) appear to the right of + * all other types. + * e.g. `Null | (T1 | Null) | T2` => `T1 | T2 | Null` + * e.g. `JavaNull | (T1 | Null) | Null` => `T1 | JavaNull` + * + * Let `self` denote the current type: + * 1. If `self` is not a union, then the result is not a union and equal to `self`. + * 2. If `self` is a union then + * 2.1 If `self` does not contain `Null` as part of the union, then the result is `self`. + * 2.2 If `self` contains `Null` (resp `JavaNull`) as part of the union, let `self2` denote + * the same type as `self`, but where all instances of `Null` (`JavaNull`) in the union + * have been removed. Then the result is `self2 | Null` (`self2 | JavaNull`). + */ + def normNullableUnion(implicit ctx: Context): Type = { + var isUnion = false + var hasNull = false + var hasJavaNull = false + def strip(tp: Type): Type = tp match { + case tp @ OrType(lhs, rhs) => + isUnion = true + val llhs = strip(lhs) + val rrhs = strip(rhs) + if (rrhs.isNullType) llhs + else if (llhs.isNullType) rrhs + else tp.derivedOrType(llhs, rrhs) + case _ => + if (tp.isNullType) { + if (tp.isJavaNullType) hasJavaNull = true + else hasNull = true + } + tp + } + val tp = strip(self) + if (!isUnion) self + else if (hasJavaNull) OrType(tp, defn.JavaNullAliasType) + else if (hasNull) OrType(tp, defn.NullType) + else self + } + + /** Is self (after widening and dealiasing) a type of the form `T | Null`? */ + def isNullableUnion(implicit ctx: Context): Boolean = { + assert(ctx.explicitNulls) + self.widenDealias.normNullableUnion match { + case OrType(_, rhs) => rhs.isNullType + case _ => false + } + } + + /** Is self (after widening and dealiasing) a type of the form `T | JavaNull`? */ + def isJavaNullableUnion(implicit ctx: Context): Boolean = { + assert(ctx.explicitNulls) + self.widenDealias.normNullableUnion match { + case OrType(_, rhs) => rhs.isJavaNullType + case _ => false + } + } + + /** Is this type guaranteed not to have `null` as a value? */ + final def isNotNull(implicit ctx: Context): Boolean = self match { + case tp: ConstantType => tp.value.value != null + case tp: ClassInfo => !tp.cls.isNullableClass && tp.cls != defn.NothingClass + case tp: TypeBounds => tp.lo.isNotNull + case tp: TypeProxy => tp.underlying.isNotNull + case AndType(tp1, tp2) => tp1.isNotNull || tp2.isNotNull + case OrType(tp1, tp2) => tp1.isNotNull && tp2.isNotNull + case _ => false + } + + def maybeNullable(implicit ctx: Context): Type = + if (ctx.explicitNulls) OrType(self, defn.NullType) else self + + /** Syntactically strips the nullability from this type. + * If the normalized form (as per `normNullableUnion`) of this type is `T1 | ... | Tn-1 | Tn`, + * and `Tn` references to `Null` (or `JavaNull`), then return `T1 | ... | Tn-1`. + * If this type isn't (syntactically) nullable, then returns the type unchanged. + */ + def stripNull(implicit ctx: Context): Type = { + assert(ctx.explicitNulls) + self.widenDealias.normNullableUnion match { + case OrType(lhs, rhs) if rhs.isNullType => lhs + case _ => self + } + } + + /** Like `stripNull`, but removes only the `JavaNull`s. */ + def stripJavaNull(implicit ctx: Context): Type = { + assert(ctx.explicitNulls) + self.widenDealias.normNullableUnion match { + case OrType(lhs, rhs) if rhs.isJavaNullType => lhs + case _ => self + } + } + + /** Collapses all `JavaNull` unions within this type, and not just the outermost ones (as `stripJavaNull` does). + * e.g. (Array[String|Null]|Null).stripNull => Array[String|Null] + * (Array[String|Null]|Null).stripInnerNulls => Array[String] + * If no `JavaNull` unions are found within the type, then returns the input type unchanged. + */ + def stripAllJavaNull(implicit ctx: Context): Type = { + assert(ctx.explicitNulls) + var diff = false + object RemoveNulls extends TypeMap { + override def apply(tp: Type): Type = + tp.normNullableUnion match { + case OrType(lhs, rhs) if rhs.isJavaNullType => + diff = true + mapOver(lhs) + case _ => mapOver(tp) + } + } + val rem = RemoveNulls(self.widenDealias) + if (diff) rem else self + } + + /** Injects this type into a union with `JavaNull`. */ + def toJavaNullableUnion(implicit ctx: Context): Type = { + assert(ctx.explicitNulls) + if (self.isJavaNullableUnion) self + else OrType(self, defn.JavaNullAliasType) + } + } +} diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index b4983df55b0b..56d5cabf9c47 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -196,6 +196,7 @@ object StdNames { final val Mirror: N = "Mirror" final val Nothing: N = "Nothing" 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 50b940ec9211..383b49248950 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -786,8 +786,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..ebecb2980a2c 100644 --- a/compiler/src/dotty/tools/dotc/core/Symbols.scala +++ b/compiler/src/dotty/tools/dotc/core/Symbols.scala @@ -383,6 +383,15 @@ 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(pathes: List[PreName]): List[ClassSymbol] = + pathes.foldLeft(List.empty){ case (acc, path) => getClassIfDefined(path) match { + case cls: ClassSymbol => cls :: acc + case _ => acc + }} + /** 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 a881b361d553..edc1b8765ff0 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 { @@ -1606,9 +1607,18 @@ 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. + lazy val formal2IsObject = + if (ctx.explicitNulls) formal2.isNullableUnion && formal2.stripNull(ctx).isRef(ObjectClass) + else formal2.isRef(ObjectClass) + lazy val formal1IsObject = + if (ctx.explicitNulls) formal1.isNullableUnion && formal1.stripNull(ctx).isRef(ObjectClass) + else formal1.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 fb0f712a3e8f..ac1349c95d91 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._ @@ -272,17 +273,6 @@ object Types { loop(this) } - /** Is this type guaranteed not to have `null` as a value? */ - final def isNotNull(implicit ctx: Context): Boolean = this match { - case tp: ConstantType => tp.value.value != null - case tp: ClassInfo => !tp.cls.isNullableClass && tp.cls != defn.NothingClass - case tp: TypeBounds => tp.lo.isNotNull - case tp: TypeProxy => tp.underlying.isNotNull - case AndType(tp1, tp2) => tp1.isNotNull || tp2.isNotNull - case OrType(tp1, tp2) => tp1.isNotNull && tp2.isNotNull - case _ => false - } - /** Is this type produced as a repair for an error? */ final def isError(implicit ctx: Context): Boolean = stripTypeVar.isInstanceOf[ErrorType] @@ -598,11 +588,20 @@ 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) + if (ctx.explicitNulls && tp.isJavaNullableUnion) { + // 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. + tp.stripJavaNull.findMember(name, pre.stripJavaNull, required, excluded) + } + else { + // 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) + } case tp: JavaArrayType => defn.ObjectType.findMember(name, pre, required, excluded) case err: ErrorType => @@ -1066,21 +1065,60 @@ 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 OrType(tp1, tp2) => - ctx.typeComparer.lub(tp1.widenUnion, tp2.widenUnion, canConstrain = true) match { - case union: OrType => union.join - case res => res - } - case tp @ AndType(tp1, tp2) => - tp derived_& (tp1.widenUnion, tp2.widenUnion) - case tp: RefinedType => - tp.derivedRefinedType(tp.parent.widenUnion, tp.refinedName, tp.refinedInfo) - case tp: RecType => - tp.rebind(tp.parent.widenUnion) - case tp => - tp + def widenUnion(implicit ctx: Context): Type = { + widen match { + case tp @ OrType(lhs, rhs) => + def defaultJoin(tp1: Type, tp2: Type) = + ctx.typeComparer.lub(tp1, tp2, canConstrain = true) match { + case union: OrType => union.join + case res => res + } + + // Given a type `tpe`, if it is already a nullable union, return it unchanged. + // Otherwise, construct a nullable union where `tpe` is the lhs (use `orig` to + // potentially avoid creating a new object for the union). + def ensureNullableUnion(tpe: Type, orig: OrType): Type = tpe match { + case orTpe: OrType if orTpe.tp2.isNullType => tpe + case _ => orig.derivedOrType(tpe, defn.NullType) + } + + // Test for nullable union that assumes the type has already been normalized. + def isNullableUnionFast(tp: Type): Boolean = tp match { + case orTpe: OrType if orTpe.tp2.isNullType => true + case _ => false + } + + if (ctx.explicitNulls) { + // Don't widen `T|Null`, since otherwise we wouldn't be able to infer nullable unions. + // This part relies on the postcondition of widenUnion: the result is either a + // non-union type, or a nullable union type where the rhs is `Null` type. + if (rhs.isNullType) ensureNullableUnion(lhs.widenUnion, tp) + else if (lhs.isNullType) ensureNullableUnion(rhs.widenUnion, tp) + else { + val lhsWiden = lhs.widenUnion + val rhsWiden = rhs.widenUnion + val tmpRes = defaultJoin(lhs.widenUnion, rhs.widenUnion) + if (isNullableUnionFast(lhsWiden) || isNullableUnionFast(rhsWiden)) + // If either lhs or rhs is a nullable union, + // we need to ensure the result is also a nullable union. + ensureNullableUnion(tmpRes, tp) + else tmpRes + } + } + else defaultJoin(lhs.widenUnion, rhs.widenUnion) + case tp @ AndType(tp1, tp2) => + tp derived_& (tp1.widenUnion, tp2.widenUnion) + case tp: RefinedType => + tp.derivedRefinedType(tp.parent.widenUnion, tp.refinedName, tp.refinedInfo) + case tp: RecType => + tp.rebind(tp.parent.widenUnion) + case tp => + tp + } } /** Widen all top-level singletons reachable by dealiasing @@ -1886,18 +1924,18 @@ object Types { else computeDenot } - private def computeDenot(implicit ctx: Context): Denotation = { + protected def finish(d: Denotation)(implicit ctx: Context): Denotation = { + if (d.exists) + // Avoid storing NoDenotations in the cache - we will not be able to recover from + // them. The situation might arise that a type has NoDenotation in some later + // phase but a defined denotation earlier (e.g. a TypeRef to an abstract type + // is undefined after erasure.) We need to be able to do time travel back and + // forth also in these cases. + setDenot(d) + d + } - def finish(d: Denotation) = { - if (d.exists) - // Avoid storing NoDenotations in the cache - we will not be able to recover from - // them. The situation might arise that a type has NoDenotation in some later - // phase but a defined denotation earlier (e.g. a TypeRef to an abstract type - // is undefined after erasure.) We need to be able to do time travel back and - // forth also in these cases. - setDenot(d) - d - } + private def computeDenot(implicit ctx: Context): Denotation = { def fromDesignator = designator match { case name: Name => @@ -2197,7 +2235,7 @@ object Types { /** A reference like this one, but with the given symbol, if it exists */ final def withSym(sym: Symbol)(implicit ctx: Context): ThisType = - if ((designator ne sym) && sym.exists) NamedType(prefix, sym).asInstanceOf[ThisType] + if ((designator ne sym) && sym.exists) newLikeThis(prefix, sym).asInstanceOf[ThisType] else this /** A reference like this one, but with the given denotation, if it exists. @@ -2244,10 +2282,10 @@ object Types { d = disambiguate(d, if (lastSymbol.signature == Signature.NotAMethod) Signature.NotAMethod else lastSymbol.asSeenFrom(prefix).signature) - NamedType(prefix, name, d) + newLikeThis(prefix, name, d) } if (prefix eq this.prefix) this - else if (lastDenotation == null) NamedType(prefix, designator) + else if (lastDenotation == null) newLikeThis(prefix, designator) else designator match { case sym: Symbol => if (infoDependsOnPrefix(sym, prefix) && !prefix.isArgPrefixOf(sym)) { @@ -2256,10 +2294,10 @@ object Types { // A false override happens if we rebind an inner class to another type with the same name // in an outer subclass. This is wrong, since classes do not override. We need to // return a type with the existing class info as seen from the new prefix instead. - if (falseOverride) NamedType(prefix, sym.name, denot.asSeenFrom(prefix)) + if (falseOverride) newLikeThis(prefix, sym.name, denot.asSeenFrom(prefix)) else candidate } - else NamedType(prefix, sym) + else newLikeThis(prefix, sym) case name: Name => reload() } } @@ -2282,6 +2320,12 @@ object Types { } override def eql(that: Type): Boolean = this eq that // safe because named types are hash-consed separately + + protected def newLikeThis(prefix: Type, designator: Designator)(implicit ctx: Context): NamedType = + NamedType(prefix, designator) + + protected def newLikeThis(prefix: Type, designator: Name, denot: Denotation)(implicit ctx: Context): NamedType = + NamedType(prefix, designator, denot) } /** A reference to an implicit definition. This can be either a TermRef or a @@ -2298,7 +2342,7 @@ object Types { private var myDesignator: Designator) extends NamedType with SingletonType with ImplicitRef { - type ThisType = TermRef + type ThisType >: this.type <: TermRef type ThisName = TermName override def designator: Designator = myDesignator @@ -2353,6 +2397,34 @@ object Types { myHash = hc } + /** A `TermRef` that, through flow-sensitive type inference, we know is non-null. + * Accordingly, the `info` in its denotation won't be of the form `T|Null`. + * + * This class is cached differently from regular `TermRef`s. Regular `TermRef`s use the + * `uniqueNameTypes` map in the context, while these non-null `TermRef`s use + * the generic `uniques` map. This is so that regular `TermRef`s can continue to use + * a "fast path", since non-null `TermRef`s are not very common. + */ + final class NonNullTermRef(prefix: Type, designator: Designator) extends TermRef(prefix, designator) { + override type ThisType = NonNullTermRef + + override protected def finish(d: Denotation)(implicit ctx: Context): Denotation = + // If the denotation is computed for the first time, or if it's ever updated, make sure + // that the `info` is non-null. + super.finish(d.mapInfo(_.stripNull)) + + override protected def newLikeThis(prefix: Type, designator: Designator)(implicit ctx: Context): NamedType = + NonNullTermRef(prefix, designator) + + override protected def newLikeThis(prefix: Type, designator: Name, denot: Denotation)(implicit ctx: Context): NamedType = + NonNullTermRef(prefix, designator.asTermName, denot) + + override def eql(that: Type): Boolean = that match { + case that: NonNullTermRef => (this.prefix eq that.prefix) && (this.designator eq that.designator) + case _ => false + } + } + final class CachedTypeRef(prefix: Type, designator: Designator, hc: Int) extends TypeRef(prefix, designator) { assert((prefix ne NoPrefix) || designator.isInstanceOf[Symbol]) myHash = hc @@ -2384,9 +2456,11 @@ object Types { case sym: Symbol => sym.isType case name: Name => name.isTypeName } + def apply(prefix: Type, designator: Designator)(implicit ctx: Context): NamedType = if (isType(designator)) TypeRef.apply(prefix, designator) else TermRef.apply(prefix, designator) + def apply(prefix: Type, designator: Name, denot: Denotation)(implicit ctx: Context): NamedType = if (designator.isTermName) TermRef.apply(prefix, designator.asTermName, denot) else TypeRef.apply(prefix, designator.asTypeName, denot) @@ -2405,6 +2479,23 @@ object Types { apply(prefix, designatorFor(prefix, name, denot)).withDenot(denot) } + object NonNullTermRef { + // Notice these TermRefs are cached in a different map than the one used for + // regular TermRefs. The non-null TermRefs use the "slow" map, since they're less common. + // If we used the same map, then we'd end up replacing a regular TermRef by a non-null + // one with a different denotation. + + /** Create a non-null term ref with the given designator. */ + def apply(prefix: Type, desig: Designator)(implicit ctx: Context): NonNullTermRef = + unique(new NonNullTermRef(prefix, desig)) + + /** Create a non-null term ref with given initial denotation. The name of the reference is taken + * from the denotation's symbol if the latter exists, or else it is the given name. + */ + def apply(prefix: Type, name: TermName, denot: Denotation)(implicit ctx: Context): NonNullTermRef = + apply(prefix, designatorFor(prefix, name, denot)).withDenot(denot) + } + object TypeRef { /** Create a type ref with given prefix and name */ diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index df28f90dd188..59c49dabb5d3 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -287,6 +287,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) + // seal java enums if (isEnum) { val enumClass = sym.owner.linkedClass 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 18a31b12cbf2..465c44d45388 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..00509e12e865 100644 --- a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala +++ b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala @@ -16,7 +16,7 @@ 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 dotty.tools.dotc.core.NullOpsDecorator._ import scala.collection.mutable class LazyVals extends MiniPhase with IdentityDenotTransformer { diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index 6930539ec990..762076ed6e9f 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -17,6 +17,7 @@ import config.Printers.patmatch import reporting.diagnostic.messages._ import dotty.tools.dotc.ast._ import util.Property._ +import NullOpsDecorator._ /** The pattern matching transform. * After this phase, the only Match nodes remaining in the code are simple switches 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/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index 4b31f5ae28cf..7099c0b26292 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -14,6 +14,7 @@ import util.Spans._ import reporting.diagnostic.messages.TypeTestAlwaysSucceeds import reporting.trace import config.Printers.{ transforms => debug } +import NullOpsDecorator._ /** This transform normalizes type tests and type casts, * also replacing type tests with singleton argument type with reference equality check 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/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index cc3641f1c94f..f14d7bdbea91 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -857,7 +857,20 @@ trait Applications extends Compatibility { typr.println(i"result failure for $tree with type ${fun1.tpe.widen}, expected = $pt") /** Type application where arguments come from prototype, and no implicits are inserted */ - def simpleApply(fun1: Tree, proto: FunProto)(implicit ctx: Context): Tree = + def simpleApply(fun1: Tree, proto: FunProto)(implicit ctx: Context): Tree = { + implicit val ctxWithFacts: Context = if (ctx.explicitNulls) { + // The flow facts of lhs are cached in the FlowFactsOnTree attachment + val facts = fun1.getAttachment(Typer.FlowFactsOnTree) match { + case Some(fs) => fs + case None => + val fs = FlowTyper.inferWithinCond(fun1)(ctx) + fun1.putAttachment(Typer.FlowFactsOnTree, fs) + fs + } + if (facts.isEmpty) ctx else ctx.fresh.addFlowFacts(facts) + } + else ctx + methPart(fun1).tpe match { case funRef: TermRef => val app = @@ -869,6 +882,7 @@ trait Applications extends Compatibility { case _ => handleUnexpectedFunType(tree, fun1) } + } /** Try same application with an implicit inserted around the qualifier of the function * part. Return an optional value to indicate success. diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 3d8583b78c6e..d4ac42f32219 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -776,14 +776,18 @@ class Namer { typer: Typer => def creationContext: Context = ctx ctx.typerState.markShared() - protected def typeSig(sym: Symbol): Type = original match { + protected def typeSig(sym: Symbol, ctx: Context): Type = original match { case original: ValDef => if (sym.is(Module)) moduleValSig(sym) - else valOrDefDefSig(original, sym, Nil, Nil, identity)(localContext(sym).setNewScope) + else { + val withNewScope = ctx.fresh.setOwner(sym).setTree(original).setNewScope + valOrDefDefSig(original, sym, Nil, Nil, identity)(withNewScope) + } case original: DefDef => val typer1 = ctx.typer.newLikeThis nestedTyper(sym) = typer1 - typer1.defDefSig(original, sym)(localContext(sym).setTyper(typer1)) + val withTyper = ctx.fresh.setOwner(sym).setTree(original).setTyper(typer1) + typer1.defDefSig(original, sym)(withTyper) case imp: Import => try { val expr1 = typedAheadExpr(imp.expr, AnySelectionProto) @@ -811,7 +815,8 @@ class Namer { typer: Typer => } else try - completeInCreationContext(denot) + // the default behaviour is to complete using creation context + completeInContext(denot, this.ctx) if (denot.isCompleted) registerIfChild(denot) catch case ex: CompilationUnit.SuspendException => @@ -896,14 +901,18 @@ class Namer { typer: Typer => } } - /** Intentionally left without `implicit ctx` parameter. We need - * to pick up the context at the point where the completer was created. + /** Intentionally left without `implicit ctx` parameter. + * We use the given ctx to complete this Completer. + * + * Normally, the creation context is passed, as passed in the complete method. + * However, if -Yexplicit-nulls, is enabled, we pass in the current context + * so that flow typing works with blocks. */ - def completeInCreationContext(denot: SymDenotation): Unit = { + def completeInContext(denot: SymDenotation, ctx: Context): Unit = { val sym = denot.symbol addAnnotations(sym) addInlineInfo(sym) - denot.info = typeSig(sym) + denot.info = typeSig(sym, ctx) invalidateIfClashingSynthetic(denot) Checking.checkWellFormed(sym) denot.info = avoidPrivateLeaks(sym) @@ -938,7 +947,7 @@ class Namer { typer: Typer => myTypeParams } - override protected def typeSig(sym: Symbol): Type = + override protected def typeSig(sym: Symbol, ctx: Context): Type = typeDefSig(original, sym, completerTypeParams(sym)(ictx))(nestedCtx) } @@ -1108,7 +1117,7 @@ class Namer { typer: Typer => } /** The type signature of a ClassDef with given symbol */ - override def completeInCreationContext(denot: SymDenotation): Unit = { + override def completeInContext(denot: SymDenotation, ctx: Context): Unit = { val parents = impl.parents /* The type of a parent constructor. Types constructor arguments @@ -1144,28 +1153,28 @@ class Namer { typer: Typer => * (4) If the class is sealed, it is defined in the same compilation unit as the current class */ def checkedParentType(parent: untpd.Tree): Type = { - val ptype = parentType(parent)(ctx.superCallContext).dealiasKeepAnnots + val ptype = parentType(parent)(this.ctx.superCallContext).dealiasKeepAnnots if (cls.isRefinementClass) ptype else { val pt = checkClassType(ptype, parent.sourcePos, traitReq = parent ne parents.head, stablePrefixReq = true) if (pt.derivesFrom(cls)) { val addendum = parent match { - case Select(qual: Super, _) if ctx.scala2Mode => + case Select(qual: Super, _) if this.ctx.scala2Mode => "\n(Note that inheriting a class of the same name is no longer allowed)" case _ => "" } - ctx.error(CyclicInheritance(cls, addendum), parent.sourcePos) + this.ctx.error(CyclicInheritance(cls, addendum), parent.sourcePos) defn.ObjectType } else { val pclazz = pt.typeSymbol if pclazz.is(Final) then - ctx.error(ExtendFinalClass(cls, pclazz), cls.sourcePos) + this.ctx.error(ExtendFinalClass(cls, pclazz), cls.sourcePos) else if pclazz.isEffectivelySealed && pclazz.associatedFile != cls.associatedFile then if pclazz.is(Sealed) then - ctx.error(UnableToExtendSealedClass(pclazz), cls.sourcePos) - else if ctx.settings.strict.value then + this.ctx.error(UnableToExtendSealedClass(pclazz), cls.sourcePos) + else if this.ctx.settings.strict.value then checkFeature(nme.adhocExtensions, i"Unless $pclazz is declared 'open', its extension in a separate file", cls.topLevelClass, @@ -1418,7 +1427,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) + else memTpe } /** The type signature of a DefDef with given symbol */ diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 505da8883248..8c7bd4e4dcd1 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -36,6 +36,7 @@ import util.Stats.record import config.Printers.{gadts, typr} import rewrites.Rewrites.patch import NavigateAST._ +import dotty.tools.dotc.core.FlowTyper.{FlowFacts, Inferred} import dotty.tools.dotc.transform.{PCPCheckAndHeal, Staging, TreeMapWithStages} import transform.SymUtils._ import transform.TypeUtils._ @@ -77,6 +78,11 @@ object Typer { * search was tried on a tree. This will in some cases be reported in error messages */ private[typer] val HiddenSearchFailure = new Property.Key[SearchFailure] + + /** An attachment that indicates the flow-sensitive type information + * inside a condition. + */ + private[typer] val FlowFactsOnTree = new Property.Key[FlowFacts] } @@ -411,11 +417,13 @@ class Typer extends Namer else errorType(new MissingIdent(tree, kind, name.show), tree.sourcePos) - val tree1 = ownType match { - case ownType: NamedType if !prefixIsElidable(ownType) => - ref(ownType).withSpan(tree.span) + val ownType1 = if (ctx.explicitNulls) FlowTyper.refineType(ownType) else ownType + + val tree1 = ownType1 match { + case ownType1: NamedType if !prefixIsElidable(ownType1) => + ref(ownType1).withSpan(tree.span) case _ => - tree.withType(ownType) + tree.withType(ownType1) } checkStableIdentPattern(tree1, pt) @@ -443,8 +451,9 @@ 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 = if (ctx.explicitNulls) select.withType(FlowTyper.refineType(select.tpe)) else select + if (select1.tpe ne TryDynamicCallType) ConstFold(checkStableIdentPattern(select1, pt)) + else if (pt.isInstanceOf[FunOrPolyProto] || pt == AssignProto) select1 else typedDynamicSelect(tree, Nil, pt) } @@ -754,8 +763,12 @@ class Typer extends Namer } } - def typedBlockStats(stats: List[untpd.Tree])(implicit ctx: Context): (Context, List[tpd.Tree]) = - (index(stats), typedStats(stats, ctx.owner)) + def typedBlockStats(stats: List[untpd.Tree])(implicit ctx: Context): (Context, List[tpd.Tree]) = { + val ctx1 = index(stats) + val (stats1, facts) = typedStatsAndGetFacts(stats, ctx.owner) + val ctx2 = if (ctx.explicitNulls && facts.nonEmpty) ctx1.fresh.addFlowFacts(facts) else ctx1 + (ctx2, stats1) + } def typedBlock(tree: untpd.Block, pt: Type)(implicit ctx: Context): Tree = { val localCtx = ctx.retractMode(Mode.Pattern) @@ -807,15 +820,24 @@ class Typer extends Namer if (tree.isInline) checkInInlineContext("inline if", tree.posd) val cond1 = typed(tree.cond, defn.BooleanType) + val (thenCtx, elseCtx) = if (ctx.explicitNulls) { + val Inferred(ifTrue, ifFalse) = FlowTyper.inferFromCond(cond1) + (ctx.fresh.addFlowFacts(ifTrue), ctx.fresh.addFlowFacts(ifFalse)) + } + else (ctx, ctx) + if (tree.elsep.isEmpty) { - val thenp1 = typed(tree.thenp, defn.UnitType) + val thenp1 = typed(tree.thenp, defn.UnitType)(thenCtx) val elsep1 = tpd.unitLiteral.withSpan(tree.span.endPos) cpy.If(tree)(cond1, thenp1, elsep1).withType(defn.UnitType) } else { - val thenp1 :: elsep1 :: Nil = harmonic(harmonize, pt)( - (tree.thenp :: tree.elsep :: Nil).map(typed(_, pt.dropIfProto))) - assignType(cpy.If(tree)(cond1, thenp1, elsep1), thenp1, elsep1) + val thenp2 :: elsep2 :: Nil = harmonic(harmonize, pt) { + val thenp1 = typed(tree.thenp, pt.dropIfProto)(thenCtx) + val elsep1 = typed(tree.elsep, pt.dropIfProto)(elseCtx) + thenp1 :: elsep1 :: Nil + } + assignType(cpy.If(tree)(cond1, thenp2, elsep2), thenp2, elsep2) } } @@ -2168,20 +2190,57 @@ class Typer extends Namer trees mapconserve (typed(_)) def typedStats(stats: List[untpd.Tree], exprOwner: Symbol)(implicit ctx: Context): List[Tree] = { + val (stats1, _) = typedStatsAndGetFacts(stats, exprOwner) + stats1 + } + + def typedStatsAndGetFacts(stats: List[untpd.Tree], exprOwner: Symbol)(implicit ctx: Context): (List[Tree], FlowFacts) = { val buf = new mutable.ListBuffer[Tree] val enumContexts = new mutable.HashMap[Symbol, Context] // A map from `enum` symbols to the contexts enclosing their definitions - @tailrec def traverse(stats: List[untpd.Tree])(implicit ctx: Context): List[Tree] = stats match { + @tailrec def traverse(stats: List[untpd.Tree], facts: FlowFacts)(implicit ctx: Context): (List[Tree], FlowFacts) = stats match { case (imp: untpd.Import) :: rest => val imp1 = typed(imp) buf += imp1 - traverse(rest)(ctx.importContext(imp, imp1.symbol)) + traverse(rest, facts)(ctx.importContext(imp, imp1.symbol)) case (mdef: untpd.DefTree) :: rest => mdef.removeAttachment(ExpandedTree) match { case Some(xtree) => - traverse(xtree :: rest) + traverse(xtree :: rest, facts) case none => - typed(mdef) match { + import untpd.modsDeco + // Force completer to use calling context (as opposed to the creation context) + // to complete itself. This approach is used so that flow typing can handle definitions + // appearing within a block. + // + // Example: + // Suppose we have a block containing + // 1. val x: String|Null = ??? + // 2. if (x == null) throw NPE + // 3. val y = x + // + // We want to infer y: String on line 3, but if the completer for `y` uses its creation + // context, then we won't have the additional flow facts that say that `y` is not null. + // + // The solution is to use the context containing more information about the statements above. + val ctx2 = if (ctx.explicitNulls && ctx.owner.is(Method)) { + // We cannot use mdef.symbol to get the symbol of the tree here, + // since the tree has not been completed and doesn't have a denotation. + mdef.getAttachment(SymOfTree).map(s => (s, s.infoOrCompleter)) match { + case Some((sym, completer: Namer#Completer)) => + val ctx1 = ctx.fresh.addFlowFacts(facts) + completer.completeInContext(sym, ctx1) + ctx1 + case _ => + // If it has been completed, then it must be because there is a forward reference + // to the definition in the program. We use the default (creation) context, and flow + // typing will not be applied. + ctx + } + } + else ctx + + typed(mdef)(ctx2) 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 @@ -2194,21 +2253,31 @@ class Typer extends Namer case mdef1 => buf += mdef1 } - traverse(rest) + traverse(rest, facts) } case Thicket(stats) :: rest => - traverse(stats ++ rest) + traverse(stats ++ rest, facts) case (stat: untpd.Export) :: rest => buf ++= stat.attachmentOrElse(ExportForwarders, Nil) // no attachment can happen in case of cyclic references - traverse(rest) + traverse(rest, facts) case stat :: rest => - val stat1 = typed(stat)(ctx.exprContext(stat, exprOwner)) - checkStatementPurity(stat1)(stat, exprOwner) + val ctx1 = if (ctx.explicitNulls && facts.nonEmpty) { + ctx.fresh.addFlowFacts(facts) + } else { + ctx + } + val stat1 = typed(stat)(ctx1.exprContext(stat, exprOwner)) + checkStatementPurity(stat1)(stat, exprOwner)(ctx1) buf += stat1 - traverse(rest) + val newFacts = if (ctx.explicitNulls) { + facts ++ FlowTyper.inferWithinBlock(stat1)(ctx1) + } else { + facts + } + traverse(rest, newFacts)(ctx) case nil => - buf.toList + (buf.toList, facts) } val localCtx = { val exprOwnerOpt = if (exprOwner == ctx.owner) None else Some(exprOwner) @@ -2225,9 +2294,10 @@ class Typer extends Namer case _ => stat } - val stats1 = traverse(stats)(localCtx).mapConserve(finalize) - if (ctx.owner == exprOwner) checkNoAlphaConflict(stats1) - stats1 + val (stats1, ctx1) = traverse(stats, FlowTyper.emptyFlowFacts)(localCtx) + val stats2 = stats1.mapConserve(finalize) + if (ctx.owner == exprOwner) checkNoAlphaConflict(stats2) + (stats2, ctx1) } /** Given an inline method `mdef`, the method rewritten so that its body diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 78915d3ce2f8..37ca6360152d 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -254,6 +254,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 67f678be55eb..56f413d18359 100644 --- a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala +++ b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala @@ -66,4 +66,7 @@ object TestConfiguration { val scala2Mode = defaultOptions and "-language:Scala2" 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..4ad817ce5eeb --- /dev/null +++ b/docs/docs/internals/explicit-nulls.md @@ -0,0 +1,186 @@ +--- +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) + 4. a module for "flow typing", so we can work more naturally with nullable values + +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 +------------ +TODO(abeln): add support for recognizing nullability annotations a la +https://kotlinlang.org/docs/reference/java-interop.html#nullability-annotations + +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 `JavaNullInterop.scala`, a new file. + +The entry point is the function `def nullifyMember(sym: Symbol, tp: Type)(implicit ctx: Context): Type` +which, given a symbol and its "regular" type, produces what the type of the symbol should be in the +explicit nulls world. + +In order to nullify a member, we first pass it through a "whitelist" of symbols that need +special handling (e.g. `constructors`, which never return `null`). If none of the "policies" in the +whitelist apply, we then process the symbol with a `TypeMap` that implements the following nullification +function `n`: + 1. n(T) = T|JavaNull if T is a reference type + 2. n(T) = T if T is a value type + 3. n(T) = T|JavaNull if T is a type parameter + 4. n(C[T]) = C[T]|JavaNull if C is Java-defined + 5. n(C[T]) = C[n(T)]|JavaNull if C is Scala-defined + 6. n(A|B) = n(A)|n(B)|JavaNull + 7. n(A&B) = (n(A)&n(B))|JavaNull + 8. n((A1, ..., Am)R) = (n(A1), ..., n(Am))n(R) for a method with arguments (A1, ..., Am) and return type R + 9. n(T) = T otherwise + +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`. +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 a few utility methods to work with nullable unions. All of these +are methods of the `Type` class, so call them with `this` as a receiver: + - `isNullableUnion` determines whether `this` is a nullable union. Here, what constitutes + a nullable union is determined purely syntactically: + 1. first we "normalize" `this` (see below) + 2. if the result is of the form `T | Null`, then the type is considered a nullable union. + Otherwise, it isn't. + - `isJavaNullableUnion` determines whether `this` is syntactically a union of the form `T|JavaNull` + - `normNullableUnion` normalizes `this` as follows: + 1. if `this` is not a nullable union, it's returned unchanged. + 2. if `this` is a union, then it's re-arranged so that all the `Null`s are to the right of all + the non-`Null`s. + - `stripNull` syntactically strips nullability from `this`: e.g. `String|Null => String`. Notice this + works only at the "top level": e.g. if we have an `Array[String|Null]|Null` and we call `stripNull` + we'll get `Array[String|Null]` (only the outermost nullable union was removed). + - `stripAllJavaNull` is like `stripNull` but removes _all_ nullable unions in the type (and only works + for `JavaNull`). This is needed when we want to "revert" the Java nullification function. + +Flow Typing +----------- +Flow typing is needed so we can work with nullable unions in a more natural way. +The following is a common idiom that should work without additional casts: +```scala +val x: String|Null = ??? +if (x != null && x.length < 10) +``` +This is implemented as a "must be null in the current scope" analysis on stable paths: + - we add additional state to the `Context` in `Contexts.scala`. + Specifically, we add a set of `FlowFacts` (right now just a set of `TermRef`s), which + are the paths known to be non-nullable in the current scope. + - the bulk of the flow typing logic lives in a new `FlowTyper.scala` file. + + There are four entry points to `FlowTyper`: + 1. `inferFromCond(cond: Tree): Inferred`: given a tree representing a condition such as + `x != null && x.length < 10`, return the `Inferred` facts. + + In turn, `Inferred` is defined as `case class Inferred(ifTrue: FlowFacts, ifFalse: FlowFacts)`. + That is, `Inferred` contains the paths that _must_ be non-null if the condition is true and, + separately, the paths that must be non-null if the condition is false. + + e.g. for `x != null` we'd get `Inferred({x}, {})`, but only if `x` is stable. + However, if we had `x == null` we'd get `Inferred({}, {x})`. + + 2. `inferWithinCond(cond: Tree): FlowFacts`: given a condition of the form `lhs && rhs` or + `lhs || rhs`, calculate the paths that must be non-null for the rhs to execute (given + that these operations) are short-circuiting. + + 3. `inferWithinBlock(stat: Tree): FlowFacts`: if `stat` is a statement with a block, calculate + which paths must be non-null when the statement that _follows_ `stat` in the block executes. + This is so we can handle things like + ```scala + val x: String|Null = ??? + if (x == null) return + val y = x.length + ``` + Here, `inferWithinBlock(if (x == null) return)` gives back `{x}`, because we can tell that + the next statement will execute only if `x` is non-null. + + 4. `refineType(tpe: Type): Type`: given a type, refine it if possible using flow-sensitive type + information. This uses a `NonNullTermRef` (see below). + + - Each of the public APIs in `FlowTyper` is used to do flow typing in a different scenario + (but all the use sites of `FlowTyper` are in `Typer.scala`): + * `refineType` is used in `typedIdent` and `typedSelect` + * `inferFromCond` is used for typing if statements + * `inferWithinCond` is used when typing "applications" (which is how "&&" and "||" are encoded) + * `inferWithinBlock` is used when typing blocks + + For example, to do FlowTyping on if expressions: + * we type the condition + * we give the typed condition to the FlowTyper and obtain a pair of sets of paths `(ifTrue, ifFalse)`. + We type the `then` branch with the `ifTrue` facts, and the else branch with the `ifFalse` facts. + * profit + +Flow typing also introduces two new abstractions: `NonNullTermRef` and `ValDefInBlockCompleter`. + +#### NonNullTermRef +This is a new type of `TermRef` (path-dependent type) that, whenever its denotation is updated, makes sure +that the underlying widened type is non-null. It's defined in `Types.scala`. A `NonNullTermRef` is identified by `computeDenot` whenever the denotation is updated, and then we call `stripNull` on the widened type. + +To use the flow-typing information, whenever we see a path that we know must be non-null (in `typedIdent` or +`typedSelect`), we replace its `TermRef` by a `NonNullTermRef`. + +#### ValDefInBlockCompleter +This a new type of completer defined in `Namer.scala` that completes itself using the completion context, asopposed to the creation context. + +The problem we're trying to solve here is the following: +```scala +val x: String|Null = ??? +if (x == null) return +val y = x.length +``` +The block is usually typed as follows: + 1. first, we scan the block to create symbols for the new definitions (`val x`, `val y`) + 2. then, we type statement by statement + 3. the completers for the symbols created in 1. are _all_ invoked in step 2. However, + regular completers use the _creation_ context, so that means that `val y` is completed + with a context that doesn't contain the new flow fact "x != null". + +To fix this, whenever we're inside a block and we create completers for `val`s, we use a +`ValDefInBlockCompleter` instead of a regular completer. This new completer uses the completion context, +which is aware of the new flow fact "x != null". 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..f8ef015abf85 --- /dev/null +++ b/docs/docs/reference/other-new-features/explicit-nulls.md @@ -0,0 +1,382 @@ +--- +layout: doc-page +title: "Explicit Nulls" +--- + +This proposal describes a modification to the Scala type system that 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 + +Because of the unsoundness, we need to allow comparisons of the form `x == null` or `x != null` +even when `x` has a non-nullable reference type (but not a value type). This is so we have an +"escape hatch" for when we know `x` is nullable even when the type says it shouldn't be. +```scala +val x: String|Null = null +x == null // ok: x is a nullable string +"hello" == null // ok: String is a reference type +1 == null // error: Int is a value type +``` + +### Reference Equality + +Recall that `Null` is now a direct subtype of `Any`, as opposed to `AnyRef`. +However, we also need to allow reference equality comparisons: +```scala +val x: String = null +x eq null // ok: could return `true` because of unsoundness +``` + +We support this case by making the `eq` and `ne` methods in `AnyRef` take a `AnyRef|Null` +as argument. + +We also need to support +```scala +null.eq("hello") +val x: String|Null = null +x.eq(null) +``` + +We support this case via extension methods defined in the Predef: +```scala +def (x: AnyRef|Null) eq(y: AnyRef|Null): Boolean = + (x == null && y == null) || (x != null && x.eq(y)) +``` + +## 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 + implicit class NonNull[T](x: T|Null) extends AnyVal { + def nn: T = if (x == null) { + throw new NullPointerException("tried to cast away nullability, but value is null") + } else { + x.asInstanceOf[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`. + +## 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 + +### Nullification Function + +We do the patching with a "nullification" function `n` on types: +```scala +1. n(T) = T|JavaNull if T is a reference type +2. n(T) = T if T is a value type +3. n(T) = T|JavaNull if T is a type parameter +4. n(C[T]) = C[T]|JavaNull if C is Java-defined +5. n(C[T]) = C[n(T)]|JavaNull if C is Scala-defined +6. n(A|B) = n(A)|n(B)|JavaNull +7. n(A&B) = (n(A)&n(B))|JavaNull +8. n((A1, ..., Am)R) = (n(A1), ..., n(Am))n(R) for a method with arguments (A1, ..., Am) and return type R +9. n(T) = T otherwise +``` + +`JavaNull` is an alias for `Null` with magic properties (see below). We illustrate the rules for `nf` below with examples. + + * The first two rules are easy: we nullify reference types but not value types. + ```scala + class C { + String s; + int x; + } + ==> + class C { + val s: String|Null + val x: Int + } + ``` + + * In rule 3 we nullify type parameters because in Java a type parameter is always nullable, so the following code compiles. + ```scala + class C { T foo() { return null; } } + ==> + class C[T] { def foo(): T|Null } + ``` + + 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 + } + ``` + + * Rule 4 reduces the number of redundant nullable types we need to add. Consider + ```scala + class Box { T get(); } + class BoxFactory { Box makeBox(); } + ==> + 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`, because of rule 4. 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`, as per + rule 3. + + Notice that for rule 4 to be correct 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. + + * Rule 5 is needed because Java code might use a generic that's defined in Scala as opposed to Java. + ```scala + class BoxFactory { Box makeBox(); } // Box is Scala defined + ==> + class BoxFactory[T] { def makeBox(): Box[T|JavaNull]|JavaNull } + ``` + + In this case, since `Box` is Scala-defined, `nf` is applied to the type argument `T`, so rule 3 applies and we 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. + + * Rules 6, 7, and 8 just recurse structurally on the components of the type. + The handling of unions and intersections in the compiler is a bit more involved than the presentation above. + Specifically, the implementation makes sure to add `| Null` only at the top level of a type: + e.g. `nf(A & B) = (A & B) | JavaNull`, as opposed to `(A | JavaNull) & (B | JavaNull)`. + +### JavaNull +To enable method chaining on Java-returned values, we have a special `JavaNull` alias +```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. +``` + +## Binary Compatibility + +Our strategy for binary compatibility with Scala binaries that predate explicit nulls +is to leave the types unchanged and be compatible but unsound. + +Concretely, the problem is how to interpret the return type of `foo` below +```scala +// As compiled by e.g. Scala 2.12 +class Old { + def foo(): String = ??? +} +``` +There are two options: + - `def foo(): String` + - `def foo(): String|Null` + +The first option is unsound. The second option matches how we handle Java methods. + +However, this approach is too-conservative in the presence of generics +```scala +class Old[T] { + def id(x: T): T = x +} +==> +class Old[T] { + def id(x: T|Null): T|Null = x +} +``` + +If we instantiate `Old[T]` with a value type, then `id` now returns a nullable value, +even though it shouldn't: +```scala +val o: Old[Boolean] = ??? +val b = o.id(true) // b: Boolean|Null +``` + +So really the options are between being unsound and being too conservative. +The unsoundness only kicks in if the Scala code being used returns a `null` value. +We hypothesize that `null` is used infrequently in Scala libraries, so we go with +the first option. + +If a using an unported Scala library that _produces_ `null`, the user can wrap the +(hopefully rare) API in a type-safe wrapper: +```scala +// Unported library +class Old { + def foo(): String = null +} + +// User code in explicit-null world +def fooWrapper(o: Old): String|Null = o.foo() // ok: String <: String|Null + +val o: Old = ??? +val s = fooWrapper(o) +``` + +If the offending API _consumes_ `null`, then the user can cast the null literal to +the right type (the cast will succeed, since at runtime `Null` _is_ a subtype of +any reference type). +```scala +// Unported library +class Old() { + /** Pass a String, or null to signal a special case */ + def foo(s: String): Unit = ??? +} + +// User code in explicit-null world +val o: Old = ??? +o.foo(null.asInstanceOf[String]) // ok: cast will succeed at runtime +``` + +## Flow Typing + +We added a simple form of flow-sensitive type inference. The idea is that if `p` is a +stable path, then we can know that `p` is non-null if it's compared with the `null` literal. +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 +``` +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 +} +``` + +What exactly is considered a comparison for the purposes of the flow inference? + - `==` and `!=` + - `eq` and `ne` + +### Non-Stable Paths +If `p` isn't stable, then inferring non-nullness is potentially unsound: +```scala +var s: String|Null = "hello" +if (s != null && {s = null; true}) { + // s == null +} +``` + +We _only_ infer non-nullness if `p` is stable (`val`s and not `var`s or `def`s). + +### 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 +} +``` + +### Unsupported Idioms +We don't support + - reasoning about non-stable paths + - 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 + } + ``` 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 529266cb3922..855b2dcae03b 100644 --- a/library/src/dotty/DottyPredef.scala +++ b/library/src/dotty/DottyPredef.scala @@ -38,4 +38,32 @@ 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: T = + if (x == null) throw new NullPointerException("tried to cast away nullability, but value is null") + else x.asInstanceOf[T] + + /** Reference equality where the receiver is a nullable union. + * Note that if the receiver `r` is a reference type (e.g. `String`), then `r.eq` will invoke the + * `eq` method in `AnyRef`. + */ + def (x: AnyRef|Null) eq(y: AnyRef|Null): Boolean = + (x == null && y == null) || (x != null && x.eq(y)) + + /** Reference disequality where the receiver is a nullable union. + * Note that if the receiver `r` is a reference type (e.g. `String`), then `r.ne` will invoke the + * `ne` method in `AnyRef`. + */ + def (x: AnyRef|Null) ne(y: AnyRef|Null): Boolean = + (x == null && y != null) || (x != null && x.ne(y)) + } 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/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..970f4ec811a8 --- /dev/null +++ b/tests/explicit-nulls/neg/eq.scala @@ -0,0 +1,26 @@ +// Test what can be compared for equality against null. +class Foo { + // Null itself + val x0: Null = null + x0 == null + null == x0 + null == null + + // Nullable types: OK + val x1: String|Null = null + x1 == null + null == x1 + + // Reference types, even non-nullable ones: OK. + // Allowed as an escape hatch. + val x2: String = "hello" + x2 != null + x2 == null + null == x2 + + // Value types: not allowed. + 1 == null // error + null == 1 // error + true == null // error + null == true // error +} diff --git a/tests/explicit-nulls/neg/eq2.scala b/tests/explicit-nulls/neg/eq2.scala new file mode 100644 index 000000000000..13f3e4e7f46a --- /dev/null +++ b/tests/explicit-nulls/neg/eq2.scala @@ -0,0 +1,15 @@ + +// 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 + if (null == foo) {} // error + + // To test against null, make the type nullable. + val foo2: Foo|Null = foo + 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..013b56d3f1c2 --- /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 = b.s // error: type of `b2` isn't refined because `b2` is not stable + } + } + + 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/flow3.scala b/tests/explicit-nulls/neg/flow3.scala new file mode 100644 index 000000000000..0c3b8b47e18b --- /dev/null +++ b/tests/explicit-nulls/neg/flow3.scala @@ -0,0 +1,23 @@ + +// Test flow typing in the presence of `eq` and `ne`. + +class Test { + val x: String|Null = ??? + + if (x eq null) { + val y = x.length // error + } else { + val y = x.length // ok + } + + if (x ne null) { + val y = x.length // ok + } else { + val y = x.length // error + } + + if ((x ne null) && x.length > 10) { // ok + } else { + val y = x.length // error + } +} diff --git a/tests/explicit-nulls/neg/flow4.scala b/tests/explicit-nulls/neg/flow4.scala new file mode 100644 index 000000000000..079f8c97dd1b --- /dev/null +++ b/tests/explicit-nulls/neg/flow4.scala @@ -0,0 +1,18 @@ + +// Test that flow inference handles `eq/ne` checks. +class Foo { + val x: String|Null = "" + + if (x eq null) { + val y = x.length // error + } else { + val y = x.length + } + + + if (x ne null) { + val y = x.length + } else { + val y = x.length // error + } +} 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..d4252e5a0412 --- /dev/null +++ b/tests/explicit-nulls/neg/flow6.scala @@ -0,0 +1,50 @@ +// Test forward references handled with flow typing +// Currently, the flow typing will not be applied to definitions forwardly referred. +class Foo { + + 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 = y + if (x == null) return () + def y = 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/strip.scala b/tests/explicit-nulls/neg/strip.scala new file mode 100644 index 000000000000..1dbe1646005d --- /dev/null +++ b/tests/explicit-nulls/neg/strip.scala @@ -0,0 +1,16 @@ + +class Foo { + + class B1 + class B2 + + val x: (Null | String) | Null | (B1 | (Null | B2)) = ??? + if (x != null) { + val x2: String | B1 | B2 = x // ok: can remove all nullable unions + } + + val x2: (Null | String) & (Null | B1) = ??? + if (x2 != null) { + val x3: String & B1 = x2 // error: can't remove null from embedded intersection + } +} 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/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-singleton.scala b/tests/explicit-nulls/pos/dont-widen-singleton.scala new file mode 100644 index 000000000000..bcd3df969e23 --- /dev/null +++ b/tests/explicit-nulls/pos/dont-widen-singleton.scala @@ -0,0 +1,9 @@ + +// Test that we correctly handle nullable unions when widening +// (we don't widen them). +class Test { + def foo(): Unit = { + val x: String|Null = ??? + val y = x // this used to crash the compiler + } +} 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/flow3.scala b/tests/explicit-nulls/pos/flow3.scala new file mode 100644 index 000000000000..21198e09a748 --- /dev/null +++ b/tests/explicit-nulls/pos/flow3.scala @@ -0,0 +1,9 @@ + +// Test that flow inference can look inside type ascriptions. +// This is useful in combination with inlining (because inlined methods have an ascribed return type). +class Foo { + val x: String|Null = "hello" + if ((x != null): Boolean) { + val y = x.length + } +} diff --git a/tests/explicit-nulls/pos/flow4.scala b/tests/explicit-nulls/pos/flow4.scala new file mode 100644 index 000000000000..ef1c8b0c7ab9 --- /dev/null +++ b/tests/explicit-nulls/pos/flow4.scala @@ -0,0 +1,24 @@ + +// 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[_, _]) = (tree ne null) && tree.isInstanceOf[RedTree[_, _]] + + def foo[A, B](tree: Tree[A, B]): 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/flow5.scala b/tests/explicit-nulls/pos/flow5.scala new file mode 100644 index 000000000000..7fe74fee7a76 --- /dev/null +++ b/tests/explicit-nulls/pos/flow5.scala @@ -0,0 +1,19 @@ +// Test that flow inference handles `eq/ne`. + +class Test { + val x: String|Null = ??? + + if (!(x eq null)) { + val y = x.length + } + + if (x eq null) { + } else { + val y = x.length + } + + if (x ne null) { + val y = x.length + } +} + 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/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/ref-eq.scala b/tests/explicit-nulls/pos/ref-eq.scala new file mode 100644 index 000000000000..340c687c7d70 --- /dev/null +++ b/tests/explicit-nulls/pos/ref-eq.scala @@ -0,0 +1,31 @@ +// Test reference equality. + +class Test { + val x1: String = "hello" + val x2: String|Null = null + + // Methods in AnyRef + x1.eq(x1) + x1.ne(1) + x1.eq(null) + x1.ne(null) + x1.eq(1) // ok: implicit conversion from int to Integer + x1.ne(1) // ok: ditto + x1.eq(x2) + x1.ne(x2) + + // Extension methods + null.eq("hello") + null.ne("hello") + null.eq(null) + null.ne(null) + null.eq(x2) + null.ne(x2) + + x2.eq(null) + x2.ne(null) + x2.eq(x1) + x2.ne(x1) + x2.eq(x2) + x2.ne(x2) +} 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/run/flow-extension-methods.scala b/tests/explicit-nulls/run/flow-extension-methods.scala new file mode 100644 index 000000000000..840402e78577 --- /dev/null +++ b/tests/explicit-nulls/run/flow-extension-methods.scala @@ -0,0 +1,44 @@ +// Test the `eq` and `ne` extension methods. +object Test { + def main(args: Array[String]): Unit = { + val x: String|Null = "hello" + if (x ne null) { + assert(x.length == 5) + } else { + assert(false) + } + + val y: String|Null = null + if (y eq null) { + assert(true) + } else { + assert(y.length == 5) + } + + class Foo { + val sz: Int = 42 + } + + // Now test the `AnyRef` methods. + val s: Foo = null.asInstanceOf[Foo] + if (s eq null) { + assert(true) + } else { + assert(false) + } + + val s2 = new Foo + if (s2 ne null) { + assert(true) + } else { + assert(false) + } + + // Now test the extension methods on null itself + assert(null eq null) + assert(!(null ne null)) + assert(null ne "hello") + assert(null eq y) + assert(null ne x) + } +} 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/subtype-any.scala b/tests/explicit-nulls/run/subtype-any.scala new file mode 100644 index 000000000000..31a5bb66f092 --- /dev/null +++ b/tests/explicit-nulls/run/subtype-any.scala @@ -0,0 +1,28 @@ + +object Test { + + def main(args: Array[String]): Unit = { + assert(null.eq(null)) + assert(!null.ne(null)) + + assert(!null.eq("hello")) + assert(null.ne("hello")) + + assert(!null.eq(4)) + assert(null.ne(4)) + + assert(!"hello".eq(null)) + assert("hello".ne(null)) + + assert(!4.eq(null)) + assert(4.ne(null)) + + val x: String|Null = null + assert(x.eq(null)) + assert(!x.ne(null)) + + val x2: AnyRef|Null = "world" + assert(!x2.eq(null)) + assert(x2.ne(null)) + } +} 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 +}