diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 60201ae532a0..2f7fd5aff33b 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -10,6 +10,7 @@ import core._ import util.Spans._, Types._, Contexts._, Constants._, Names._, Flags._, NameOps._ import Symbols._, StdNames._, Annotations._, Trees._, Symbols._ import Decorators._, DenotTransformers._ +import Phases._ import collection.{immutable, mutable} import util.{Property, SourceFile, NoSource} import NameKinds.{TempResultName, OuterSelectName} @@ -469,7 +470,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { /** The wrapped array method name for an array of type elemtp */ def wrapArrayMethodName(elemtp: Type)(using Context): TermName = { - val elemCls = elemtp.classSymbol + val elemCls = atPhase(erasurePhase.next) { elemtp.classSymbol } if (elemCls.isPrimitiveValueClass) nme.wrapXArray(elemCls.name) else if (elemCls.derivesFrom(defn.ObjectClass) && !elemCls.isNotRuntimeClass) nme.wrapRefArray else nme.genericWrapArray diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index ef4005f6c1f1..3a4eb8b52934 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -453,27 +453,11 @@ class Definitions { ScalaPackageClass, tpnme.Nothing, AbstractFinal, List(AnyType)) def NothingType: TypeRef = NothingClass.typeRef @tu lazy val NullClass: ClassSymbol = { - val parent = if (ctx.explicitNulls) AnyType else ObjectType + val parent = if ctx.explicitNulls then AnyType else ObjectType enterCompleteClassSymbol(ScalaPackageClass, tpnme.Null, AbstractFinal, parent :: Nil) } def NullType: TypeRef = NullClass.typeRef - /** An alias for null values that originate in Java code. - * This type gets special treatment in the Typer. Specifically, `UncheckedNull` can be selected through: - * e.g. - * ``` - * // x: String|Null - * x.length // error: `Null` has no `length` field - * // x2: String|UncheckedNull - * x2.length // allowed by the Typer, but unsound (might throw NPE) - * ``` - */ - lazy val UncheckedNullAlias: TypeSymbol = { - assert(ctx.explicitNulls) - enterAliasType(tpnme.UncheckedNull, NullType) - } - def UncheckedNullAliasType: TypeRef = UncheckedNullAlias.typeRef - @tu lazy val ImplicitScrutineeTypeSym = newPermanentSymbol(ScalaPackageClass, tpnme.IMPLICITkw, EmptyFlags, TypeBounds.empty).entered def ImplicitScrutineeTypeRef: TypeRef = ImplicitScrutineeTypeSym.typeRef @@ -634,7 +618,7 @@ class Definitions { @tu lazy val StringModule: Symbol = StringClass.linkedClass @tu lazy val String_+ : TermSymbol = enterMethod(StringClass, nme.raw.PLUS, methOfAny(StringType), Final) @tu lazy val String_valueOf_Object: Symbol = StringModule.info.member(nme.valueOf).suchThat(_.info.firstParamTypes match { - case List(pt) => pt.isAny || pt.isAnyRef + case List(pt) => pt.isAny || pt.stripNull.isAnyRef case _ => false }).symbol @@ -646,15 +630,13 @@ class Definitions { @tu lazy val ClassCastExceptionClass: ClassSymbol = requiredClass("java.lang.ClassCastException") @tu lazy val ClassCastExceptionClass_stringConstructor: TermSymbol = ClassCastExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match { case List(pt) => - val pt1 = if (ctx.explicitNulls) pt.stripNull() else pt - pt1.isRef(StringClass) + pt.stripNull.isRef(StringClass) case _ => false }).symbol.asTerm @tu lazy val ArithmeticExceptionClass: ClassSymbol = requiredClass("java.lang.ArithmeticException") @tu lazy val ArithmeticExceptionClass_stringConstructor: TermSymbol = ArithmeticExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match { case List(pt) => - val pt1 = if (ctx.explicitNulls) pt.stripNull() else pt - pt1.isRef(StringClass) + pt.stripNull.isRef(StringClass) case _ => false }).symbol.asTerm @@ -1236,7 +1218,7 @@ class Definitions { idx == name.length || name(idx).isDigit && digitsOnlyAfter(name, idx + 1) def isBottomClass(cls: Symbol): Boolean = - if (ctx.explicitNulls && !ctx.phase.erasedTypes) cls == NothingClass + if ctx.explicitNulls && !ctx.phase.erasedTypes then cls == NothingClass else isBottomClassAfterErasure(cls) def isBottomClassAfterErasure(cls: Symbol): Boolean = cls == NothingClass || cls == NullClass @@ -1700,8 +1682,8 @@ 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] = { - val synth = List( + @tu lazy val syntheticScalaClasses: List[TypeSymbol] = + List( AnyClass, MatchableClass, AnyRefAlias, @@ -1715,9 +1697,6 @@ class Definitions { NothingClass, SingletonClass) - if (ctx.explicitNulls) synth :+ UncheckedNullAlias else synth - } - @tu lazy val syntheticCoreClasses: List[Symbol] = syntheticScalaClasses ++ List( EmptyPackageVal, OpsPackageClass) diff --git a/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala b/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala index 8d6e2552bb32..acd0088f09aa 100644 --- a/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala @@ -1,32 +1,34 @@ -package dotty.tools.dotc.core +package dotty.tools.dotc +package core -import dotty.tools.dotc.core.Contexts._ -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 config.Feature._ +import Contexts._ +import Flags.JavaDefined import NullOpsDecorator._ +import StdNames.nme +import Symbols._ +import Types._ /** 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|UncheckedNull if T is a reference type + * (1) n(T) = T | Null if T is a reference type * (2) n(T) = T if T is a value type - * (3) n(C[T]) = C[T]|UncheckedNull if C is Java-defined - * (4) n(C[T]) = C[n(T)]|UncheckedNull if C is Scala-defined - * (5) n(A|B) = n(A)|n(B)|UncheckedNull + * (3) n(C[T]) = C[T] | Null if C is Java-defined + * (4) n(C[T]) = C[n(T)] | Null if C is Scala-defined + * (5) n(A|B) = n(A) | n(B) | Null * (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]|UncheckedNull`. That is, we don't recurse - * on the type argument, and only add UncheckedNull on the outside. This is because + * - if `C` is Java-defined, then `n(C[T]) = C[T] | Null`. That is, we don't recurse + * on the type argument, and only add Null 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)]|UncheckedNull`. This is because + * 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)] | Null`. 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 @@ -43,10 +45,9 @@ object JavaNullInterop { * * After calling `nullifyMember`, Scala will see the method as * - * def foo(arg: String|UncheckedNull): String|UncheckedNull + * def foo(arg: String | Null): String | Null * - * This nullability function uses `UncheckedNull` instead of vanilla `Null`, for usability. - * This means that we can select on the return of `foo`: + * If unsafeNulls is enabled, we can select on the return of `foo`: * * val len = foo("hello").length * @@ -57,10 +58,10 @@ object JavaNullInterop { assert(sym.is(JavaDefined), "can only nullify java-defined members") // Some special cases when nullifying the type - if (isEnumValueDef || sym.name == nme.TYPE_) + if isEnumValueDef || sym.name == nme.TYPE_ then // Don't nullify the `TYPE` field in every class and Java enum instances tp - else if (sym.name == nme.toString_ || sym.isConstructor || hasNotNullAnnot(sym)) + else if sym.name == nme.toString_ || sym.isConstructor || hasNotNullAnnot(sym) then // 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. @@ -81,20 +82,20 @@ object JavaNullInterop { private def nullifyExceptReturnType(tp: Type)(using Context): Type = new JavaNullMap(true)(tp) - /** Nullifies a Java type by adding `| UncheckedNull` in the relevant places. */ + /** Nullifies a Java type by adding `| Null` in the relevant places. */ private def nullifyType(tp: Type)(using Context): Type = new JavaNullMap(false)(tp) - /** A type map that implements the nullification function on types. Given a Java-sourced type, this adds `| UncheckedNull` + /** A type map that implements the nullification function on types. Given a Java-sourced type, this adds `| Null` * 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]|UncheckedNull` is already nullable at the - * outermost level, but `Array[String|UncheckedNull]` isn't. + * For example, `Array[String] | Null` is already nullable at the + * outermost level, but `Array[String | Null]` 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) | UncheckedNull`, instead of `(A|UncheckedNull & B|UncheckedNull) | UncheckedNull`. + * to `(A & B) | Null`, instead of `(A | Null & B | Null) | Null`. */ private class JavaNullMap(var outermostLevelAlreadyNullable: Boolean)(using Context) extends TypeMap { /** Should we nullify `tp` at the outermost level? */ @@ -107,7 +108,7 @@ object JavaNullInterop { !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|UncheckedNull)*): Unit`. + // then its Scala signature will be `def setNames(names: (String|Null)*): 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) @@ -115,7 +116,7 @@ object JavaNullInterop { }) override def apply(tp: Type): Type = tp match { - case tp: TypeRef if needsNull(tp) => OrUncheckedNull(tp) + case tp: TypeRef if needsNull(tp) => OrNull(tp) case appTp @ AppliedType(tycon, targs) => val oldOutermostNullable = outermostLevelAlreadyNullable // We don't make the outmost levels of type arguments nullable if tycon is Java-defined. @@ -125,7 +126,7 @@ object JavaNullInterop { val targs2 = targs map this outermostLevelAlreadyNullable = oldOutermostNullable val appTp2 = derivedAppliedType(appTp, tycon, targs2) - if (needsNull(tycon)) OrUncheckedNull(appTp2) else appTp2 + if needsNull(tycon) then OrNull(appTp2) else appTp2 case ptp: PolyType => derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType)) case mtp: MethodType => @@ -136,11 +137,11 @@ object JavaNullInterop { derivedLambdaType(mtp)(paramInfos2, this(mtp.resType)) case tp: TypeAlias => mapOver(tp) case tp: AndType => - // nullify(A & B) = (nullify(A) & nullify(B)) | UncheckedNull, but take care not to add - // duplicate `UncheckedNull`s at the outermost level inside `A` and `B`. + // nullify(A & B) = (nullify(A) & nullify(B)) | Null, but take care not to add + // duplicate `Null`s at the outermost level inside `A` and `B`. outermostLevelAlreadyNullable = true - OrUncheckedNull(derivedAndType(tp, this(tp.tp1), this(tp.tp2))) - case tp: TypeParamRef if needsNull(tp) => OrUncheckedNull(tp) + OrNull(derivedAndType(tp, this(tp.tp1), this(tp.tp2))) + case tp: TypeParamRef if needsNull(tp) => OrNull(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. diff --git a/compiler/src/dotty/tools/dotc/core/Mode.scala b/compiler/src/dotty/tools/dotc/core/Mode.scala index a42493fad2f1..05986682e924 100644 --- a/compiler/src/dotty/tools/dotc/core/Mode.scala +++ b/compiler/src/dotty/tools/dotc/core/Mode.scala @@ -120,7 +120,7 @@ object Mode { /** Are we resolving a TypeTest node? */ val InTypeTest: Mode = newMode(27, "InTypeTest") - /** Are we enforcing null safety */ + /** Are we enforcing null safety? */ val SafeNulls = newMode(28, "SafeNulls") /** We are typing the body of the condition of an `inline if` or the scrutinee of an `inline match` diff --git a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala index 878c52d5f32b..d0799ca89d24 100644 --- a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala +++ b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala @@ -1,86 +1,62 @@ -package dotty.tools.dotc.core +package dotty.tools.dotc +package core -import dotty.tools.dotc.core.Contexts._ -import dotty.tools.dotc.core.Symbols.defn -import dotty.tools.dotc.core.Types._ +import ast.Trees._ +import Contexts._ +import Symbols.defn +import Types._ -/** Defines operations on nullable types. */ -object NullOpsDecorator { - - extension (self: Type) { - /** Is this type exactly `UncheckedNull` (no vars, aliases, refinements etc allowed)? */ - def isUncheckedNullType(using Context): Boolean = { - assert(ctx.explicitNulls) - // We can't do `self == defn.UncheckedNull` because when trees are unpickled new references - // to `UncheckedNull` could be created that are different from `defn.UncheckedNull`. - // Instead, we compare the symbol. - self.isDirectRef(defn.UncheckedNullAlias) - } +/** Defines operations on nullable types and tree. */ +object NullOpsDecorator: + extension (self: Type) /** Syntactically strips the nullability from this type. - * If the type is `T1 | ... | Tn`, and `Ti` references to `Null` (or `UncheckedNull`), + * If the type is `T1 | ... | Tn`, and `Ti` references to `Null`, * then return `T1 | ... | Ti-1 | Ti+1 | ... | Tn`. * If this type isn't (syntactically) nullable, then returns the type unchanged. - * - * @param onlyUncheckedNull whether we only remove `UncheckedNull`, the default value is false + * The type will not be changed if explicit-nulls is not enabled. */ - def stripNull(onlyUncheckedNull: Boolean = false)(using Context): Type = { - assert(ctx.explicitNulls) - - def isNull(tp: Type) = - if (onlyUncheckedNull) tp.isUncheckedNullType - else tp.isNullType - - def strip(tp: Type): Type = tp match { - case tp @ OrType(lhs, rhs) => - val llhs = strip(lhs) - val rrhs = strip(rhs) - if (isNull(rrhs)) llhs - else if (isNull(llhs)) rrhs - else tp.derivedOrType(llhs, rrhs) - case tp @ AndType(tp1, tp2) => - // We cannot `tp.derivedAndType(strip(tp1), strip(tp2))` directly, - // since `stripNull((A | Null) & B)` would produce the wrong - // result `(A & B) | Null`. - val tp1s = strip(tp1) - val tp2s = strip(tp2) - if((tp1s ne tp1) && (tp2s ne tp2)) - tp.derivedAndType(tp1s, tp2s) - else tp - case _ => tp - } - - val self1 = self.widenDealias - val stripped = strip(self1) - if (stripped ne self1) stripped else self - } - - /** Like `stripNull`, but removes only the `UncheckedNull`s. */ - def stripUncheckedNull(using Context): Type = self.stripNull(true) - - /** Collapses all `UncheckedNull` unions within this type, and not just the outermost ones (as `stripUncheckedNull` does). - * e.g. (Array[String|UncheckedNull]|UncheckedNull).stripUncheckedNull => Array[String|UncheckedNull] - * (Array[String|UncheckedNull]|UncheckedNull).stripAllUncheckedNull => Array[String] - * If no `UncheckedNull` unions are found within the type, then returns the input type unchanged. - */ - def stripAllUncheckedNull(using Context): Type = { - object RemoveNulls extends TypeMap { - override def apply(tp: Type): Type = mapOver(tp.stripNull(true)) - } - val rem = RemoveNulls(self) - if (rem ne self) rem else self + def stripNull(using Context): Type = { + def strip(tp: Type): Type = + val tpWiden = tp.widenDealias + val tpStripped = tpWiden match { + case tp @ OrType(lhs, rhs) => + val llhs = strip(lhs) + val rrhs = strip(rhs) + if rrhs.isNullType then llhs + else if llhs.isNullType then rrhs + else tp.derivedOrType(llhs, rrhs) + case tp @ AndType(tp1, tp2) => + // We cannot `tp.derivedAndType(strip(tp1), strip(tp2))` directly, + // since `stripNull((A | Null) & B)` would produce the wrong + // result `(A & B) | Null`. + val tp1s = strip(tp1) + val tp2s = strip(tp2) + if (tp1s ne tp1) && (tp2s ne tp2) then + tp.derivedAndType(tp1s, tp2s) + else tp + case tp @ TypeBounds(lo, hi) => + tp.derivedTypeBounds(strip(lo), strip(hi)) + case tp => tp + } + if tpStripped ne tpWiden then tpStripped else tp + + if ctx.explicitNulls then strip(self) else self } /** Is self (after widening and dealiasing) a type of the form `T | Null`? */ def isNullableUnion(using Context): Boolean = { - val stripped = self.stripNull() + val stripped = self.stripNull stripped ne self } + end extension - /** Is self (after widening and dealiasing) a type of the form `T | UncheckedNull`? */ - def isUncheckedNullableUnion(using Context): Boolean = { - val stripped = self.stripNull(true) - stripped ne self + import ast.tpd._ + + extension (self: Tree) + // cast the type of the tree to a non-nullable type + def castToNonNullable(using Context): Tree = self.typeOpt match { + case OrNull(tp) => self.cast(tp) + case _ => self } - } -} +end NullOpsDecorator \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 3570be66e8e3..8ab199fdc76a 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -199,7 +199,6 @@ object StdNames { final val Nothing: N = "Nothing" final val NotNull: N = "NotNull" final val Null: N = "Null" - final val UncheckedNull: N = "UncheckedNull" final val Object: N = "Object" final val FromJavaObject: N = "" final val Product: N = "Product" diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 4b04d8b8c108..d9fc96ff7ef3 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -797,7 +797,8 @@ object SymDenotations { /** Is this symbol a class of which `null` is a value? */ final def isNullableClass(using Context): Boolean = - if (ctx.explicitNulls && !ctx.phase.erasedTypes) symbol == defn.NullClass || symbol == defn.AnyClass + if ctx.mode.is(Mode.SafeNulls) && !ctx.phase.erasedTypes + then symbol == defn.NullClass || symbol == defn.AnyClass else isNullableClassAfterErasure /** Is this symbol a class of which `null` is a value after erasure? @@ -1823,10 +1824,14 @@ object SymDenotations { ) final override def isSubClass(base: Symbol)(using Context): Boolean = - derivesFrom(base) || - base.isClass && ( - (symbol eq defn.NothingClass) || - (symbol eq defn.NullClass) && (base ne defn.NothingClass)) + derivesFrom(base) + || base.isClass + && ( + (symbol eq defn.NothingClass) + || (symbol eq defn.NullClass) + && (!ctx.mode.is(Mode.SafeNulls) || ctx.phase.erasedTypes) + && (base ne defn.NothingClass) + ) /** Is it possible that a class inherits both `this` and `that`? * diff --git a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala index d3dc0713a1ed..96f3b34bf967 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala @@ -400,15 +400,16 @@ class TypeApplications(val self: Type) extends AnyVal { case _ => if (self.isMatch) MatchAlias(self) else TypeAlias(self) } - /** Translate a type of the form From[T] to either To[T] or To[? <: T] (if `wildcardArg` is set). Keep other types as they are. + /** Translate a type of the form From[T] to either To[T] or To[? <: T] (if `wildcardArg` is set). + * Keep other types as they are. * `from` and `to` must be static classes, both with one type parameter, and the same variance. * Do the same for by name types => From[T] and => To[T] */ - def translateParameterized(from: ClassSymbol, to: ClassSymbol, wildcardArg: Boolean = false)(using Context): Type = self match { + def translateParameterized(from: ClassSymbol, to: ClassSymbol, wildcardArg: Boolean = false)(using Context): Type = self match case self @ ExprType(tp) => self.derivedExprType(tp.translateParameterized(from, to)) case _ => - if (self.derivesFrom(from)) { + if self.derivesFrom(from) then def elemType(tp: Type): Type = tp.widenDealias match case tp: OrType => if tp.tp1.isBottomType then elemType(tp.tp2) @@ -419,15 +420,12 @@ class TypeApplications(val self: Type) extends AnyVal { val arg = elemType(self) val arg1 = if (wildcardArg) TypeBounds.upper(arg) else arg to.typeRef.appliedTo(arg1) - } else self - } /** If this is a repeated parameter `*T`, translate it to either `Seq[T]` or * `Array[? <: T]` depending on the value of `toArray`. * Additionally, if `translateWildcard` is true, a wildcard type - * will be translated to `*`. - * Other types are kept as-is. + * will be translated to `*`. Other types are kept as-is. */ def translateFromRepeated(toArray: Boolean, translateWildcard: Boolean = false)(using Context): Type = val seqClass = if (toArray) defn.ArrayClass else defn.SeqClass diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index b5bd1cfb92bc..e6f76b6f7f25 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -516,8 +516,8 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean private def eraseArray(tp: Type)(using Context) = { val defn.ArrayOf(elemtp) = tp - if (classify(elemtp).derivesFrom(defn.NullClass)) JavaArrayType(defn.ObjectType) - else if (isUnboundedGeneric(elemtp) && !isJava) defn.ObjectType + if classify(elemtp).derivesFrom(defn.NullClass) then JavaArrayType(defn.ObjectType) + else if isUnboundedGeneric(elemtp) && !isJava then defn.ObjectType else JavaArrayType(erasureFn(isJava, semiEraseVCs = false, isConstructor, wildcardOK)(elemtp)) } diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 971267112723..a0dbac9bef43 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -20,6 +20,7 @@ import Denotations._ import Periods._ import CheckRealizable._ import Variances.{Variance, varianceFromInt, varianceToInt, setStructuralVariances, Invariant} +import typer.Nullables import util.Stats._ import util.SimpleIdentitySet import ast.tpd._ @@ -30,6 +31,7 @@ import Hashable._ import Uniques._ import collection.mutable import config.Config +import config.Feature import annotation.{tailrec, constructorOnly} import language.implicitConversions import scala.util.hashing.{ MurmurHash3 => hashing } @@ -170,6 +172,8 @@ object Types { case tp: ExprType => tp.resultType.isStable case tp: AnnotatedType => tp.parent.isStable case tp: AndType => + // TODO: fix And type check when tp contains type parames for explicit-nulls flow-typing + // see: tests/explicit-nulls/pos/flow-stable.scala.disabled tp.tp1.isStable && (realizability(tp.tp2) eq Realizable) || tp.tp2.isStable && (realizability(tp.tp1) eq Realizable) case _ => false @@ -213,6 +217,13 @@ object Types { case tp: TypeRef => defn.topClasses.contains(tp.symbol) case _ => false + /** Is this type exactly Null (no vars, aliases, refinements etc allowed)? */ + def isExactlyNull(using Context): Boolean = this match { + case tp: TypeRef => + tp.name == tpnme.Null && (tp.symbol eq defn.NullClass) + case _ => false + } + /** Is this type exactly Nothing (no vars, aliases, refinements etc allowed)? */ def isExactlyNothing(using Context): Boolean = this match { case tp: TypeRef => @@ -474,7 +485,7 @@ object Types { * instance, or NoSymbol if none exists (either because this type is not a * value type, or because superclasses are ambiguous). */ - final def classSymbol(using Context): Symbol = this match { + final def classSymbol(using Context): Symbol = this match case tp: TypeRef => val sym = tp.symbol if (sym.isClass) sym else tp.superType.classSymbol @@ -489,12 +500,22 @@ object Types { else if (rsym isSubClass lsym) rsym else NoSymbol case tp: OrType => - tp.join.classSymbol + if tp.tp1.hasClassSymbol(defn.NothingClass) then + tp.tp2.classSymbol + else if tp.tp2.hasClassSymbol(defn.NothingClass) then + tp.tp1.classSymbol + else + def tp1Null = tp.tp1.hasClassSymbol(defn.NullClass) + def tp2Null = tp.tp2.hasClassSymbol(defn.NullClass) + if ctx.erasedTypes && (tp1Null || tp2Null) then + val otherSide = if tp1Null then tp.tp2.classSymbol else tp.tp1.classSymbol + if otherSide.isValueClass then defn.AnyClass else otherSide + else + tp.join.classSymbol case _: JavaArrayType => defn.ArrayClass case _ => NoSymbol - } /** The least (wrt <:<) set of symbols satisfying the `include` predicate of which this type is a subtype */ @@ -822,12 +843,10 @@ object Types { go(l).meet(go(r), pre, safeIntersection = ctx.base.pendingMemberSearches.contains(name)) def goOr(tp: OrType) = tp match { - case OrUncheckedNull(tp1) => - // Selecting `name` from a type `T|UncheckedNull` is like selecting `name` from `T`. - // This can throw at runtime, but we trade soundness for usability. - // We need to strip `UncheckedNull` from both the type and the prefix so that - // `pre <: tp` continues to hold. - tp1.findMember(name, pre.stripUncheckedNull, required, excluded) + case OrNull(tp1) if Nullables.unsafeNullsEnabled => + // Selecting `name` from a type `T | Null` is like selecting `name` from `T`, if + // unsafeNulls is enabled. This can throw at runtime, but we trade soundness for usability. + tp1.findMember(name, pre.stripNull, required, excluded) case _ => // we need to keep the invariant that `pre <: tp`. Branch `union-types-narrow-prefix` // achieved that by narrowing `pre` to each alternative, but it led to merge errors in @@ -1073,7 +1092,8 @@ object Types { */ def matches(that: Type)(using Context): Boolean = { record("matches") - TypeComparer.matchesType(this, that, relaxed = !ctx.phase.erasedTypes) + withoutMode(Mode.SafeNulls)( + TypeComparer.matchesType(this, that, relaxed = !ctx.phase.erasedTypes)) } /** This is the same as `matches` except that it also matches => T with T and @@ -1605,6 +1625,9 @@ object Types { /** Is this (an alias of) the `scala.Null` type? */ final def isNullType(using Context) = isRef(defn.NullClass) + /** Is this (an alias of) the `scala.Nothing` type? */ + final def isNothingType(using Context) = isRef(defn.NothingClass) + /** The resultType of a LambdaType, or ExprType, the type itself for others */ def resultType(using Context): Type = this @@ -3186,31 +3209,10 @@ object Types { */ object OrNull { def apply(tp: Type)(using Context) = - OrType(tp, defn.NullType, soft = false) - def unapply(tp: Type)(using Context): Option[Type] = - if (ctx.explicitNulls) { - val tp1 = tp.stripNull() - if tp1 ne tp then Some(tp1) else None - } - else None - } - - /** An extractor object to pattern match against a Java-nullable union. - * e.g. - * - * (tp: Type) match - * case OrUncheckedNull(tp1) => // tp had the form `tp1 | UncheckedNull` - * case _ => // tp was not a Java-nullable union - */ - object OrUncheckedNull { - def apply(tp: Type)(using Context) = - OrType(tp, defn.UncheckedNullAliasType, soft = false) + if tp.isNullType then tp else OrType(tp, defn.NullType, soft = false) def unapply(tp: Type)(using Context): Option[Type] = - if (ctx.explicitNulls) { - val tp1 = tp.stripUncheckedNull - if tp1 ne tp then Some(tp1) else None - } - else None + val tp1 = tp.stripNull + if tp1 ne tp then Some(tp1) else None } // ----- ExprType and LambdaTypes ----------------------------------- diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index d2b9b607a391..a6c272626fe7 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -24,6 +24,7 @@ import Variances.Invariant import TastyUnpickler.NameTable import typer.ConstFold import typer.Checking.checkNonCyclic +import typer.Nullables._ import util.Spans._ import util.SourceFile import ast.{TreeTypeMap, Trees, tpd, untpd} @@ -362,7 +363,9 @@ class TreeUnpickler(reader: TastyReader, if nothingButMods(end) then if lo.isMatch then MatchAlias(readVariances(lo)) else TypeAlias(readVariances(lo)) - else TypeBounds(lo, readVariances(readType())) + else + val hi = readVariances(readType()) + createNullableTypeBounds(lo, hi) case ANNOTATEDtype => AnnotatedType(readType(), Annotation(readTerm())) case ANDtype => @@ -1247,7 +1250,7 @@ class TreeUnpickler(reader: TastyReader, val lo = readTpt() val hi = if currentAddr == end then lo else readTpt() val alias = if currentAddr == end then EmptyTree else readTpt() - TypeBoundsTree(lo, hi, alias) + createNullableTypeBoundsTree(lo, hi, alias) case HOLE => val idx = readNat() val tpe = readType() @@ -1489,4 +1492,4 @@ object TreeUnpickler { final val AllDefs = 2 // add everything class TreeWithoutOwner extends Exception -} +} \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala index 8087396e4084..68c0a4bbfa2c 100644 --- a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala @@ -18,6 +18,7 @@ import printing.Printer import io.AbstractFile import util.common._ import typer.Checking.checkNonCyclic +import typer.Nullables._ import transform.SymUtils._ import PickleBuffer._ import PickleFormat._ @@ -778,7 +779,9 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas else if (sym.typeParams.nonEmpty) tycon.EtaExpand(sym.typeParams) else tycon case TYPEBOUNDStpe => - TypeBounds(readTypeRef(), readTypeRef()) + val lo = readTypeRef() + val hi = readTypeRef() + createNullableTypeBounds(lo, hi) case REFINEDtpe => val clazz = readSymbolRef().asClass val decls = symScope(clazz) @@ -1257,7 +1260,7 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas case TYPEBOUNDStree => val lo = readTreeRef() val hi = readTreeRef() - TypeBoundsTree(lo, hi) + createNullableTypeBoundsTree(lo, hi) case EXISTENTIALTYPEtree => val tpt = readTreeRef() @@ -1318,4 +1321,4 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas case other => errorBadSignature("expected an TypeDef (" + other + ")") } -} +} \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala index c0ea4adee353..a152ec3ed981 100644 --- a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala +++ b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala @@ -5,6 +5,7 @@ import core._ import Contexts._, Symbols._, Types._, Flags._, Decorators._, StdNames._, Constants._ import MegaPhase._ import SymUtils._ +import NullOpsDecorator._ import ast.Trees._ import reporting._ import dotty.tools.dotc.util.Spans.Span @@ -53,7 +54,7 @@ class ExpandSAMs extends MiniPhase: checkRefinements(tpe, fn) tree case tpe => - val tpe1 = checkRefinements(tpe, fn) + val tpe1 = checkRefinements(tpe.stripNull, fn) val Seq(samDenot) = tpe1.possibleSamMethods cpy.Block(tree)(stats, AnonClass(tpe1 :: Nil, fn.symbol.asTerm :: Nil, samDenot.symbol.asTerm.name :: Nil)) diff --git a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala index 8f43f63a0304..1ff9edda16d8 100644 --- a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala +++ b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala @@ -52,21 +52,7 @@ class FirstTransform extends MiniPhase with InfoTransformer { thisPhase => override def checkPostCondition(tree: Tree)(using Context): Unit = tree match { case Select(qual, name) if !name.is(OuterSelectName) && tree.symbol.exists => - val qualTpe = if (ctx.explicitNulls) { - // `UncheckedNull` is already special-cased in the Typer, but needs to be handled here as well. - // We need `stripAllUncheckedNull` and not `stripUncheckedNull` because of the following case: - // - // val s: (String|UncheckedNull)&(String|UncheckedNull) = "hello" - // val l = s.length - // - // The invariant below is that the type of `s`, which isn't a top-level UncheckedNull union, - // must derive from the type of the owner of `length`, which is `String`. Because we don't - // know which `UncheckedNull`s were used to find the `length` member, we conservatively remove - // all of them. - qual.tpe.stripAllUncheckedNull - } else { - qual.tpe - } + val qualTpe = qual.tpe assert( qualTpe.isErasedValueType || qualTpe.derivesFrom(tree.symbol.owner) || tree.symbol.is(JavaStatic) && qualTpe.derivesFrom(tree.symbol.enclosingClass), diff --git a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala index 70d41abe8d63..652b7ccc9d74 100644 --- a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala +++ b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala @@ -210,8 +210,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) { // 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.size == 1 => - val pinfo = if (ctx.explicitNulls) m.paramInfos.head.stripUncheckedNull else m.paramInfos.head - pinfo == defn.StringType + m.paramInfos.head.stripNull == defn.StringType case _ => false } val constructor = ioob.typeSymbol.info.decls.find(filterStringConstructor _).asTerm diff --git a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala index 4ebe2cf41fad..603dfee2aa3a 100644 --- a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala @@ -411,7 +411,7 @@ class TreeChecker extends Phase with SymTransformer { ex"""symbols differ for $tree |was : $sym |alternatives by type: $memberSyms%, % of types ${memberSyms.map(_.info)}%, % - |qualifier type : ${tree.qualifier.typeOpt} + |qualifier type : ${qualTpe} |tree type : ${tree.typeOpt} of class ${tree.typeOpt.getClass}""") } diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index bce4f6cbb242..a8339e72db74 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -15,6 +15,7 @@ import TreeInfo._ import ProtoTypes._ import Scopes._ import CheckRealizable._ +import NullOpsDecorator._ import ErrorReporting.errorTree import rewrites.Rewrites.patch import util.Spans.Span @@ -815,16 +816,21 @@ trait Checking { * enabled. */ def checkImplicitConversionUseOK(tree: Tree)(using Context): Unit = - val sym = tree.symbol + val tree1 = if Nullables.unsafeNullsEnabled then + // If unsafeNulls is enabled, a cast and a closure could be added to the original tree + // !!! TODO: We need a test for that. Right now everything passes even if this case is commented out. + stripCast(closureBody(tree)) + else tree + val sym = tree1.symbol if sym.name == nme.apply && sym.owner.derivesFrom(defn.ConversionClass) && !sym.info.isErroneous then - def conv = methPart(tree) match + def conv = methPart(tree1) match case Select(qual, _) => qual.symbol.orElse(sym.owner) case _ => sym.owner checkFeature(nme.implicitConversions, - i"Use of implicit conversion ${conv.showLocated}", NoSymbol, tree.srcPos) + i"Use of implicit conversion ${conv.showLocated}", NoSymbol, tree1.srcPos) private def infixOKSinceFollowedBy(tree: untpd.Tree): Boolean = tree match { case _: untpd.Block | _: untpd.Match => true diff --git a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala index be512af9a7c6..b3e35176f475 100644 --- a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala +++ b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala @@ -146,7 +146,7 @@ object ErrorReporting { |${fail.whyFailed.message.indented(8)}""" def selectErrorAddendum - (tree: untpd.RefTree, qual1: Tree, qualType: Type, suggestImports: Type => String) + (tree: untpd.RefTree, qual1: Tree, qualType: Type, suggestImports: Type => String, foundWithoutNull: Boolean = false) (using Context): String = val attempts = mutable.ListBuffer[(Tree, String)]() @@ -160,7 +160,14 @@ object ErrorReporting { case fail: FailedExtension => attempts += ((failure.tree, whyFailedStr(fail))) case fail: Implicits.NoMatchingImplicits => // do nothing case _ => attempts += ((failure.tree, "")) - if qualType.derivesFrom(defn.DynamicClass) then + if foundWithoutNull then + // !!! TODO: The need a test that tests this error message with a check file + i""". + |Since explicit-nulls is enabled, the selection is rejected because + |${qualType.widen} could be null at runtime. + |If you want to select ${tree.name} without checking for a null value, + |insert a .nn before .${tree.name} or import scala.language.unsafeNulls.""" + else if qualType.derivesFrom(defn.DynamicClass) then "\npossible cause: maybe a wrong Dynamic method signature?" else if attempts.nonEmpty then val attemptStrings = diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index a3dcd86759fd..a666198b3b5b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -250,7 +250,7 @@ object Implicits: val candidates = new mutable.ListBuffer[Candidate] def tryCandidate(extensionOnly: Boolean)(ref: ImplicitRef) = var ckind = exploreInFreshCtx { (ctx: FreshContext) ?=> - ctx.setMode(ctx.mode | Mode.TypevarsMissContext) + ctx.setMode(ctx.mode &~ Mode.SafeNulls | Mode.TypevarsMissContext) candidateKind(ref.underlyingRef) } if extensionOnly then ckind &= Candidate.Extension diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 782b156e32a4..0ea7207c7b2b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -20,6 +20,30 @@ import ast.Trees.mods object Nullables: import ast.tpd._ + inline def unsafeNullsEnabled(using Context): Boolean = + ctx.explicitNulls && !ctx.mode.is(Mode.SafeNulls) + + private def needNullifyHi(lo: Type, hi: Type)(using Context): Boolean = + ctx.explicitNulls + && lo.isExactlyNull // only nullify hi if lo is exactly Null type + && hi.isValueType + // We cannot check if hi is nullable, because it can cause cyclic reference. + + /** Create a nullable type bound + * If lo is `Null`, `| Null` is added to hi + */ + def createNullableTypeBounds(lo: Type, hi: Type)(using Context): TypeBounds = + val newHi = if needNullifyHi(lo, hi) then OrType(hi, defn.NullType, soft = false) else hi + TypeBounds(lo, newHi) + + /** Create a nullable type bound tree + * If lo is `Null`, `| Null` is added to hi + */ + def createNullableTypeBoundsTree(lo: Tree, hi: Tree, alias: Tree = EmptyTree)(using Context): TypeBoundsTree = + val hiTpe = hi.typeOpt + val newHi = if needNullifyHi(lo.typeOpt, hiTpe) then TypeTree(OrType(hiTpe, defn.NullType, soft = false)) else hi + TypeBoundsTree(lo, newHi, alias) + /** A set of val or var references that are known to be not null, plus a set of * variable references that are not known (anymore) to be not null */ @@ -240,7 +264,6 @@ object Nullables: && s != refOwner && (s.isOneOf(Lazy | Method) // not at the rhs of lazy ValDef or in a method (or lambda) || s.isClass // not in a class - // TODO: need to check by-name parameter || recur(s.owner)) refSym.is(Mutable) // if it is immutable, we don't need to check the rest conditions diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index fa00cc62be71..042e3f60b16e 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -22,6 +22,7 @@ import config.Printers.refcheck import reporting._ import scala.util.matching.Regex._ import Constants.Constant +import NullOpsDecorator._ object RefChecks { import tpd.{Tree, MemberDef, NamedArg, Literal, Template, DefDef} @@ -249,9 +250,14 @@ object RefChecks { jointBounds.lo frozen_<:< jointBounds.hi }) else - member.name.is(DefaultGetterName) || // default getters are not checked for compatibility - memberTp.overrides(otherTp, - member.matchNullaryLoosely || other.matchNullaryLoosely || fallBack) + // releaxed override check for explicit nulls if one of the symbols is Java defined, + // force `Null` being a subtype of reference types during override checking + val relaxedCtxForNulls = + if ctx.explicitNulls && (member.is(JavaDefined) || other.is(JavaDefined)) then + ctx.retractMode(Mode.SafeNulls) + else ctx + member.name.is(DefaultGetterName) // default getters are not checked for compatibility + || memberTp.overrides(otherTp, member.matchNullaryLoosely || other.matchNullaryLoosely || fallBack)(using relaxedCtxForNulls) catch case ex: MissingType => // can happen when called with upwardsSelf as qualifier of memberTp and otherTp, // because in that case we might access types that are not members of the qualifier. diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index db6f5f9f60c0..8735c4174901 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -32,7 +32,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): case defn.ArrayOf(elemTp) => val etag = typer.inferImplicitArg(defn.ClassTagClass.typeRef.appliedTo(elemTp), span) if etag.tpe.isError then EmptyTree else etag.select(nme.wrap) - case tp if hasStableErasure(tp) && !defn.isBottomClass(tp.typeSymbol) => + case tp if hasStableErasure(tp) && !defn.isBottomClassAfterErasure(tp.typeSymbol) => val sym = tp.typeSymbol val classTag = ref(defn.ClassTagModule) val tag = @@ -114,10 +114,11 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): cmpWithBoxed(cls1, cls2) else if cls2.isPrimitiveValueClass then cmpWithBoxed(cls2, cls1) - else if ctx.explicitNulls then - // If explicit nulls is enabled, we want to disallow comparison between Object and Null. - // If a nullable value has a non-nullable type, we can still cast it to nullable type - // then compare. + else if ctx.mode.is(Mode.SafeNulls) then + // If explicit nulls is enabled, and unsafeNulls is not enabled, + // we want to disallow comparison between Object and Null. + // If a nullable value has a non-nullable type, we can still cast it to + // nullable type then compare. // // Example: // val x: String = null.asInstanceOf[String] diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index 36adc62487c4..8295a99bd33e 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -154,7 +154,13 @@ trait TypeAssigner { def notAMemberErrorType(tree: untpd.Select, qual: Tree)(using Context): ErrorType = val qualType = qual.tpe.widenIfUnstable def kind = if tree.isType then "type" else "value" - def addendum = err.selectErrorAddendum(tree, qual, qualType, importSuggestionAddendum) + val foundWithoutNull = qualType match + case OrNull(qualType1) => + val name = tree.name + val pre = maybeSkolemizePrefix(qualType1, name) + reallyExists(qualType1.findMember(name, pre)) + case _ => false + def addendum = err.selectErrorAddendum(tree, qual, qualType, importSuggestionAddendum, foundWithoutNull) val msg: Message = if tree.name == nme.CONSTRUCTOR then ex"$qualType does not have a constructor" else NotAMember(qualType, tree.name, kind, addendum) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index cff9407e9328..f83be730946d 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -432,7 +432,7 @@ class Typer extends Namer // If a reference is in the context, it is already trackable at the point we add it. // Hence, we don't use isTracked in the next line, because checking use out of order is enough. !ref.usedOutOfOrder => - tree.select(defn.Any_typeCast).appliedToType(AndType(ref, tpnn)) + tree.cast(AndType(ref, tpnn)) case _ => tree @@ -588,9 +588,20 @@ class Typer extends Namer record("typedSelect") def typeSelectOnTerm(using Context): Tree = - typedSelect(tree, pt, typedExpr(tree.qualifier, shallowSelectionProto(tree.name, pt, this))) - .withSpan(tree.span) - .computeNullable() + val qual = typedExpr(tree.qualifier, shallowSelectionProto(tree.name, pt, this)) + val qual1 = if Nullables.unsafeNullsEnabled then + qual.tpe match { + case OrNull(tpe1) => + qual.cast(AndType(qual.tpe, tpe1)) + case tp => + if tp.isNullType + && (tree.name == nme.eq || tree.name == nme.ne) then + // Allow selecting `eq` and `ne` on `Null` specially + qual.cast(defn.ObjectType) + else qual + } + else qual + typedSelect(tree, pt, qual1).withSpan(tree.span).computeNullable() def typeSelectOnType(qual: untpd.Tree)(using Context) = typedSelect(untpd.cpy.Select(tree)(qual, tree.name.toTypeName), pt) @@ -786,13 +797,26 @@ class Typer extends Namer // A sequence argument `xs: _*` can be either a `Seq[T]` or an `Array[_ <: T]`, // irrespective of whether the method we're calling is a Java or Scala method, // so the expected type is the union `Seq[T] | Array[_ <: T]`. + // If unsafe nulls is enabled, the expected type is `Seq[T | Null] | Array[_ <: T | Null] | Null`. + val unsafeNulls = Nullables.unsafeNullsEnabled val ptArg = // FIXME(#8680): Quoted patterns do not support Array repeated arguments - if (ctx.mode.is(Mode.QuotedPattern)) pt.translateFromRepeated(toArray = false, translateWildcard = true) - else pt.translateFromRepeated(toArray = false, translateWildcard = true) | - pt.translateFromRepeated(toArray = true, translateWildcard = true) - val expr1 = typedExpr(tree.expr, ptArg) - val fromCls = if expr1.tpe.derivesFrom(defn.ArrayClass) then defn.ArrayClass else defn.SeqClass + if (ctx.mode.is(Mode.QuotedPattern)) + pt.translateFromRepeated(toArray = false, translateWildcard = true) + else + pt.translateFromRepeated(toArray = false, translateWildcard = true) + | pt.translateFromRepeated(toArray = true, translateWildcard = true) + val expr0 = typedExpr(tree.expr, ptArg) + val expr1 = if ctx.explicitNulls && (!ctx.mode.is(Mode.Pattern)) then + if expr0.tpe.isNullType then + // If the type of the argument is `Null`, we cast it to array directly. + expr0.cast(pt.translateParameterized(defn.RepeatedParamClass, defn.ArrayClass)) + else + // We need to make sure its type is no longer nullable + expr0.castToNonNullable + else expr0 + val fromCls = if expr1.tpe.stripNull.derivesFrom(defn.ArrayClass) + then defn.ArrayClass else defn.SeqClass val tpt1 = TypeTree(expr1.tpe.widen.translateToRepeated(fromCls)).withSpan(tree.tpt.span) assignType(cpy.Typed(tree)(expr1, tpt1), tpt1) } @@ -1321,8 +1345,8 @@ class Typer extends Namer if (tree.tpt.isEmpty) meth1.tpe.widen match { case mt: MethodType => - pt match { - case SAMType(sam) + pt.stripNull match { + case pt @ SAMType(sam) if !defn.isFunctionType(pt) && mt <:< sam => // SAMs of the form C[?] where C is a class cannot be conversion targets. // The resulting class `class $anon extends C[?] {...}` would be illegal, @@ -1680,7 +1704,7 @@ class Typer extends Namer } def typedSeqLiteral(tree: untpd.SeqLiteral, pt: Type)(using Context): SeqLiteral = { - val elemProto = pt.elemType match { + val elemProto = pt.stripNull.elemType match { case NoType => WildcardType case bounds: TypeBounds => WildcardType(bounds) case elemtp => elemtp diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index dc2890d42f3c..fec4454fe206 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -67,7 +67,7 @@ class CompilationTests { aggregateTests( compileFile("tests/rewrites/rewrites.scala", scala2CompatMode.and("-rewrite", "-indent")), - compileFile("tests/rewrites/rewrites3x.scala", defaultOptions.and("-rewrite", "-source", "3.1-migration")), + compileFile("tests/rewrites/rewrites3x.scala", defaultOptions.and("-rewrite", "-source", "future-migration")), compileFile("tests/rewrites/i8982.scala", defaultOptions.and("-indent", "-rewrite")), compileFile("tests/rewrites/i9632.scala", defaultOptions.and("-indent", "-rewrite")) ).checkRewrites() @@ -292,7 +292,8 @@ class CompilationTests { implicit val testGroup: TestGroup = TestGroup("explicitNullsNeg") aggregateTests( compileFilesInDir("tests/explicit-nulls/neg", explicitNullsOptions), - compileFilesInDir("tests/explicit-nulls/neg-patmat", explicitNullsOptions and "-Xfatal-warnings") + compileFilesInDir("tests/explicit-nulls/neg-patmat", explicitNullsOptions and "-Xfatal-warnings"), + compileFilesInDir("tests/explicit-nulls/unsafe-common", explicitNullsOptions), ) }.checkExpectedErrors() @@ -300,7 +301,8 @@ class CompilationTests { implicit val testGroup: TestGroup = TestGroup("explicitNullsPos") aggregateTests( compileFilesInDir("tests/explicit-nulls/pos", explicitNullsOptions), - compileFilesInDir("tests/explicit-nulls/pos-separate", explicitNullsOptions) + compileFilesInDir("tests/explicit-nulls/pos-separate", explicitNullsOptions), + compileFilesInDir("tests/explicit-nulls/unsafe-common", explicitNullsOptions and "-language:unsafeNulls"), ) }.checkCompile() diff --git a/docs/docs/internals/explicit-nulls.md b/docs/docs/internals/explicit-nulls.md index 87348b46098d..fe6b2923d9ad 100644 --- a/docs/docs/internals/explicit-nulls.md +++ b/docs/docs/internals/explicit-nulls.md @@ -5,35 +5,50 @@ 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`. +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" `UncheckedNull` type (an alias for `Null`) that is recognized by the compiler and - allows unsound member selections (trading soundness for usability) + 2. a "translation layer" for Java interoperability that exposes the nullability in Java APIs + 3. a `unsafeNulls` language feature which enables implicit unsafe conversion between `T` and `T | Null` -## Feature Flag +## Explicit-Nulls Flag -Explicit nulls are disabled by default. They can be enabled via `-Yexplicit-nulls` defined in +The explicit-nulls flag is currently disabled by default. It 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` + to include _only_ `Null` and `Any`, which is used by `TypeComparer` - changing the parent of `Null` in `Definitions` to point to `Any` and not `AnyRef` - changing `isBottomType` and `isBottomClass` in `Definitions` -## Java Interop +## Working with Nullable Unions + +There are some utility functions for nullable types in `NullOpsDecorator.scala`. +They are extension methods for `Type`; hence we can use them in this way: `tp.f(...)`. + +- `stripNull` syntactically strips all `Null` types in the union: + e.g. `T | Null => T`. This should only be used if we can guarantee `T` is a reference type. +- `isNullableUnion` determines whether `this` is a nullable union. +- `isNullableAfterErasure` determines whether `this` type can have `null` value after erasure. + +Within `Types.scala`, we also defined an extractor `OrNull` to extract the non-nullable part of a nullable unions . + +```scala +(tp: Type) match + case OrNull(tp1) => // if tp is a nullable union: tp1 | Null + case _ => // otherwise +``` + +## Java Interoperability 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|UncheckedNull` - - since Java methods might return `null`, the return type should be `String|UncheckedNull` - -`UncheckedNull` here is a type alias for `Null` with "magic" properties (see below). + - since we should be able to pass `null` into Java methods, the argument type should be `String | Null` + - since Java methods might return `null`, the return type should be `String | Null` At a high-level: - we track the loading of Java fields and methods as they're loaded by the compiler @@ -49,50 +64,46 @@ produces what the type of the symbol should be in the explicit nulls world. 1. If the symbol is a Enum value definition or a `TYPE_` field, we don't nullify the type 2. If it is `toString()` method or the constructor, or it has a `@NotNull` annotation, - we nullify the type, without a `UncheckedNull` at the outmost level. + we nullify the type, without a `Null` at the outmost level. 3. Otherwise, we nullify the type in regular way. +The `@NotNull` annotations are defined in `Definitions.scala`. + See `JavaNullMap` in `JavaNullInterop.scala` for more details about how we nullify different types. -## UncheckedNull +## Relaxed Overriding Check -`UncheckedNull` is just an alias for `Null`, but with magic power. `UncheckedNull`'s magic (anti-)power is that -it's unsound. +If the explicit nulls flag is enabled, the overriding check between Scala classes and Java classes is relaxed. -```scala -val s: String|UncheckedNull = "hello" -s.length // allowed, but might throw NPE -``` +The `matches` function in `Types.scala` is used to select condidated for overriding check. -`UncheckedNull` is defined as `UncheckedNullAlias` in `Definitions.scala`. -The logic to allow member selections is defined in `findMember` in `Types.scala`: - - if we're finding a member in a type union - - and the union contains `UncheckedNull` 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) +The `compatibleTypes` in `RefCheck.scala` determines whether the overriding types are compatible. -## Working with Nullable Unions +## Nullified Upper Bound -Within `Types.scala`, we defined some extractors to work with nullable unions: -`OrNull` and `OrUncheckedNull`. +Suppose we have a type bound `class C[T >: Null <: String]`, it becomes unapplicable in explicit nulls, since +we don't have a type that is a supertype of `Null` and a subtype of `String`. -```scala -(tp: Type) match - case OrNull(tp1) => // if tp is a nullable union: tp1 | Null - case _ => // otherwise -``` +Hence, when we read a type bound from Scala 2 Tasty or Scala 3 Tasty, the upper bound is nullified if the lower +bound is exactly `Null`. The example above would become `class C[T >: Null <: String | Null]`. -This extractor will call utility methods in `NullOpsDecorator.scala`. All of these -are methods of the `Type` class, so call them with `this` as a receiver: +## Unsafe Nulls -- `stripNull` syntactically strips all `Null` types in the union: - e.g. `String|Null => String`. -- `stripUncheckedNull` is like `stripNull` but only removes `UncheckedNull` from the union. - This is needed when we want to "revert" the Java nullification function. -- `stripAllUncheckedNull` collapses all `UncheckedNull` unions within this type, and not just the outermost - ones (as `stripUncheckedNull` does). -- `isNullableUnion` determines whether `this` is a nullable union. -- `isUncheckedNullableUnion` determines whether `this` is syntactically a union of the form - `T|UncheckedNull`. +The `unsafeNulls` language feature is currently disabled by default. It can be enabled by importing `scala.language.unsafeNulls` or using `-language:unsafeNulls`. The feature object is defined in `library/src/scalaShadowing/language.scala`. We can use `config.Feature.enabled(nme.unsafeNulls)` to check if this feature is enabled. + +The unsafe nulls conversion could happen if: +1. the explicit nulls flag is enabled, and +2. `unsafeNulls` language feature is enabled, or `UnsafeNullConversion` mode is in the context. + +The reason to use the `UnsafeNullConversion` mode is because the current context may not see the language feature. For example, implicit search could run in some different contexts. + +Since we want to allow selecting member on nullable values, when searching a member of a type, the `| Null` part should be ignored. See `goOr` in `Types.scala`. + +During adapting, if the type of the tree is not a subtype of the expected type, the `adaptToSubType` in `Typer.scala` will run. The implicit search is invoked to find conversions for the tree. Since implicit search (finding candidates and trying to type the new tree) could run in some different contexts, we have to pass the `UnsafeNullConversion` mode to the search context. + +The SAM type conversion also happens in `adaptToSubType`. We need to strip `Null` from `pt` in order to get class information. + +We need to modify the overloading resolution as well. The `isCompatible` and `necessarilyCompatible` functions in `ProtoTypes.scala` are used to compare types for overloading resolution. When `unsafeNulls` is enabled, we need to strip all nulls from the type before comparison. ## Flow Typing diff --git a/docs/docs/reference/other-new-features/explicit-nulls.md b/docs/docs/reference/other-new-features/explicit-nulls.md index b531774ac1d5..ae02b97a0543 100644 --- a/docs/docs/reference/other-new-features/explicit-nulls.md +++ b/docs/docs/reference/other-new-features/explicit-nulls.md @@ -8,7 +8,7 @@ Explicit nulls is an opt-in feature that modifies the Scala type system, which m This means the following code will no longer typecheck: ```scala -val x: String = null // error: found `Null`, but required `String` +val x: String = null // error: found `Null`, but required `String` ``` Instead, to mark a type as nullable we use a [union type](../new-types/union-types.md) @@ -17,6 +17,12 @@ Instead, to mark a type as nullable we use a [union type](../new-types/union-typ val x: String | Null = null // ok ``` +A nullable type could have null value during runtime; hence, it is not safe to select a member without checking its nullity. + +```scala +x.trim // error: trim is not member of String | Null +``` + Explicit nulls are enabled via a `-Yexplicit-nulls` flag. Read on for details. @@ -24,7 +30,7 @@ Read on for details. ## New Type Hierarchy When explicit nulls are enabled, the type hierarchy changes so that `Null` is only a subtype of -`Any`, as opposed to every reference type. +`Any`, as opposed to every reference type, which means `null` is no longer a value of `AnyRef` and its subtypes. This is the new type hierarchy: @@ -32,6 +38,31 @@ This is the new type hierarchy: After erasure, `Null` remains a subtype of all reference types (as forced by the JVM). +## 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 + extension [T](x: T | Null) + inline def nn: T = + assert(x != null) + 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`. + + Don't use `.nn` on mutable variables directly, because it may introduce an unknown type into the type of the variable. + + - An `unsafeNulls` language feature + + When imported, `T | Null` can be used as `T`, similar to regular Scala (without explicit nulls). + + See UnsafeNulls section for more details. + ## Unsoundness The new type system is unsound with respect to `null`. This means there are still instances where an expression has a non-nullable type like `String`, but its value is actually `null`. @@ -73,25 +104,6 @@ y == x // ok (x: Any) == null // ok ``` -## Working with `Null` - -To make working with nullable values easier, we propose adding a few utilities to the standard library. -So far, we have found the following useful: - - - An extension method `.nn` to "cast away" nullability - - ```scala - extension [T](x: T | Null) - inline def nn: T = - assert(x != null) - 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`. - - Don't use `.nn` on mutable variables directly, because it may introduce an unknown type into the type of the variable. - ## Java Interoperability The Scala compiler can load Java classes in two ways: from source or from bytecode. In either case, @@ -102,7 +114,7 @@ Specifically, we patch * the type of fields * the argument type and return type of methods -`UncheckedNull` is an alias for `Null` with magic properties (see [below](#uncheckednull)). We illustrate the rules with following examples: +We illustrate the rules with following examples: * The first two rules are easy: we nullify reference types but not value types. @@ -115,7 +127,7 @@ Specifically, we patch ==> ```scala class C: - val s: String|UncheckedNull + val s: String | Null val x: Int ``` @@ -126,7 +138,7 @@ Specifically, we patch ``` ==> ```scala - class C[T] { def foo(): T|UncheckedNull } + class C[T] { def foo(): T | Null } ``` Notice this is rule is sometimes too conservative, as witnessed by @@ -145,21 +157,21 @@ Specifically, we patch ``` ==> ```scala - class Box[T] { def get(): T|UncheckedNull } - class BoxFactory[T] { def makeBox(): Box[T]|UncheckedNull } + class Box[T] { def get(): T | Null } + class BoxFactory[T] { def makeBox(): Box[T] | Null } ``` Suppose we have a `BoxFactory[String]`. Notice that calling `makeBox()` on it returns a - `Box[String]|UncheckedNull`, not a `Box[String|UncheckedNull]|UncheckedNull`. This seems at first + `Box[String] | Null`, not a `Box[String | Null] | Null`. 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|UncheckedNull`. + `get()` on a `Box[String]` returns a `String | Null`. Notice that we need to patch _all_ Java-defined classes that transitively appear in the argument or return type of a field or method accessible from the Scala code being compiled. Absent crazy reflection magic, we think that all such Java classes _must_ be visible to the Typer in the first place, so they will be patched. - * We will append `UncheckedNull` to the type arguments if the generic class is defined in Scala. + * We will append `Null` to the type arguments if the generic class is defined in Scala. ```java class BoxFactory { @@ -170,16 +182,16 @@ Specifically, we patch ==> ```scala class BoxFactory[T]: - def makeBox(): Box[T | UncheckedNull] | UncheckedNull - def makeCrazyBoxes(): List[Box[List[T] | UncheckedNull]] | UncheckedNull + def makeBox(): Box[T | Null] | Null + def makeCrazyBoxes(): List[Box[List[T] | Null]] | Null ``` - In this case, since `Box` is Scala-defined, we will get `Box[T|UncheckedNull]|UncheckedNull`. + In this case, since `Box` is Scala-defined, we will get `Box[T | Null] | Null`. This is needed because our nullability function is only applied (modularly) to the Java classes, but not to the Scala ones, so we need a way to tell `Box` that it contains a nullable value. - The `List` is Java-defined, so we don't append `UncheckedNull` to its type argument. But we + The `List` is Java-defined, so we don't append `Null` to its type argument. But we still need to nullify its inside. * We don't nullify _simple_ literal constant (`final`) fields, since they are known to be non-null @@ -203,7 +215,7 @@ Specifically, we patch val NAME_GENERATED: String | Null = ??? ``` - * We don't append `UncheckedNull` to a field nor to a return type of a method which is annotated with a + * We don't append `Null` to a field nor to a return type of a method which is annotated with a `NotNull` annotation. ```java @@ -217,8 +229,8 @@ Specifically, we patch ```scala class C: val name: String - def getNames(prefix: String | UncheckedNull): List[String] // we still need to nullify the paramter types - def getBoxedName(): Box[String | UncheckedNull] // we don't append `UncheckedNull` to the outmost level, but we still need to nullify inside + def getNames(prefix: String | Null): List[String] // we still need to nullify the paramter types + def getBoxedName(): Box[String | Null] // we don't append `Null` to the outmost level, but we still need to nullify inside ``` The annotation must be from the list below to be recognized as `NotNull` by the compiler. @@ -247,41 +259,24 @@ Specifically, we patch "io.reactivex.annotations.NonNull" :: Nil map PreNamedString) ``` -### UncheckedNull +### Override check -To enable method chaining on Java-returned values, we have the special type alias for `Null`: - -```scala -type UncheckedNull = Null -``` +When we check overriding between Scala classes and Java classes, the rules are relaxed for `Null` type with this feature, in order to help users to working with Java libraries. -`UncheckedNull` behaves just like `Null`, except it allows (unsound) member selections: +Suppose we have Java method `String f(String x)`, we can override this method in Scala in any of the following forms: ```scala -// Assume someJavaMethod()'s original Java signature is -// String someJavaMethod() {} -val s2: String = someJavaMethod().trim().substring(2).toLowerCase() // unsound -``` +def f(x: String | Null): String | Null -Here, all of `trim`, `substring` and `toLowerCase` return a `String|UncheckedNull`. -The Typer notices the `UncheckedNull` and allows the member selection to go through. -However, if `someJavaMethod` were to return `null`, then the first member selection -would throw a `NPE`. +def f(x: String): String | Null -Without `UncheckedNull`, the chaining becomes too cumbersome +def f(x: String | Null): String -```scala -val ret = someJavaMethod() -val s2 = - if ret != null then - val tmp = ret.trim() - if tmp != null then - val tmp2 = tmp.substring(2) - if tmp2 != null then - tmp2.toLowerCase() -// Additionally, we need to handle the `else` branches. +def f(x: String): String ``` +Note that some of the definitions could cause unsoundness. For example, the return type is not nullable, but a `null` value is actually returned. + ## Flow Typing We added a simple form of flow-sensitive type inference. The idea is that if `p` is a @@ -434,6 +429,89 @@ We don't support: // s2: String not inferred ``` +### UnsafeNulls + +It is difficult to work with many nullable values, we introduce a language feature `unsafeNulls`. +Inside this "unsafe" scope, all `T | Null` values can be used as `T`. + +Users can import `scala.language.unsafeNulls` to create such scopes, or use `-language:unsafeNulls` to enable this feature globally (for migration purpose only). + +Assume `T` is a reference type (a subtype of `AnyRef`), the following unsafe operation rules are +applied in this unsafe-nulls scope: + +1. the members of `T` can be found on `T | Null` + +2. a value with type `T` can be compared with `T | Null` and `Null` + +3. suppose `T1` is not a subtype of `T2` using explicit-nulls subtyping (where `Null` is a direct +subtype of Any), extension methods and implicit conversions designed for `T2` can be used for +`T1` if `T1` is a subtype of `T2` using regular subtyping rules (where `Null` is a subtype of every +reference type) + +4. suppose `T1` is not a subtype of `T2` using explicit-nulls subtyping, a value with type `T1` +can be used as `T2` if `T1` is a subtype of `T2` using regular subtyping rules + +Addtionally, `null` can be used as `AnyRef` (`Object`), which means you can select `.eq` or `.toString` on it. + +The program in `unsafeNulls` will have a **similar** semantic as regular Scala, but not **equivalent**. + +For example, the following code cannot be compiled even using unsafe nulls. Because of the +Java interoperation, the type of the get method becomes `T | Null`. + +```Scala +def head[T](xs: java.util.List[T]): T = xs.get(0) // error +``` + +Since the compiler doesn’t know whether `T` is a reference type, it is unable to cast `T | Null` +to `T`. A `.nn` need to be inserted after `xs.get(0)` by user manually to fix the error, which +strips the `Nul`l from its type. + +The intention of this `unsafeNulls` is to give users a better migration path for explicit nulls. +Projects for Scala 2 or regular dotty can try this by adding `-Yexplicit-nulls -language:unsafeNulls` +to the compile options. A small number of manual modifications are expected. To migrate to the full +explicit nulls feature in the future, `-language:unsafeNulls` can be dropped and add +`import scala.language.unsafeNulls` only when needed. + +```scala +def f(x: String): String = ??? +def nullOf[T >: Null]: T = null + +import scala.language.unsafeNulls + +val s: String | Null = ??? +val a: String = s // unsafely convert String | Null to String + +val b1 = s.trim() // call .trim() on String | Null unsafely +val b2 = b1.length() + +f(s).trim() // pass String | Null as an argument of type String unsafely + +val c: String = null // Null to String + +val d1: Array[String] = ??? +val d2: Array[String | Null] = d1 // unsafely convert Array[String] to Array[String | Null] +val d3: Array[String] = Array(null) // unsafe + +class C[T >: Null <: String] // define a type bound with unsafe conflict bound + +val n = nullOf[String] // apply a type bound unsafely +``` + +Without the `unsafeNulls`, all these unsafe operations will not be type-checked. + +`unsafeNulls` also works for extension methods and implicit search. + +```scala +import scala.language.unsafeNulls + +val x = "hello, world!".split(" ").map(_.length) + +given Conversion[String, Array[String]] = _ => ??? + +val y: String | Null = ??? +val z: Array[String | Null] = y +``` + ## Binary Compatibility Our strategy for binary compatibility with Scala binaries that predate explicit nulls diff --git a/library/src/scala/runtime/stdLibPatches/Predef.scala b/library/src/scala/runtime/stdLibPatches/Predef.scala index e27e77e0d9f9..13dfc77ac60b 100644 --- a/library/src/scala/runtime/stdLibPatches/Predef.scala +++ b/library/src/scala/runtime/stdLibPatches/Predef.scala @@ -35,12 +35,15 @@ object Predef: // 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 + /** Strips away the nullability from a value. Note that `.nn` performs a checked cast, + * so if invoked on a `null` value it will throw an `NullPointerException`. + * @example {{{ + * val s1: String | Null = "hello" + * val s2: String = s1.nn * - * Note that `.nn` performs a checked cast, so if invoked on a null value it'll throw an NPE. + * val s3: String | Null = null + * val s4: String = s3.nn // throw NullPointerException + * }}} */ extension [T](x: T | Null) inline def nn: x.type & T = scala.runtime.Scala3RunTime.nn(x) diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index c63bebd96766..ee373c615f6c 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -77,8 +77,6 @@ object language: */ object adhocExtensions - object unsafeNulls - object future object `future-migration` @@ -108,4 +106,10 @@ object language: */ object `3.1` */ + /** Unsafe Nulls fot Explicit Nulls + * Inside the "unsafe" scope, `Null` is considered as a subtype of all reference types. + * + * @see [[http://dotty.epfl.ch/docs/reference/other-new-features/explicit-nulls.html]] + */ + object unsafeNulls end language diff --git a/tests/explicit-nulls/neg-patmat/patmat1.scala b/tests/explicit-nulls/neg-patmat/patmat1.scala index 6e9710a56dec..a73b1fca2e1e 100644 --- a/tests/explicit-nulls/neg-patmat/patmat1.scala +++ b/tests/explicit-nulls/neg-patmat/patmat1.scala @@ -1,4 +1,3 @@ - class Foo { val s: String = ??? s match { diff --git a/tests/explicit-nulls/neg/alias.scala b/tests/explicit-nulls/neg/alias.scala index f8dea4864027..c84fa594b842 100644 --- a/tests/explicit-nulls/neg/alias.scala +++ b/tests/explicit-nulls/neg/alias.scala @@ -1,6 +1,6 @@ - // Test that nullability is correctly detected // in the presence of a type alias. + class Base { type T >: Null <: AnyRef|Null } @@ -13,7 +13,7 @@ object foo { } class Derived extends Base { - type Nullable[X] = X|Null + type Nullable[X] = X | Null type Foo = Nullable[foo.Foo] def fun(foo: Foo): Unit = { @@ -21,4 +21,3 @@ class Derived extends Base { foo.doFoo() // error: foo is nullable } } - diff --git a/tests/explicit-nulls/neg/basic.scala b/tests/explicit-nulls/neg/basic.scala index 7c652887590b..4dac69d8e561 100644 --- a/tests/explicit-nulls/neg/basic.scala +++ b/tests/explicit-nulls/neg/basic.scala @@ -1,11 +1,18 @@ // 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 -} + val s: String = null // error + val s1: String | Null = null + val s2: String | Null = "" + + val b: Boolean = null // error + val c: Int | Null = null + val ar1: AnyRef = null // error + val ar2: AnyRef | Null = null + val ob: Object = null // error + + val av: AnyVal = null // error + val a: Any = null + val n: Null = null +} diff --git a/tests/explicit-nulls/neg/bounds.scala b/tests/explicit-nulls/neg/bounds.scala new file mode 100644 index 000000000000..180665eb4cb8 --- /dev/null +++ b/tests/explicit-nulls/neg/bounds.scala @@ -0,0 +1,22 @@ +val x1: String = ??? +val x2: String | Null = ??? + +// T has to be nullable type +def f1[T >: Null <: AnyRef | Null](x: T): T = x + +// Null is no longer a subtype of AnyRef, impossible to apply this method directly. +// We can bypass this restriction by importing unsafeNulls. +def f2[T >: Null <: AnyRef](x: T): T = x + +def nullOf[T >: Null <: AnyRef | Null]: T = null + +def g = { + f1(x1) + f1(x2) + + f2(x1) // error + f2(x2) // error + + val n1: String = nullOf // error + val n3: String | Null = nullOf +} diff --git a/tests/explicit-nulls/neg/default.scala b/tests/explicit-nulls/neg/default.scala index fe115861e926..fe3bd82e782b 100644 --- a/tests/explicit-nulls/neg/default.scala +++ b/tests/explicit-nulls/neg/default.scala @@ -1,4 +1,3 @@ - class Foo { val x: String = null // error: String is non-nullable @@ -10,4 +9,4 @@ class Foo { class Bar val b: Bar = null // error: user-created classes are also non-nullable -} +} \ No newline at end of file diff --git a/tests/explicit-nulls/neg/eq2.scala b/tests/explicit-nulls/neg/eq2.scala deleted file mode 100644 index 8c730407daa4..000000000000 --- a/tests/explicit-nulls/neg/eq2.scala +++ /dev/null @@ -1,18 +0,0 @@ -// Test that we can't compare for equality `null` and -// classes that derive from AnyVal. -class Foo(x: Int) extends AnyVal - -class Bar { - val foo: Foo = new Foo(15) - if (foo == null) {} // error: Values of types Null and Foo cannot be compared - if (null == foo) {} // error - if (foo != null) {} // error - if (null != foo) {} // error - - // To test against null, make the type nullable. - val foo2: Foo|Null = foo - if (foo2 == null) {} - if (null == foo2) {} - if (foo2 != null) {} - if (null != foo2) {} -} diff --git a/tests/explicit-nulls/neg/eq.scala b/tests/explicit-nulls/neg/equal1.scala similarity index 100% rename from tests/explicit-nulls/neg/eq.scala rename to tests/explicit-nulls/neg/equal1.scala diff --git a/tests/explicit-nulls/neg/equal2.scala b/tests/explicit-nulls/neg/equal2.scala new file mode 100644 index 000000000000..32cf21d918ac --- /dev/null +++ b/tests/explicit-nulls/neg/equal2.scala @@ -0,0 +1,39 @@ +// Test that we can't compare for equality `null` with classes. +// This rule is for both regular classes and value classes. + +class Foo(x: Int) +class Bar(x: Int) extends AnyVal + +class Test { + locally { + val foo: Foo = new Foo(15) + foo == null // error: Values of types Null and Foo cannot be compared + null == foo // error + foo != null // error + null != foo // error + + // To test against null, make the type nullable. + val foo2: Foo | Null = foo + // ok + foo2 == null + null == foo2 + foo2 != null + null != foo2 + } + + locally { + val bar: Bar = new Bar(15) + bar == null // error: Values of types Null and Foo cannot be compared + null == bar // error + bar != null // error + null != bar // error + + // To test against null, make the type nullable. + val bar2: Bar | Null = bar + // ok + bar2 == null + null == bar2 + bar2 != null + null != bar2 + } +} diff --git a/tests/explicit-nulls/neg/after-assign.scala b/tests/explicit-nulls/neg/flow-after-assign.scala similarity index 100% rename from tests/explicit-nulls/neg/after-assign.scala rename to tests/explicit-nulls/neg/flow-after-assign.scala diff --git a/tests/explicit-nulls/neg/flow.scala b/tests/explicit-nulls/neg/flow-basic.scala similarity index 100% rename from tests/explicit-nulls/neg/flow.scala rename to tests/explicit-nulls/neg/flow-basic.scala diff --git a/tests/explicit-nulls/neg/flow5.scala b/tests/explicit-nulls/neg/flow-early-exit.scala similarity index 94% rename from tests/explicit-nulls/neg/flow5.scala rename to tests/explicit-nulls/neg/flow-early-exit.scala index 0d11e45c6d54..9b7e5fe628dc 100644 --- a/tests/explicit-nulls/neg/flow5.scala +++ b/tests/explicit-nulls/neg/flow-early-exit.scala @@ -1,6 +1,5 @@ +// Test that flow-sensitive type inference handles early exits from blocks. -// Test that flow-sensitive type inference handles -// early exists from blocks. class Foo(x: String|Null) { // Test within constructor diff --git a/tests/explicit-nulls/neg/flow6.scala b/tests/explicit-nulls/neg/flow-forward-ref.scala similarity index 99% rename from tests/explicit-nulls/neg/flow6.scala rename to tests/explicit-nulls/neg/flow-forward-ref.scala index 6890a43018dd..f27d5dc92847 100644 --- a/tests/explicit-nulls/neg/flow6.scala +++ b/tests/explicit-nulls/neg/flow-forward-ref.scala @@ -1,5 +1,6 @@ // Test forward references handled with flow typing // Currently, the flow typing will not be applied to definitions forwardly referred. + class Foo { def test0(): Unit = { diff --git a/tests/explicit-nulls/neg/flow-implicitly.scala b/tests/explicit-nulls/neg/flow-implicitly.scala index 33934cf1f70f..fc4a5170210a 100644 --- a/tests/explicit-nulls/neg/flow-implicitly.scala +++ b/tests/explicit-nulls/neg/flow-implicitly.scala @@ -1,10 +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 + summon[x.type <:< String] // error: x.type is widened String|Null if (x != null) { - implicitly[x.type <:< String] // ok: x.type is widened to String + summon[x.type <:< String] // ok: x.type is widened to String } } diff --git a/tests/explicit-nulls/neg/flow2.scala b/tests/explicit-nulls/neg/flow-in-block.scala similarity index 100% rename from tests/explicit-nulls/neg/flow2.scala rename to tests/explicit-nulls/neg/flow-in-block.scala index 7ac243f7fd36..e337324fa0bb 100644 --- a/tests/explicit-nulls/neg/flow2.scala +++ b/tests/explicit-nulls/neg/flow-in-block.scala @@ -1,5 +1,5 @@ - // Test that flow inference can handle blocks. + class Foo { val x: String|Null = "hello" if ({val z = 10; {1 + 1 == 2; x != null}}) { diff --git a/tests/explicit-nulls/neg/flow7.scala b/tests/explicit-nulls/neg/flow-not-in-constructors.scala similarity index 100% rename from tests/explicit-nulls/neg/flow7.scala rename to tests/explicit-nulls/neg/flow-not-in-constructors.scala diff --git a/tests/explicit-nulls/neg/simple-var.scala b/tests/explicit-nulls/neg/flow-simple-var.scala similarity index 96% rename from tests/explicit-nulls/neg/simple-var.scala rename to tests/explicit-nulls/neg/flow-simple-var.scala index 66ac053a4fbb..5ef50f8e8c6a 100644 --- a/tests/explicit-nulls/neg/simple-var.scala +++ b/tests/explicit-nulls/neg/flow-simple-var.scala @@ -39,7 +39,7 @@ class SimpleVar { val a: String = x val b: String | String = a x = b - val _: String = x // ok + val c: String = x // ok } } } \ No newline at end of file diff --git a/tests/explicit-nulls/neg/strip.scala b/tests/explicit-nulls/neg/flow-strip-null.scala similarity index 100% rename from tests/explicit-nulls/neg/strip.scala rename to tests/explicit-nulls/neg/flow-strip-null.scala diff --git a/tests/explicit-nulls/neg/var-ref-in-closure.scala b/tests/explicit-nulls/neg/flow-varref-in-closure.scala similarity index 100% rename from tests/explicit-nulls/neg/var-ref-in-closure.scala rename to tests/explicit-nulls/neg/flow-varref-in-closure.scala diff --git a/tests/explicit-nulls/neg/interop-array-src/J.java b/tests/explicit-nulls/neg/interop-array-src/J.java index 80fda83e89d7..741c3739b296 100644 --- a/tests/explicit-nulls/neg/interop-array-src/J.java +++ b/tests/explicit-nulls/neg/interop-array-src/J.java @@ -1,3 +1,13 @@ class J { - void foo(String[] ss) {} + void foo1(String[] ss) {} + + String[] foo2() { + return new String[]{""}; + } + + void bar1(int[] is) {} + + int[] bar2() { + return new int[]{0}; + } } diff --git a/tests/explicit-nulls/neg/interop-array-src/S.scala b/tests/explicit-nulls/neg/interop-array-src/S.scala index 3796bab79970..585e299a832a 100644 --- a/tests/explicit-nulls/neg/interop-array-src/S.scala +++ b/tests/explicit-nulls/neg/interop-array-src/S.scala @@ -1,10 +1,25 @@ 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 + + def f = { + val x1: Array[String] = ??? + j.foo1(x1) // error: expected Array[String | Null] but got Array[String] + + val x2: Array[String | Null] = ??? + j.foo1(x2) // ok + j.foo1(null) // ok + + val y1: Array[String] = j.foo2() // error + val y2: Array[String | Null] = j.foo2() // error: expected Array[String | Null] but got Array[String] + val y3: Array[String | Null] | Null = j.foo2() + } + + def g = { + val x1: Array[Int] = ??? + j.bar1(x1) // ok + + val y1: Array[Int] = j.bar2() // error + val y2: Array[Int] | Null = j.bar2() + } } diff --git a/tests/explicit-nulls/neg/interop-java-enum-src/Planet.java b/tests/explicit-nulls/neg/interop-enum-src/Planet.java similarity index 100% rename from tests/explicit-nulls/neg/interop-java-enum-src/Planet.java rename to tests/explicit-nulls/neg/interop-enum-src/Planet.java diff --git a/tests/explicit-nulls/neg/interop-java-enum-src/S.scala b/tests/explicit-nulls/neg/interop-enum-src/S.scala similarity index 100% rename from tests/explicit-nulls/neg/interop-java-enum-src/S.scala rename to tests/explicit-nulls/neg/interop-enum-src/S.scala index 8e4e228a5e76..99e92cedc68d 100644 --- a/tests/explicit-nulls/neg/interop-java-enum-src/S.scala +++ b/tests/explicit-nulls/neg/interop-enum-src/S.scala @@ -1,5 +1,5 @@ - // 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-generics/J.java b/tests/explicit-nulls/neg/interop-generics/J.java new file mode 100644 index 000000000000..4bbdbd4cf319 --- /dev/null +++ b/tests/explicit-nulls/neg/interop-generics/J.java @@ -0,0 +1,13 @@ + +class I {} + +class J { + I foo(T x) { + return new I(); + } + + I[] bar(T x) { + Object[] r = new Object[]{new I()}; + return (I[]) r; + } +} diff --git a/tests/explicit-nulls/neg/interop-generics/S.scala b/tests/explicit-nulls/neg/interop-generics/S.scala new file mode 100644 index 000000000000..6222cde7d6d2 --- /dev/null +++ b/tests/explicit-nulls/neg/interop-generics/S.scala @@ -0,0 +1,12 @@ +class S { + val j = new J() + + // Check that the inside of a generic type is correctly nullified + val x1: I[String] | Null = j.foo("hello") //ok + val x2: I[String] = j.foo("hello") // error + val x3: I[String | Null] = j.foo("hello") // error + + val y1: Array[I[String] | Null] = j.bar[String](null) // error + val y2: Array[I[String]] | Null = j.bar[String](null) // error + val y3: Array[I[String] | Null] | Null = j.bar[String](null) +} diff --git a/tests/explicit-nulls/neg/interop-javanull.scala b/tests/explicit-nulls/neg/interop-javanull.scala deleted file mode 100644 index c9c6bf6f8a4c..000000000000 --- a/tests/explicit-nulls/neg/interop-javanull.scala +++ /dev/null @@ -1,8 +0,0 @@ - -// Test that UncheckedNull 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-propagate.scala b/tests/explicit-nulls/neg/interop-propagate.scala index 761acbe9769c..6af7ee182cac 100644 --- a/tests/explicit-nulls/neg/interop-propagate.scala +++ b/tests/explicit-nulls/neg/interop-propagate.scala @@ -1,8 +1,7 @@ class Foo { import java.util.ArrayList - // Test that as we extract return values, we're missing the |UncheckedNull in the return type. - // i.e. test that the nullability is propagated to nested containers. + // 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 diff --git a/tests/explicit-nulls/neg/interop-return.scala b/tests/explicit-nulls/neg/interop-return.scala index fb1f106f1d47..1d6df4da93bc 100644 --- a/tests/explicit-nulls/neg/interop-return.scala +++ b/tests/explicit-nulls/neg/interop-return.scala @@ -1,14 +1,15 @@ - // 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|UncheckedNull instead of String + val r: String = x.get(0) // error: got String | Null 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 |UncheckedNull + // (for purposes of generics) in Java (Integer) is. So we're missing `| Null` } } diff --git a/tests/explicit-nulls/neg/java-null.scala b/tests/explicit-nulls/neg/java-null.scala deleted file mode 100644 index bde68466c040..000000000000 --- a/tests/explicit-nulls/neg/java-null.scala +++ /dev/null @@ -1,10 +0,0 @@ -// Test that `UncheckedNull` 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|UncheckedNull = "world" - val l2 = s2.length // ok -} - diff --git a/tests/explicit-nulls/neg/nn.scala b/tests/explicit-nulls/neg/nn.scala new file mode 100644 index 000000000000..b765a0be8b62 --- /dev/null +++ b/tests/explicit-nulls/neg/nn.scala @@ -0,0 +1,10 @@ +// `.nn` extension method only strips away the outer Null. + +class Test { + val s1: String | Null = ??? + val s2: String = s1.nn + + val ss1: Array[String | Null] | Null = ??? + val ss2: Array[String | Null] = ss1.nn + val ss3: Array[String] = ss1.nn // error +} \ No newline at end of file diff --git a/tests/explicit-nulls/neg/null-classtag.scala b/tests/explicit-nulls/neg/null-classtag.scala new file mode 100644 index 000000000000..796a463d5148 --- /dev/null +++ b/tests/explicit-nulls/neg/null-classtag.scala @@ -0,0 +1,8 @@ +def f = { + val a: Array[Null] = Array(null) // error: No ClassTag available for Null +} + +def g = { + import scala.language.unsafeNulls + val a: Array[Null] = Array(null) // error: No ClassTag available for Null +} \ No newline at end of file diff --git a/tests/explicit-nulls/neg/nullable-types.scala b/tests/explicit-nulls/neg/nullable-types.scala new file mode 100644 index 000000000000..570d3cc53676 --- /dev/null +++ b/tests/explicit-nulls/neg/nullable-types.scala @@ -0,0 +1,19 @@ +// 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 + + def foo(x: String): String = "x" + + val y = foo(null) // error: String argument is non-nullable + + val z: String = foo("hello") + + class Bar + val bar: Bar = null // error: user-created classes are also non-nullable +} diff --git a/tests/explicit-nulls/neg/nullnull.scala b/tests/explicit-nulls/neg/nullnull.scala index c5710e17e78a..e1a1cf74914e 100644 --- a/tests/explicit-nulls/neg/nullnull.scala +++ b/tests/explicit-nulls/neg/nullnull.scala @@ -1,7 +1,5 @@ // 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 = { @@ -11,7 +9,7 @@ class Foo { } def foo2: Unit = { - val x: UncheckedNull | String | Null = ??? + val x: Null | 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 deleted file mode 100644 index ccce4af660c7..000000000000 --- a/tests/explicit-nulls/neg/override-java-object-arg.scala +++ /dev/null @@ -1,26 +0,0 @@ - -// 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() { // error: object creation impossible - 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() { // error: object creation impossible - 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 deleted file mode 100644 index 9dae93be404f..000000000000 --- a/tests/explicit-nulls/neg/override-java-object-arg2.scala +++ /dev/null @@ -1,13 +0,0 @@ - -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 = { // error - } - } - } - -} diff --git a/tests/explicit-nulls/neg/type-arg.scala b/tests/explicit-nulls/neg/type-arg.scala index c145ce562e6e..0bde5380bec9 100644 --- a/tests/explicit-nulls/neg/type-arg.scala +++ b/tests/explicit-nulls/neg/type-arg.scala @@ -1,12 +1,12 @@ - // Test that reference types being non-nullable // is checked when lower bound of a type argument // is Null. + object Test { type Untyped = Null class TreeInstances[T >: Untyped] class Type - + object untpd extends TreeInstances[Null] // There are two errors reported for the line below (don't know why). object tpd extends TreeInstances[Type] // error // error diff --git a/tests/explicit-nulls/neg/type-param.scala b/tests/explicit-nulls/neg/type-param.scala new file mode 100644 index 000000000000..ba7aa65c78bb --- /dev/null +++ b/tests/explicit-nulls/neg/type-param.scala @@ -0,0 +1,22 @@ +/** We used to use `T >: Null <: AnyRef` to represent a reference type, + * and `T` cannot be `Nothing`. However, with explicit nulls, this definition + * is no longer valid, because `Null` is not a subtype of `AnyRef`. + * + * For example: + * ```scala + * def nullOf[T >: Null <: AnyRef]: T = null + * ``` + * + * We can modify the definition as following to allow only nullable type paramters. + */ + +def nullOf[T >: Null <: AnyRef | Null]: T = null + +def f = { + val s1 = nullOf[String] // error: Type argument String does not conform to lower bound Null + val s2 = nullOf[String | Null] // ok + + val n = nullOf[Null] + val i = nullOf[Int] // error: Type argument Int does not conform to upper bound AnyRef | Null + val a = nullOf[Any] // error: Type argument Any does not conform to upper bound AnyRef | Null +} \ No newline at end of file diff --git a/tests/explicit-nulls/neg/unsafe-scope.scala b/tests/explicit-nulls/neg/unsafe-scope.scala new file mode 100644 index 000000000000..ab9a121f50a5 --- /dev/null +++ b/tests/explicit-nulls/neg/unsafe-scope.scala @@ -0,0 +1,31 @@ +class S { + given Conversion[String, Array[String]] = _ => ??? + + def f = { + val s: String | Null = ??? + + val x: String = s // error + val xl = s.length // error + val xs: Array[String | Null] | Null = s // error + + { + import scala.language.unsafeNulls + // ensure the previous search cache is not used here + val y: String = s + val yl = s.length + val ys: Array[String | Null] | Null = s + + { + // disable unsafeNulls here + import scala.language.{unsafeNulls => _} + val z: String = s // error + val zl = s.length // error + val zs: Array[String | Null] | Null = s // error + } + } + + val z: String = s // error + val zl = s.length // error + val zs: Array[String | Null] | Null = s // error + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos-separate/notnull/S_3.scala b/tests/explicit-nulls/pos-separate/notnull/S_3.scala index cc55ef54e7e2..49964b369d87 100644 --- a/tests/explicit-nulls/pos-separate/notnull/S_3.scala +++ b/tests/explicit-nulls/pos-separate/notnull/S_3.scala @@ -10,6 +10,6 @@ class S_3 { 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 | UncheckedNull] = (new J_2).genericf(a) + def genericff(a: String | Null): Array[String | Null] = (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/dont-widen-src/S.scala b/tests/explicit-nulls/pos/dont-widen-src/S.scala index 0fbca30fac0a..5414e418e79d 100644 --- a/tests/explicit-nulls/pos/dont-widen-src/S.scala +++ b/tests/explicit-nulls/pos/dont-widen-src/S.scala @@ -3,5 +3,5 @@ class S { 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 + val y: String | Null = x } diff --git a/tests/explicit-nulls/pos/flow.scala b/tests/explicit-nulls/pos/flow-basic.scala similarity index 100% rename from tests/explicit-nulls/pos/flow.scala rename to tests/explicit-nulls/pos/flow-basic.scala diff --git a/tests/explicit-nulls/pos/flow2.scala b/tests/explicit-nulls/pos/flow-condition.scala similarity index 100% rename from tests/explicit-nulls/pos/flow2.scala rename to tests/explicit-nulls/pos/flow-condition.scala diff --git a/tests/explicit-nulls/pos/flow4.scala b/tests/explicit-nulls/pos/flow-inline.scala similarity index 99% rename from tests/explicit-nulls/pos/flow4.scala rename to tests/explicit-nulls/pos/flow-inline.scala index 994b76065d8c..e64258ba988f 100644 --- a/tests/explicit-nulls/pos/flow4.scala +++ b/tests/explicit-nulls/pos/flow-inline.scala @@ -2,6 +2,7 @@ // 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) diff --git a/tests/explicit-nulls/pos/match.scala b/tests/explicit-nulls/pos/flow-match.scala similarity index 71% rename from tests/explicit-nulls/pos/match.scala rename to tests/explicit-nulls/pos/flow-match.scala index 0e65b9584328..9e3806c97363 100644 --- a/tests/explicit-nulls/pos/match.scala +++ b/tests/explicit-nulls/pos/flow-match.scala @@ -1,4 +1,4 @@ -// Test NotNullInfo from non-null cases +// Test flow-typing when NotNullInfos are from non-null cases object MatchTest { locally { diff --git a/tests/explicit-nulls/pos/stable-path.scala b/tests/explicit-nulls/pos/flow-stable-path.scala similarity index 100% rename from tests/explicit-nulls/pos/stable-path.scala rename to tests/explicit-nulls/pos/flow-stable-path.scala diff --git a/tests/explicit-nulls/pos/flow-stable.scala.disabled b/tests/explicit-nulls/pos/flow-stable.scala.disabled new file mode 100644 index 000000000000..155247e73a52 --- /dev/null +++ b/tests/explicit-nulls/pos/flow-stable.scala.disabled @@ -0,0 +1,15 @@ +// TODO: temporarily disable, +// in the if expression, `x.type` becomes `((x : T | Null) & T).type` due to `x != null` +// We need to make sure `(x : T | Null) & T` stable and concrete in order to use `.type` + +class S { + def i[T](x: T): x.type = x + + def f[T <: AnyRef](x: T | Null): x.type & T = { + if x != null then + // Any TermRef of x is rewriten to `x.asInstanceOf[(T | Null) & T] + i[x.type](x) + else + throw Exception() + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/tref-caching.scala b/tests/explicit-nulls/pos/flow-tref-caching.scala similarity index 88% rename from tests/explicit-nulls/pos/tref-caching.scala rename to tests/explicit-nulls/pos/flow-tref-caching.scala index 7a4c3bc412ea..f6c9773bd7fe 100644 --- a/tests/explicit-nulls/pos/tref-caching.scala +++ b/tests/explicit-nulls/pos/flow-tref-caching.scala @@ -1,9 +1,9 @@ - // 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` @@ -12,8 +12,8 @@ class Test { 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 y = x // regular tref `x` } - val x2 = x // regular tref `x` + val x2 = x // regular tref `x` } } diff --git a/tests/explicit-nulls/pos/flow6.scala b/tests/explicit-nulls/pos/flow-val-def.scala similarity index 100% rename from tests/explicit-nulls/pos/flow6.scala rename to tests/explicit-nulls/pos/flow-val-def.scala index 555e24335c26..d02942d5972a 100644 --- a/tests/explicit-nulls/pos/flow6.scala +++ b/tests/explicit-nulls/pos/flow-val-def.scala @@ -1,6 +1,6 @@ - // 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 = { diff --git a/tests/explicit-nulls/pos/while-loop.scala b/tests/explicit-nulls/pos/flow-while-loop.scala similarity index 100% rename from tests/explicit-nulls/pos/while-loop.scala rename to tests/explicit-nulls/pos/flow-while-loop.scala diff --git a/tests/explicit-nulls/pos/i10001.scala b/tests/explicit-nulls/pos/i10001.scala new file mode 100644 index 000000000000..32fc4a458631 --- /dev/null +++ b/tests/explicit-nulls/pos/i10001.scala @@ -0,0 +1,6 @@ +object Issue10001 { + val a: String = "Issue10001" + val b: String | Null = a + val c = s"$a" + val d = s"$b" +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/i8981.scala b/tests/explicit-nulls/pos/i8981.scala new file mode 100644 index 000000000000..f72b508cf64e --- /dev/null +++ b/tests/explicit-nulls/pos/i8981.scala @@ -0,0 +1 @@ +class Foo extends javax.swing.JPanel \ No newline at end of file diff --git a/tests/explicit-nulls/pos/interop-constructor-src/S.scala b/tests/explicit-nulls/pos/interop-constructor-src/S.scala index 6cbfea9b57b1..3defd73f3945 100644 --- a/tests/explicit-nulls/pos/interop-constructor-src/S.scala +++ b/tests/explicit-nulls/pos/interop-constructor-src/S.scala @@ -1,6 +1,6 @@ class S { - val x: J = new J("hello") + val x1: 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 index 1f631e6efff6..f222d24b0919 100644 --- a/tests/explicit-nulls/pos/interop-constructor.scala +++ b/tests/explicit-nulls/pos/interop-constructor.scala @@ -1,5 +1,5 @@ - // 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() diff --git a/tests/explicit-nulls/pos/interop-generics/J.java b/tests/explicit-nulls/pos/interop-generics/J.java index b8eab374844b..4bbdbd4cf319 100644 --- a/tests/explicit-nulls/pos/interop-generics/J.java +++ b/tests/explicit-nulls/pos/interop-generics/J.java @@ -5,5 +5,9 @@ class J { I foo(T x) { return new I(); } - // TODO(abeln): test returning a Scala generic from Java + + I[] bar(T x) { + Object[] r = new Object[]{new I()}; + return (I[]) r; + } } diff --git a/tests/explicit-nulls/pos/interop-generics/S.scala b/tests/explicit-nulls/pos/interop-generics/S.scala index 8c33ba3f0368..10a0572b0edf 100644 --- a/tests/explicit-nulls/pos/interop-generics/S.scala +++ b/tests/explicit-nulls/pos/interop-generics/S.scala @@ -1,7 +1,6 @@ -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") + val x: I[String] | Null = j.foo("hello") + val y: Array[I[String] | Null] | Null = j.bar[String](null) } diff --git a/tests/explicit-nulls/pos/java-varargs-src/Names.java b/tests/explicit-nulls/pos/interop-java-varargs-src/Names.java similarity index 100% rename from tests/explicit-nulls/pos/java-varargs-src/Names.java rename to tests/explicit-nulls/pos/interop-java-varargs-src/Names.java diff --git a/tests/explicit-nulls/pos/java-varargs-src/S.scala b/tests/explicit-nulls/pos/interop-java-varargs-src/S.scala similarity index 100% rename from tests/explicit-nulls/pos/java-varargs-src/S.scala rename to tests/explicit-nulls/pos/interop-java-varargs-src/S.scala index 5c180fcca400..e867202e506d 100644 --- a/tests/explicit-nulls/pos/java-varargs-src/S.scala +++ b/tests/explicit-nulls/pos/interop-java-varargs-src/S.scala @@ -1,6 +1,6 @@ - // 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() diff --git a/tests/explicit-nulls/pos/java-varargs.scala b/tests/explicit-nulls/pos/interop-java-varargs.scala similarity index 79% rename from tests/explicit-nulls/pos/java-varargs.scala rename to tests/explicit-nulls/pos/interop-java-varargs.scala index 1f7fd133fba7..46dc388d02af 100644 --- a/tests/explicit-nulls/pos/java-varargs.scala +++ b/tests/explicit-nulls/pos/interop-java-varargs.scala @@ -1,22 +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|UncheckedNull)*) + // def get(first: String | Null, more: (String | Null)*) // Test that we can avoid providing the varargs argument altogether. - Paths.get("out").toAbsolutePath + Paths.get("out") // Test with one argument in the varargs. Paths.get("home", "src") + Paths.get("home", null) // Test multiple arguments in the varargs. Paths.get("home", "src", "compiler", "src") + Paths.get("home", null, null, null) } diff --git a/tests/explicit-nulls/pos/interop-javanull-src/S.scala b/tests/explicit-nulls/pos/interop-javanull-src/S.scala deleted file mode 100644 index 42693066bf14..000000000000 --- a/tests/explicit-nulls/pos/interop-javanull-src/S.scala +++ /dev/null @@ -1,6 +0,0 @@ - -// Test that UncheckedNull 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 deleted file mode 100644 index d771b96e5507..000000000000 --- a/tests/explicit-nulls/pos/interop-javanull.scala +++ /dev/null @@ -1,10 +0,0 @@ - -// Tests that the "UncheckedNull" 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 "|UncheckedNull" (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/S.scala b/tests/explicit-nulls/pos/interop-nn-src/S.scala index 819f080eab0c..6250c4c3c961 100644 --- a/tests/explicit-nulls/pos/interop-nn-src/S.scala +++ b/tests/explicit-nulls/pos/interop-nn-src/S.scala @@ -3,13 +3,13 @@ class S { // 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 + 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 x2: String | Null = null val y2: String = x2.nn } diff --git a/tests/explicit-nulls/pos/interop-sam-src/J.java b/tests/explicit-nulls/pos/interop-sam-src/J.java new file mode 100644 index 000000000000..336e252aa861 --- /dev/null +++ b/tests/explicit-nulls/pos/interop-sam-src/J.java @@ -0,0 +1,22 @@ +import java.util.function.*; + +@FunctionalInterface +interface SAMJava1 { + public String[] f(String x); +} + +@FunctionalInterface +interface SAMJava2 { + public void f(int x); +} + +class J { + public void g1(SAMJava1 s) { + } + + public void g2(SAMJava2 s) { + } + + public void h1(Function s) { + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/interop-sam-src/S.scala b/tests/explicit-nulls/pos/interop-sam-src/S.scala new file mode 100644 index 000000000000..c0da89163018 --- /dev/null +++ b/tests/explicit-nulls/pos/interop-sam-src/S.scala @@ -0,0 +1,19 @@ +def m = { + val j: J = ??? + + def f1(x: String | Null): Array[String | Null] | Null = null + + def f2(i: Int): Unit = () + + j.g1(f1) + j.g1((_: String | Null) => null) + j.g1(null) + + j.g2(f2) + j.g2((_: Int) => ()) + j.g2(null) + + j.h1(f1) + j.h1((_: String | Null) => null) + j.h1(null) +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/interop-static-src/J.java b/tests/explicit-nulls/pos/interop-static-src/J.java index 10965aa9ef4c..a233d9662950 100644 --- a/tests/explicit-nulls/pos/interop-static-src/J.java +++ b/tests/explicit-nulls/pos/interop-static-src/J.java @@ -1,4 +1,5 @@ class J { static int foo(String s) { return 42; } + static String bar(int i) { return null; } } diff --git a/tests/explicit-nulls/pos/interop-static-src/S.scala b/tests/explicit-nulls/pos/interop-static-src/S.scala index e54a33cd175b..3db9c3f6d281 100644 --- a/tests/explicit-nulls/pos/interop-static-src/S.scala +++ b/tests/explicit-nulls/pos/interop-static-src/S.scala @@ -1,6 +1,5 @@ - class S { - - J.foo(null) // Java static methods are also nullified - + // Java static methods are also nullified + val x: Int = J.foo(null) + val y: String | Null = J.bar(0) } diff --git a/tests/explicit-nulls/pos/interop-valuetypes.scala b/tests/explicit-nulls/pos/interop-valuetypes.scala index d7b0a867d5d3..0c20fb097cce 100644 --- a/tests/explicit-nulls/pos/interop-valuetypes.scala +++ b/tests/explicit-nulls/pos/interop-valuetypes.scala @@ -1,6 +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|UncheckedNull + val len: Int = x.length() // type is Int and not Int|Null } diff --git a/tests/explicit-nulls/pos/java-null.scala b/tests/explicit-nulls/pos/java-null.scala deleted file mode 100644 index ddb4a1026338..000000000000 --- a/tests/explicit-nulls/pos/java-null.scala +++ /dev/null @@ -1,16 +0,0 @@ -// Test that `UncheckedNull`able unions are transparent -// w.r.t member selections. - -class Test { - val s: String|UncheckedNull = "hello" - val l: Int = s.length // ok: `UncheckedNull` allows (unsound) member selections. - - val s2: UncheckedNull|String = "world" - val l2: Int = s2.length - - val s3: UncheckedNull|String|UncheckedNull = "hello" - val l3: Int = s3.length - - val s4: (String|UncheckedNull)&(UncheckedNull|String) = "hello" - val l4 = s4.length -} diff --git a/tests/explicit-nulls/pos/nn2.scala b/tests/explicit-nulls/pos/nn2.scala index 417d8855e405..a39618b97f22 100644 --- a/tests/explicit-nulls/pos/nn2.scala +++ b/tests/explicit-nulls/pos/nn2.scala @@ -1,4 +1,3 @@ - // Test that is fixed when explicit nulls are enabled. // https://github.com/lampepfl/dotty/issues/6247 diff --git a/tests/explicit-nulls/pos/notnull/S.scala b/tests/explicit-nulls/pos/notnull/S.scala index 2700ec939c9b..1b80b9e524b2 100644 --- a/tests/explicit-nulls/pos/notnull/S.scala +++ b/tests/explicit-nulls/pos/notnull/S.scala @@ -10,6 +10,6 @@ class S_3 { 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 | UncheckedNull] = (new J).genericf(a) + def genericff(a: String | Null): Array[String | Null] = (new J).genericf(a) def genericgg(a: String | Null): java.util.List[String] = (new J).genericg(a) } diff --git a/tests/explicit-nulls/pos/opaque-nullable.scala b/tests/explicit-nulls/pos/opaque-nullable.scala index 4b6f4f3f88aa..a7f626054ad3 100644 --- a/tests/explicit-nulls/pos/opaque-nullable.scala +++ b/tests/explicit-nulls/pos/opaque-nullable.scala @@ -10,12 +10,13 @@ object Nullable { def some[A <: AnyRef](x: A): Nullable[A] = x def none: Nullable[Nothing] = null - implicit class NullableOps[A <: AnyRef](x: Nullable[A]) { + extension [A <: AnyRef](x: Nullable[A]) def isEmpty: Boolean = x == null - def flatMap[B <: AnyRef](f: A => Nullable[B]): Nullable[B] = + + extension [A <: AnyRef, B <: AnyRef](x: Nullable[A]) + def flatMap(f: A => Nullable[B]): Nullable[B] = if (x == null) null else f(x) - } val s1: Nullable[String] = "hello" val s2: Nullable[String] = null diff --git a/tests/explicit-nulls/pos/option-transform.scala b/tests/explicit-nulls/pos/option-transform.scala new file mode 100644 index 000000000000..d665e5dde05a --- /dev/null +++ b/tests/explicit-nulls/pos/option-transform.scala @@ -0,0 +1,17 @@ +class OptionTransform { + /** Transform an nullable value to Option. It returns Some(x) if the argument x is not null, + * and None if it is null. + * + * @return Some(value) if value != null, None if value == null + */ + extension[T <: AnyRef](x: T | Null) def toOption: Option[T] = + if x == null then None else Some(x) + + def test = { + val x: String | Null = ??? + val y: Option[String] = x.toOption + + val xs: Array[String | Null] = ??? + val ys: Array[Option[String]] = xs.map(_.toOption) + } +} \ No newline at end of file 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 index 333e6e710d57..757a3b6b1235 100644 --- a/tests/explicit-nulls/pos/override-java-object-arg-src/S.scala +++ b/tests/explicit-nulls/pos/override-java-object-arg-src/S.scala @@ -1,4 +1,3 @@ - // 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'. @@ -15,6 +14,20 @@ class S { override def handleNotification(n: Notification|Null, emitter: AnyRef|Null): Unit = { } } - } + val listener3 = new NotificationListener() { + override def handleNotification(n: Notification|Null, emitter: Object): Unit = { + } + } + + val listener4 = new NotificationListener() { + override def handleNotification(n: Notification, emitter: Object|Null): Unit = { + } + } + + val listener5 = new NotificationListener() { + override def handleNotification(n: Notification, emitter: Object): Unit = { + } + } + } } diff --git a/tests/explicit-nulls/pos/override-java-object-arg.scala b/tests/explicit-nulls/pos/override-java-object-arg.scala index 3591d46d2e95..8c5a76e15a6c 100644 --- a/tests/explicit-nulls/pos/override-java-object-arg.scala +++ b/tests/explicit-nulls/pos/override-java-object-arg.scala @@ -1,9 +1,8 @@ - // 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 overridden -// with a corresponding argument with type 'AnyRef'. +// with a corresponding argument with type 'AnyRef | Null' (or `Object | Null`). // This test verifies that we can continue to override such methods, except that in // the explicit nulls world we override with 'AnyRef|Null'. @@ -25,6 +24,20 @@ class Foo { override def handleNotification(n: Notification|Null, emitter: AnyRef|Null): Unit = { } } - } + val listener3 = new NotificationListener() { + override def handleNotification(n: Notification|Null, emitter: Object): Unit = { + } + } + + val listener4 = new NotificationListener() { + override def handleNotification(n: Notification, emitter: Object|Null): Unit = { + } + } + + val listener5 = new NotificationListener() { + override def handleNotification(n: Notification, emitter: Object): Unit = { + } + } + } } diff --git a/tests/explicit-nulls/pos/override-java-varargs/J.java b/tests/explicit-nulls/pos/override-java-varargs/J.java new file mode 100644 index 000000000000..24313aad2241 --- /dev/null +++ b/tests/explicit-nulls/pos/override-java-varargs/J.java @@ -0,0 +1,4 @@ +abstract class J { + abstract void foo(String... x); + abstract void bar(String x, String... y); +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/override-java-varargs/S.scala b/tests/explicit-nulls/pos/override-java-varargs/S.scala new file mode 100644 index 000000000000..bb98c86b455c --- /dev/null +++ b/tests/explicit-nulls/pos/override-java-varargs/S.scala @@ -0,0 +1,14 @@ +class S1 extends J { + override def foo(x: (String | Null)*): Unit = ??? + override def bar(x: String | Null, y: (String | Null)*): Unit = ??? +} + +class S2 extends J { + override def foo(x: String*): Unit = ??? + override def bar(x: String | Null, y: String*): Unit = ??? +} + +class S3 extends J { + override def foo(x: String*): Unit = ??? + override def bar(x: String, y: String*): Unit = ??? +} diff --git a/tests/explicit-nulls/pos/override-java/J1.java b/tests/explicit-nulls/pos/override-java/J1.java new file mode 100644 index 000000000000..0c66c26fdea9 --- /dev/null +++ b/tests/explicit-nulls/pos/override-java/J1.java @@ -0,0 +1,4 @@ +abstract class J1 { + abstract void foo1(String x); + abstract String foo2(); +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/override-java/J2.java b/tests/explicit-nulls/pos/override-java/J2.java new file mode 100644 index 000000000000..8ff04d59f54f --- /dev/null +++ b/tests/explicit-nulls/pos/override-java/J2.java @@ -0,0 +1,9 @@ +import java.util.List; + +abstract class J2 { + abstract void bar1(List xs); + abstract void bar2(List xss); + + abstract List bar3(); + abstract List bar4(); +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/override-java/S1.scala b/tests/explicit-nulls/pos/override-java/S1.scala new file mode 100644 index 000000000000..01a95c8e0ef7 --- /dev/null +++ b/tests/explicit-nulls/pos/override-java/S1.scala @@ -0,0 +1,9 @@ +class S1a extends J1 { + override def foo1(x: String | Null): Unit = ??? + override def foo2(): String | Null = ??? +} + +class S1b extends J1 { + override def foo1(x: String): Unit = ??? + override def foo2(): String = ??? +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/override-java/S2.scala b/tests/explicit-nulls/pos/override-java/S2.scala new file mode 100644 index 000000000000..ec440ca8f150 --- /dev/null +++ b/tests/explicit-nulls/pos/override-java/S2.scala @@ -0,0 +1,25 @@ +import java.util.List + +class S2a extends J2 { + override def bar1(xs: List[String] | Null): Unit = ??? + override def bar2(xss: List[Array[String | Null]] | Null): Unit = ??? + + override def bar3(): List[String] | Null = ??? + override def bar4(): List[Array[String | Null]] | Null = ??? +} + +class S2b extends J2 { + override def bar1(xs: List[String]): Unit = ??? + override def bar2(xss: List[Array[String | Null]]): Unit = ??? + + override def bar3(): List[String] = ??? + override def bar4(): List[Array[String | Null]] = ??? +} + +class S2c extends J2 { + override def bar1(xs: List[String]): Unit = ??? + override def bar2(xss: List[Array[String]]): Unit = ??? + + override def bar3(): List[String] = ??? + override def bar4(): List[Array[String]] = ??? +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/sam-type.scala b/tests/explicit-nulls/pos/sam-type.scala new file mode 100644 index 000000000000..0eddfb36ef37 --- /dev/null +++ b/tests/explicit-nulls/pos/sam-type.scala @@ -0,0 +1,4 @@ +def f = { + val smap: Map[String, String] = ??? + val ss = smap.map { case (n, v) => (n, n + v) } +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/unpickler.scala b/tests/explicit-nulls/pos/unpickler.scala new file mode 100644 index 000000000000..bca668a58250 --- /dev/null +++ b/tests/explicit-nulls/pos/unpickler.scala @@ -0,0 +1,5 @@ +// Ensure we don't have cyclic complete + +import scala.collection.immutable.BitSet + +val e = BitSet.empty \ No newline at end of file diff --git a/tests/explicit-nulls/pos/dont-widen.scala b/tests/explicit-nulls/pos/widen-dont.scala similarity index 100% rename from tests/explicit-nulls/pos/dont-widen.scala rename to tests/explicit-nulls/pos/widen-dont.scala diff --git a/tests/explicit-nulls/run/erasure.scala b/tests/explicit-nulls/run/erasure.scala new file mode 100644 index 000000000000..3b97439fe4c9 --- /dev/null +++ b/tests/explicit-nulls/run/erasure.scala @@ -0,0 +1,6 @@ +object Test { + def main(args: Array[String]): Unit = { + val v: Vector[String | Null] = Vector("a", "b") + println(v) + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/run/instanceof-nothing.scala b/tests/explicit-nulls/run/instanceof-nothing.scala index e51aabc7fe00..ef5fc4ede841 100644 --- a/tests/explicit-nulls/run/instanceof-nothing.scala +++ b/tests/explicit-nulls/run/instanceof-nothing.scala @@ -2,6 +2,7 @@ // 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" diff --git a/tests/explicit-nulls/run/java-null.scala b/tests/explicit-nulls/run/java-null.scala deleted file mode 100644 index ba3ba46b48dd..000000000000 --- a/tests/explicit-nulls/run/java-null.scala +++ /dev/null @@ -1,17 +0,0 @@ -// Check that selecting a member from a `UncheckedNull`able union is unsound. - -object Test { - def main(args: Array[String]): Unit = { - val s: String|UncheckedNull = "hello" - assert(s.length == 5) - - val s2: String|UncheckedNull = null - try { - s2.length // should throw - assert(false) - } catch { - case e: NullPointerException => - // ok: selecting on a UncheckedNull can throw - } - } -} diff --git a/tests/explicit-nulls/run/unsafe-nulls.scala b/tests/explicit-nulls/run/unsafe-nulls.scala new file mode 100644 index 000000000000..384a99e0ff97 --- /dev/null +++ b/tests/explicit-nulls/run/unsafe-nulls.scala @@ -0,0 +1,36 @@ +// Check that selecting a member from a nullable union is unsound. +// Enabling unsafeNulls allows this kind of unsafe operations, +// but could cause exception during runtime. + +object F { + def apply(x: String): String = x +} + +object G { + def h(f: String | Null => String, x: String | Null): String | Null = + f(x) +} + +object Test { + import scala.language.unsafeNulls + + def main(args: Array[String]): Unit = { + val s1: String | Null = "hello" + assert(s1.length == 5) + + val s2: String | Null = null + try { + s2.length // should throw + assert(false) + } catch { + case e: NullPointerException => + // ok: Selecting on a null value would throw NullPointerException. + } + + val s3: String = F(s1) + assert(s3.length == 5) + + val s4: String = G.h(F.apply, s1) + assert(s4.length == 5) + } +} diff --git a/tests/explicit-nulls/unsafe-common/README.md b/tests/explicit-nulls/unsafe-common/README.md new file mode 100644 index 000000000000..db3e46db5cec --- /dev/null +++ b/tests/explicit-nulls/unsafe-common/README.md @@ -0,0 +1,5 @@ +Common tests for unsafe-nulls feature. + +All the tests should be compiled with `-language:unsafeNulls`. + +All the tests should produce errors at `// error` comment locations without `-language:unsafeNulls`. diff --git a/tests/explicit-nulls/unsafe-common/unsafe-cast.scala b/tests/explicit-nulls/unsafe-common/unsafe-cast.scala new file mode 100644 index 000000000000..0e319f067ad9 --- /dev/null +++ b/tests/explicit-nulls/unsafe-common/unsafe-cast.scala @@ -0,0 +1,73 @@ +class S { + def m0(s: String): String = s + + def m1(s: String): String | Null = s + + def n0(x: Array[String]): Array[String] = x + + def n1(x: Array[String | Null]): Array[String | Null] = x + + def n2(x: Array[String | Null] | Null): Array[String | Null] | Null = x + + def test1 = { + val s: String = ??? + + val a1: String | Null = s // safe + val a2: String = a1 // error + + val b1 = s.trim() // String | Null + val b2 = b1.trim() // error + val b3 = b1.length() // error + + val c1: String | Null = null // safe + val c2: String = null // error + val c3: Int | String = null // error + + val d1: Array[String | Null] | Null = Array(s) + val d2: Array[String] = d1 // error + val d3: Array[String | Null] = d2 // error + val d4: Array[String] = Array(null) // error + } + + def test2 = { + m0("") + m0(null) // error + + val a: String | Null = ??? + val b: String = m0(a) // error + val c: String = m1(a).trim() // error + + val x: Array[String | Null] | Null = ??? + val y: Array[String] = ??? + val z: Array[String | Null] = ??? + + n0(x) // error + n0(y) + n0(z) // error + + n1(x) // error + n1(y) // error + n1(z) + + n2(x) + n2(y) // error + n2(z) + + n0(Array("a", "b")) + n1(Array("a", "b")) + n2(Array("a", "b")) + + n0(Array[String](null)) // error + n1(Array(null)) + n2(Array(null)) + + n0(Array("a", null)) // error + n1(Array("a", null)) + n2(Array("a", null)) + } + + locally { + val os: Option[String] = None + val s: String = os.orNull // error + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/unsafe-common/unsafe-chain.scala b/tests/explicit-nulls/unsafe-common/unsafe-chain.scala new file mode 100644 index 000000000000..f087e22d50f9 --- /dev/null +++ b/tests/explicit-nulls/unsafe-common/unsafe-chain.scala @@ -0,0 +1,9 @@ +// Test that we can select through "| Null" is unsafeNulls is enabled (unsoundly). + +class Foo { + import java.util.ArrayList + import java.util.Iterator + + val x3 = new ArrayList[ArrayList[ArrayList[String]]]() + val x4: Int = x3.get(0).get(0).get(0).length() // error +} diff --git a/tests/explicit-nulls/unsafe-common/unsafe-eq.scala b/tests/explicit-nulls/unsafe-common/unsafe-eq.scala new file mode 100644 index 000000000000..48fcd07bef80 --- /dev/null +++ b/tests/explicit-nulls/unsafe-common/unsafe-eq.scala @@ -0,0 +1,16 @@ +val s1: String = ??? +val s2: String | Null = ??? + +def f = { + s1 eq s2 // error + s2 eq s1 // error + + s1 ne s2 // error + s2 ne s1 // error + + s1 eq null // error + s2 eq null // error + + null eq s1 // error + null eq s2 // error +} \ No newline at end of file diff --git a/tests/explicit-nulls/unsafe-common/unsafe-equal.scala b/tests/explicit-nulls/unsafe-common/unsafe-equal.scala new file mode 100644 index 000000000000..493aaebfbff2 --- /dev/null +++ b/tests/explicit-nulls/unsafe-common/unsafe-equal.scala @@ -0,0 +1,49 @@ +class S { + val s1: String | Null = ??? + val s2: String = ??? + val n: Null = ??? + val ss1: Array[String] = ??? + val ss2: Array[String | Null] = ??? + + locally { + s1 == null + s1 != null + null == s1 + null != s1 + + s2 == null // error + s2 != null // error + null == s2 // error + null != s2 // error + + s1 == s2 + s1 != s2 + s2 == s1 + s2 != s1 + + n == null + n != null + null == n + null != n + + s1 == n + s2 == n // error + n != s1 + n != s2 // error + } + + locally { + ss1 == null // error + ss1 != null // error + null == ss1 // error + null != ss1 // error + + ss1 == n // error + ss1 != n // error + n == ss1 // error + n != ss1 // error + + ss1 == ss2 + ss2 != ss1 + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/unsafe-common/unsafe-extensions.scala b/tests/explicit-nulls/unsafe-common/unsafe-extensions.scala new file mode 100644 index 000000000000..844c311bec83 --- /dev/null +++ b/tests/explicit-nulls/unsafe-common/unsafe-extensions.scala @@ -0,0 +1,47 @@ +class Extensions { + + extension (s1: String) def ext1(s2: String): Unit = ??? + extension (s1: String | Null) def ext2(s2: String | Null): Unit = ??? + + val x: String = ??? + val y: String | Null = ??? + + locally { + x.ext1(x) + x.ext1(y) // error + y.ext1(x) // error + y.ext1(y) // error + } + + locally { + x.ext2(x) + x.ext2(y) + y.ext2(x) + y.ext2(y) + } + + extension (ss1: Array[String]) def exts1(ss2: Array[String]): Unit = ??? + extension (ss1: Array[String | Null]) def exts2(ss2: Array[String | Null]): Unit = ??? + + val xs: Array[String] = ??? + val ys: Array[String | Null] = ??? + + locally { + xs.exts1(xs) + xs.exts1(ys) // error + ys.exts1(xs) // error + ys.exts1(ys) // error + } + + locally { + xs.exts2(xs) // error + xs.exts2(ys) // error + ys.exts2(xs) // error + ys.exts2(ys) + } + + // i7828 + locally { + val x = "hello, world!".split(" ").map(_.length) // error + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/unsafe-common/unsafe-implicit.scala b/tests/explicit-nulls/unsafe-common/unsafe-implicit.scala new file mode 100644 index 000000000000..01ebbde2aa01 --- /dev/null +++ b/tests/explicit-nulls/unsafe-common/unsafe-implicit.scala @@ -0,0 +1,59 @@ +class S { + + def f1(using String) = {} + + def f2(using String | Null) = {} + + locally { + implicit val x: String = ??? + + val y1: String = summon + val y2: String | Null = summon + } + + def test1(implicit x: String) = { + val y1: String = summon + val y2: String | Null = summon + } + + def test2(using String) = { + val y1: String = summon + val y2: String | Null = summon + } + + def test3(using String) = { + f1 + f2 + } + + locally { + implicit val x: String | Null = ??? + + val y1: String = summon // error + val y2: String | Null = summon + } + + def test4(implicit x: String | Null) = { + val y1: String = summon // error + val y2: String | Null = summon + } + + def test5(using String | Null) = { + val y1: String = summon // error + val y2: String | Null = summon + } + + def test6(using String | Null) = { + f1 // error + f2 + } + + locally { + // OfType Implicits + + import java.nio.charset.StandardCharsets + import scala.io.Codec + + val c: Codec = StandardCharsets.UTF_8 // error + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/unsafe-common/unsafe-implicit2.scala b/tests/explicit-nulls/unsafe-common/unsafe-implicit2.scala new file mode 100644 index 000000000000..37fd9bb24251 --- /dev/null +++ b/tests/explicit-nulls/unsafe-common/unsafe-implicit2.scala @@ -0,0 +1,56 @@ +class S { + locally { + implicit def f(x: String): Array[String] = ??? + + val y1: String = ??? + val y2: String | Null = ??? + + y1: Array[String] + y1: Array[String | Null] // error + y1: Array[String] | Null + y1: Array[String | Null] | Null // error + + y2: Array[String] // error + y2: Array[String | Null] // error + y2: Array[String] | Null // error + y2: Array[String | Null] | Null // error + } + + locally { + implicit def g(x: Array[String]): String = ??? + + val y1: Array[String] = ??? + val y2: Array[String] | Null = ??? + val y3: Array[String | Null] = ??? + val y4: Array[String | Null] | Null = ??? + + y1: String + y2: String // error + y3: String // error + y4: String // error + + y1: String | Null + y2: String | Null // error + y3: String | Null // error + y4: String | Null // error + } + + locally { + implicit def g(x: Array[String | Null]): String | Null = ??? + + val y1: Array[String] = ??? + val y2: Array[String] | Null = ??? + val y3: Array[String | Null] = ??? + val y4: Array[String | Null] | Null = ??? + + y1: String // error + y2: String // error + y3: String // error + y4: String // error + + y1: String | Null // error + y2: String | Null // error + y3: String | Null + y4: String | Null // error + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/unsafe-common/unsafe-implicit3.scala b/tests/explicit-nulls/unsafe-common/unsafe-implicit3.scala new file mode 100644 index 000000000000..06d894d85555 --- /dev/null +++ b/tests/explicit-nulls/unsafe-common/unsafe-implicit3.scala @@ -0,0 +1,115 @@ +class S { + import scala.language.implicitConversions + + class C[T](x: T) {} + + locally { + given Conversion[String, Array[String]] = _ => ??? + + val y1: String = ??? + val y2: String | Null = ??? + + y1: Array[String] + y1: Array[String | Null] // error + y1: Array[String] | Null + y1: Array[String | Null] | Null // error + + y2: Array[String] // error + y2: Array[String | Null] // error + y2: Array[String] | Null // error + y2: Array[String | Null] | Null // error + } + + locally { + given Conversion[Array[String], String] = _ => ??? + + val y1: Array[String] = ??? + val y2: Array[String] | Null = ??? + val y3: Array[String | Null] | Null = ??? + + y1: String + y2: String // error + y3: String // error + + y1: String | Null + y2: String | Null // error + y3: String | Null // error + } + + locally { + given Conversion[Array[String | Null], String] = _ => ??? + + val y1: Array[String] = ??? + val y2: Array[String] | Null = ??? + val y3: Array[String | Null] = ??? + val y4: Array[String | Null] | Null = ??? + + y1: String // error + y2: String // error + y3: String + y4: String // error + + y1: String | Null // error + y2: String | Null // error + y3: String | Null + y4: String | Null // error + } + + locally { + given Conversion[C[Array[String | Null]], String] = _ => ??? + + val y1: C[Array[String]] = ??? + val y2: C[Array[String] | Null] = ??? + val y3: C[Array[String | Null]] = ??? + val y4: C[Array[String | Null] | Null] = ??? + val y5: C[Array[String | Null] | Null] | Null = ??? + + y1: String // error + y2: String // error + y3: String + y4: String // error + y5: String // error + + y1: String | Null // error + y2: String | Null // error + y3: String | Null + y4: String | Null // error + y5: String | Null // error + } + + abstract class MyConversion[T] extends Conversion[T, Array[T]] + + locally { + given MyConversion[String] = _ => ??? + + val y1: String = ??? + val y2: String | Null = ??? + + val z1: Array[String] = y1 + val z2: Array[String | Null] = y1 // error + val z3: Array[String] | Null = y1 + val z4: Array[String | Null] | Null = y1 // error + + val z5: Array[String] = y2 // error + val z6: Array[String | Null] = y2 // error + val z7: Array[String] | Null = y2 // error + val z8: Array[String | Null] | Null = y2 // error + } + + def test5[T >: Null <: AnyRef | Null] = { + given Conversion[T, Array[T]] = _ => ??? + + val y1: T = ??? + val y2: T | Null = ??? + + val z1: Array[T] = y1 + val z2: Array[T | Null] = y1 + val z3: Array[T] | Null = y1 + val z4: Array[T | Null] | Null = y1 + + val z5: Array[T] = y2 + val z6: Array[T | Null] = y2 + val z7: Array[T] | Null = y2 + val z8: Array[T | Null] | Null = y2 + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/interop-javanull-src/J.java b/tests/explicit-nulls/unsafe-common/unsafe-java-chain/J.java similarity index 60% rename from tests/explicit-nulls/pos/interop-javanull-src/J.java rename to tests/explicit-nulls/unsafe-common/unsafe-java-chain/J.java index a85afa17c859..bd266bae13d9 100644 --- a/tests/explicit-nulls/pos/interop-javanull-src/J.java +++ b/tests/explicit-nulls/unsafe-common/unsafe-java-chain/J.java @@ -1,8 +1,7 @@ - class J1 { J2 getJ2() { return new J2(); } } class J2 { - J1 getJ1() { return new J1(); } -} + J1 getJ1() { return new J1(); } +} \ No newline at end of file diff --git a/tests/explicit-nulls/unsafe-common/unsafe-java-chain/S.scala b/tests/explicit-nulls/unsafe-common/unsafe-java-chain/S.scala new file mode 100644 index 000000000000..9fe5aa3f08ce --- /dev/null +++ b/tests/explicit-nulls/unsafe-common/unsafe-java-chain/S.scala @@ -0,0 +1,4 @@ +class S { + val j: J2 = new J2() + j.getJ1().getJ2().getJ1().getJ2().getJ1().getJ2() // error +} diff --git a/tests/explicit-nulls/unsafe-common/unsafe-java-varargs-src/J.java b/tests/explicit-nulls/unsafe-common/unsafe-java-varargs-src/J.java new file mode 100644 index 000000000000..21ba08be66c9 --- /dev/null +++ b/tests/explicit-nulls/unsafe-common/unsafe-java-varargs-src/J.java @@ -0,0 +1,3 @@ +abstract class J { + abstract void foo(String... x); +} \ No newline at end of file diff --git a/tests/explicit-nulls/unsafe-common/unsafe-java-varargs-src/S.scala b/tests/explicit-nulls/unsafe-common/unsafe-java-varargs-src/S.scala new file mode 100644 index 000000000000..e27b0dcaacbf --- /dev/null +++ b/tests/explicit-nulls/unsafe-common/unsafe-java-varargs-src/S.scala @@ -0,0 +1,19 @@ +class S { + val j: J = ??? + + j.foo() + j.foo("") + j.foo(null) + j.foo("", "") + j.foo("", null, "") + + val arg1: Array[String] = ??? + val arg2: Array[String | Null] = ??? + val arg3: Array[String] | Null = ??? + val arg4: Array[String | Null] | Null = ??? + + j.foo(arg1: _*) + j.foo(arg2: _*) + j.foo(arg3: _*) // error + j.foo(arg4: _*) // error +} \ No newline at end of file diff --git a/tests/explicit-nulls/unsafe-common/unsafe-java-varargs.scala b/tests/explicit-nulls/unsafe-common/unsafe-java-varargs.scala new file mode 100644 index 000000000000..8e61f5763391 --- /dev/null +++ b/tests/explicit-nulls/unsafe-common/unsafe-java-varargs.scala @@ -0,0 +1,38 @@ +import java.nio.file.Paths + +def test1 = { + Paths.get("") + Paths.get("", null) + Paths.get("", "") + Paths.get("", "", null) + + val x1: String = ??? + val x2: String | Null = ??? + + Paths.get("", x1) + Paths.get("", x2) +} + +def test2 = { + val xs1: Seq[String] = ??? + val xs2: Seq[String | Null] = ??? + val xs3: Seq[String | Null] | Null = ??? + val xs4: Seq[String] | Null = ??? + + val ys1: Array[String] = ??? + val ys2: Array[String | Null] = ??? + val ys3: Array[String | Null] | Null = ??? + val ys4: Array[String] | Null = ??? + + Paths.get("", xs1: _*) + Paths.get("", xs2: _*) + Paths.get("", xs3: _*) // error + Paths.get("", xs4: _*) // error + + Paths.get("", ys1: _*) + Paths.get("", ys2: _*) + Paths.get("", ys3: _*) // error + Paths.get("", ys4: _*) // error + + Paths.get("", null: _*) // error +} \ No newline at end of file diff --git a/tests/explicit-nulls/unsafe-common/unsafe-overload.scala b/tests/explicit-nulls/unsafe-common/unsafe-overload.scala new file mode 100644 index 000000000000..e7e551f1bda1 --- /dev/null +++ b/tests/explicit-nulls/unsafe-common/unsafe-overload.scala @@ -0,0 +1,59 @@ +class S { + class O { + def f(s: String | Null): String | Null = ??? + def f(ss: Array[String] | Null): Array[String] | Null = ??? + + def g(s: String): String = ??? + def g(ss: Array[String]): Array[String] = ??? + + def h(ts: String => String): String = ??? + def h(ts: Array[String] => Array[String]): Array[String] = ??? + + def i(ts: String | Null => String | Null): String | Null = ??? + def i(ts: Array[String] | Null => Array[String] | Null): Array[String] | Null = ??? + } + + val o: O = ??? + + locally { + def h1(hh: String => String) = ??? + def h2(hh: Array[String] => Array[String]) = ??? + def f1(x: String | Null): String | Null = ??? + def f2(x: Array[String | Null]): Array[String | Null] = ??? + + h1(f1) // error + h1(o.f) // error + + h2(f2) // error + h2(o.f) // error + } + + locally { + def h1(hh: String | Null => String | Null) = ??? + def h2(hh: Array[String | Null] => Array[String | Null]) = ??? + def g1(x: String): String = ??? + def g2(x: Array[String]): Array[String] = ??? + + h1(g1) // error + h1(o.g) // error + + h2(g2) // error + h2(o.g) // error + } + + locally { + def f1(x: String | Null): String | Null = ??? + def f2(x: Array[String | Null]): Array[String | Null] = ??? + + o.h(f1) // error + o.h(f2) // error + } + + locally { + def g1(x: String): String = ??? + def g2(x: Array[String]): Array[String] = ??? + + o.i(g1) // error + o.i(g2) // error + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/unsafe-common/unsafe-path.scala b/tests/explicit-nulls/unsafe-common/unsafe-path.scala new file mode 100644 index 000000000000..52f4e3c0bf21 --- /dev/null +++ b/tests/explicit-nulls/unsafe-common/unsafe-path.scala @@ -0,0 +1,22 @@ +class S { + class O { + type I = Int + val a: I = 1 + + type S = String | Null + val s: S = "" + } + + def f = { + val o: O = new O + val m: O | Null = o + val n0: o.I = o.a + val n1: m.I = 0 // error + val n2: Int = m.a // error + + val s1: m.S = ??? // error + val s2: m.S | Null = ??? // error + val s3: String = m.s // error + val ss: String = o.s // error + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/unsafe-common/unsafe-select.scala b/tests/explicit-nulls/unsafe-common/unsafe-select.scala new file mode 100644 index 000000000000..b1615b006311 --- /dev/null +++ b/tests/explicit-nulls/unsafe-common/unsafe-select.scala @@ -0,0 +1,36 @@ +class C { + var x: String = "" + var y: String | Null = null + var child: C | Null = null +} + +class S { + val c: C = new C + val d: C | Null = c + + def test1 = { + val x1: String = c.x + val x2: String | Null = c.x + val y1: String = c.y // error + val y2: String | Null = c.y + val c1: C = c.child // error + val c2: C | Null = c.child + + val yy: String = c.child.child.y // error + } + + def test2 = { + c.x = "" + c.x = null // error + c.y = "" + c.y = null + c.child = c + c.child = null + } + + def test3 = { + d.x = "" // error + d.y = "" // error + d.child = c // error + } +} \ No newline at end of file diff --git a/tests/neg-custom-args/explicit-nulls/i7883.check b/tests/neg-custom-args/explicit-nulls/i7883.check index 57775b962f3f..70cd510c3d21 100644 --- a/tests/neg-custom-args/explicit-nulls/i7883.check +++ b/tests/neg-custom-args/explicit-nulls/i7883.check @@ -5,7 +5,7 @@ | (m: scala.util.matching.Regex.Match): Option[List[String]] | (c: Char): Option[List[Char]] | (s: CharSequence): Option[List[String]] - | match arguments (String | UncheckedNull) + | match arguments (String | Null) -- [E006] Not Found Error: tests/neg-custom-args/explicit-nulls/i7883.scala:6:30 --------------------------------------- 6 | case r(hd, tl) => Some((hd, tl)) // error // error // error | ^^ diff --git a/tests/neg-custom-args/explicit-nulls/i7883.scala b/tests/neg-custom-args/explicit-nulls/i7883.scala index 9ee92553b60d..7938c92dce1e 100644 --- a/tests/neg-custom-args/explicit-nulls/i7883.scala +++ b/tests/neg-custom-args/explicit-nulls/i7883.scala @@ -6,4 +6,11 @@ object Test extends App { case r(hd, tl) => Some((hd, tl)) // error // error // error case _ => None } + + def headUnsafe(s: String, r: Regex): Option[(String, String)] = + import scala.language.unsafeNulls + s.trim match { + case r(hd, tl) => Some((hd, tl)) + case _ => None + } } \ No newline at end of file