diff --git a/community-build/community-projects/shapeless b/community-build/community-projects/shapeless index 27d1b736962b..273524d806fc 160000 --- a/community-build/community-projects/shapeless +++ b/community-build/community-projects/shapeless @@ -1 +1 @@ -Subproject commit 27d1b736962b07e4561abe1dc939c16da2cbce28 +Subproject commit 273524d806fc38a4fdfff27550c3fb3720716754 diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 206e02ec9245..ed60b817a11f 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -946,7 +946,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { /** The current tree applied to given type argument list: `tree[targs(0), ..., targs(targs.length - 1)]` */ def appliedToTypeTrees(targs: List[Tree])(using Context): Tree = - if (targs.isEmpty) tree else TypeApply(tree, targs) + if targs.isEmpty then tree else TypeApply(tree, targs) /** Apply to `()` unless tree's widened type is parameterless */ def ensureApplied(using Context): Tree = diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 176f9c80cc29..916a5132e48e 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -452,27 +452,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 @@ -633,7 +617,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 @@ -645,15 +629,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,8 @@ 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.mode.is(Mode.SafeNulls) && !ctx.phase.erasedTypes + then cls == NothingClass else isBottomClassAfterErasure(cls) def isBottomClassAfterErasure(cls: Symbol): Boolean = cls == NothingClass || cls == NullClass @@ -1700,8 +1683,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 +1698,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 183928342626..ff8a1ef035ce 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 f46fe5ef6198..7f25655d0334 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -798,7 +798,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? @@ -1829,10 +1830,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/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index f0af9e7ffc28..ccf344fa8b49 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -277,26 +277,33 @@ object TypeErasure { case _ => tp } - /** Is `tp` an abstract type or polymorphic type parameter that has `Any`, `AnyVal`, + /** Is `tp` an abstract type or polymorphic type parameter that has `Any`, `AnyVal`, `Null`, * or a universal trait as upper bound and that is not Java defined? Arrays of such types are * erased to `Object` instead of `Object[]`. */ - def isUnboundedGeneric(tp: Type)(using Context): Boolean = tp.dealias match { - case tp: TypeRef if !tp.symbol.isOpaqueAlias => - !tp.symbol.isClass && - !classify(tp).derivesFrom(defn.ObjectClass) && - !tp.symbol.is(JavaDefined) - case tp: TypeParamRef => - !classify(tp).derivesFrom(defn.ObjectClass) - case tp: TypeAlias => isUnboundedGeneric(tp.alias) - case tp: TypeBounds => - val upper = classify(tp.hi) - !upper.derivesFrom(defn.ObjectClass) && - !upper.isPrimitiveValueType - case tp: TypeProxy => isUnboundedGeneric(tp.translucentSuperType) - case tp: AndType => isUnboundedGeneric(tp.tp1) && isUnboundedGeneric(tp.tp2) - case tp: OrType => isUnboundedGeneric(tp.tp1) || isUnboundedGeneric(tp.tp2) - case _ => false + def isUnboundedGeneric(tp: Type)(using Context): Boolean = { + def isBoundedType(t: Type): Boolean = t match { + case t: OrType => isBoundedType(t.tp1) && isBoundedType(t.tp2) + case _ => t.derivesFrom(defn.ObjectClass) || t.isNullType + } + + tp.dealias match { + case tp: TypeRef if !tp.symbol.isOpaqueAlias => + !tp.symbol.isClass && + !isBoundedType(classify(tp)) && + !tp.symbol.is(JavaDefined) + case tp: TypeParamRef => + !isBoundedType(classify(tp)) + case tp: TypeAlias => isUnboundedGeneric(tp.alias) + case tp: TypeBounds => + val upper = classify(tp.hi) + !isBoundedType(upper) && + !upper.isPrimitiveValueType + case tp: TypeProxy => isUnboundedGeneric(tp.translucentSuperType) + case tp: AndType => isUnboundedGeneric(tp.tp1) && isUnboundedGeneric(tp.tp2) + case tp: OrType => isUnboundedGeneric(tp.tp1) || isUnboundedGeneric(tp.tp2) + case _ => false + } } /** Is `tp` an abstract type or polymorphic type parameter, or another unbounded generic type? */ diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index cfd441f36bc6..72fcd284b924 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 } @@ -168,6 +170,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 @@ -211,6 +215,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 => @@ -250,6 +261,10 @@ object Types { * a non-bottom subclass of `cls`. */ final def derivesFrom(cls: Symbol)(using Context): Boolean = { + def isLowerBottomType(tp: Type) = + tp.isBottomType + && (tp.hasClassSymbol(defn.NothingClass) + || cls != defn.NothingClass && !cls.isValueClass) def loop(tp: Type): Boolean = tp match { case tp: TypeRef => val sym = tp.symbol @@ -266,10 +281,6 @@ object Types { // If the type is `T | Null` or `T | Nothing`, the class is != Nothing, // and `T` derivesFrom the class, then the OrType derivesFrom the class. // Otherwise, we need to check both sides derivesFrom the class. - def isLowerBottomType(tp: Type) = - tp.isBottomType - && (tp.hasClassSymbol(defn.NothingClass) - || cls != defn.NothingClass && !cls.isValueClass) if isLowerBottomType(tp.tp1) then loop(tp.tp2) else if isLowerBottomType(tp.tp2) then @@ -469,7 +480,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 @@ -484,12 +495,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 */ @@ -817,12 +838,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 @@ -1065,10 +1084,15 @@ object Types { * * (*) when matching with a Java method, we also regard Any and Object as equivalent * parameter types. + * + * Under explicit nulls, this function will always use unsafe-nulls semamtics to + * check the types. This is because we are using a relaxed rule (ignoring `Null` types) + * to check overriding Java methods. */ 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 @@ -1600,6 +1624,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 @@ -3177,31 +3204,10 @@ object Types { */ object OrNull { def apply(tp: Type)(using Context) = - OrType(tp, defn.NullType, 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.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) - 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 78d058ef0a11..f210f9934df2 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 => @@ -1249,7 +1252,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() @@ -1491,4 +1494,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 a38c2a12fd0a..7738bda6a800 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._ @@ -780,7 +781,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) @@ -1259,7 +1262,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() @@ -1320,4 +1323,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/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 96e5b2ecae3a..675ac9f58a35 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -437,4 +437,4 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase private def normalizeErasedRhs(rhs: Tree, sym: Symbol)(using Context) = if (sym.isEffectivelyErased) dropInlines.transform(rhs) else rhs } -} +} \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala index 36a9812b9864..21a88ec50e76 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 10dddd12a90a..e3a0489cf0fa 100644 --- a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala @@ -402,7 +402,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 565f7e51d85d..e00253515aa8 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 diff --git a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala index ffac54960578..108b31cc27be 100644 --- a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala +++ b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala @@ -141,7 +141,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)]() @@ -155,7 +155,13 @@ 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 + 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..2969d7f4804f 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 @@ -1699,4 +1699,3 @@ object TermRefSet: override def += (ref: TermRef): Unit = throw UnsupportedOperationException("+=") end TermRefSet - 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 566891903344..4321d67677bc 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..76c8523adb14 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,12 @@ 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 we have to check whether a variable with a non-nullable type has null value + // (for example, a NotNull java method returns null for some reasons), + // we can still cast it to a nullable type then compare its value. // // 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 8bc16f1aa738..8ce74a6061cc 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 a8f889cf56eb..850cfedfb366 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -433,7 +433,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 @@ -592,9 +592,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 javaSelectOnType(qual: Tree)(using Context) = // semantic name conversion for `O$` in java code @@ -806,11 +817,22 @@ class Typer extends Namer // so the expected type is the union `Seq[T] | Array[_ <: T]`. 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) then + 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.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) } @@ -1339,8 +1361,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, @@ -1698,7 +1720,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 1c65c423040e..725041f36988 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -295,7 +295,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() @@ -303,7 +304,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..6269c81ae163 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,36 @@ 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 Feature and SafeNulls Mode -- `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. + +We use the `SafeNulls` mode to track `unsafeNulls`. If explicit nulls is enabled without `unsafeNulls`, there is a `SafeNulls` mode in the context; when `unsafeNulls` is enabled, `SafeNulls` mode will be removed from the context. + +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`. ## 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 3afda5002829..d823b545e60b 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 849825ea3d76..2daa11816303 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -93,6 +93,11 @@ object language: */ object adhocExtensions + /** 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 object future 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/array.scala b/tests/explicit-nulls/neg/array.scala new file mode 100644 index 000000000000..83e39cc80929 --- /dev/null +++ b/tests/explicit-nulls/neg/array.scala @@ -0,0 +1,44 @@ +// Test array with nulls. + +class ArrayWithNulls { + def test1 = { + // A non-nullable array of non-nullable strings + val a1: Array[String] = Array("hello") + val s1: String = a1(0) + val s2: String | Null = a1(0) + val a2: Array[String] = Array() + + // Array type is non-nullable + val b1: Array[String] = null // error + val b2: Array[Int] = null // error + } + + def test2 = { + // A nullable array of non-nullable strings + val a1: Array[String] | Null = null + val a2: Array[String] | Null = Array() + val a3: Array[String] | Null = Array("") + val a4: Array[String] | Null = Array("", null) // error + + // A non-nullable array of nullable strings + val b1: Array[String | Null] = Array() + val b2: Array[String | Null] = Array(null) + val b3: Array[String | Null] = Array("") + val b4: Array[String | Null] = Array("", null) + val b5: Array[String | Null] = null // error + + val s1: String = b1(0) // error + val s2: String | Null = b1(0) + + // A nullable array of nullable strings + val c1: Array[String | Null] | Null = Array() + } + + def test3 = { + val a1: Array[String] = Array() + + val a2: Array[String] | Null = a1 + + val a3: Array[String | Null] = a1 // error + } +} diff --git a/tests/explicit-nulls/neg/basic.scala b/tests/explicit-nulls/neg/basic.scala index 7c652887590b..cafe4b156d85 100644 --- a/tests/explicit-nulls/neg/basic.scala +++ b/tests/explicit-nulls/neg/basic.scala @@ -1,11 +1,33 @@ // 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 -} +class Basic { + val no: Nothing = ??? + + val n: Null = null + val n2: Null = no + + val any1: Any = null + val any2: Any = n + + val s1: String = null // error + val s2: String = n // error + val s3: String | Null = null + val s4: String | Null = n + val s5: String | Null = "" + val ar1: AnyRef = null // error + val ar2: AnyRef = n // error + val ar3: AnyRef | Null = null + val ob1: Object = null // error + val ob2: Object | Null = null + + val b1: Boolean = null // error + val b2: Boolean = n // error + val b3: Boolean | Null = null + + val c1: Int = null // error + val c2: Int = n // error + val i3: Int | Null = null + + val av: AnyVal = null // error +} diff --git a/tests/explicit-nulls/neg/bounds.scala b/tests/explicit-nulls/neg/bounds.scala new file mode 100644 index 000000000000..5631ee11781e --- /dev/null +++ b/tests/explicit-nulls/neg/bounds.scala @@ -0,0 +1,30 @@ +// Test Null type in bounds + +class Test { + 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, so it is impossible to apply this method directly. + // However, defining this kind of functions is allowed. + // 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 + } + + // Bounds in class definition is strictly checked + class A[T >: Null <: AnyRef] {} // error: conflicting bounds Null <: ... <: AnyRef +} diff --git a/tests/explicit-nulls/neg/default.scala b/tests/explicit-nulls/neg/default.scala index fe115861e926..2b696b2991ea 100644 --- a/tests/explicit-nulls/neg/default.scala +++ b/tests/explicit-nulls/neg/default.scala @@ -1,13 +1,18 @@ - class Foo { - val x: String = null // error: String is non-nullable + val s: String = null // error: String is non-nullable def foo(x: String): String = "x" - val y = foo(null) // error: String argument is non-nullable + val fn = foo(null) // error: String argument is non-nullable + + val x: String = foo("hello") + + val arr: Array[Int] = null // error: Array is non-nullable + + val t: (String, Int) = null // error: Tuple is non-nullable - val z: String = foo("hello") + val f: String => Int = null // error: Function is non-nullable 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-basic.check b/tests/explicit-nulls/neg/nn-basic.check new file mode 100644 index 000000000000..49d2e9ddf649 --- /dev/null +++ b/tests/explicit-nulls/neg/nn-basic.check @@ -0,0 +1,16 @@ +-- [E008] Not Found Error: tests/explicit-nulls/neg/nn-basic.scala:3:21 ------------------------------------------------ +3 | val s2: String = s.trim // error + | ^^^^^^ + | value trim is not a member of String | Null. + | Since explicit-nulls is enabled, the selection is rejected because + | String | Null could be null at runtime. + | If you want to select trim without checking for a null value, + | insert a .nn before .trim or import scala.language.unsafeNulls. +-- [E008] Not Found Error: tests/explicit-nulls/neg/nn-basic.scala:5:17 ------------------------------------------------ +5 | val l: Int = s.length // error + | ^^^^^^^^ + | value length is not a member of String | Null. + | Since explicit-nulls is enabled, the selection is rejected because + | String | Null could be null at runtime. + | If you want to select length without checking for a null value, + | insert a .nn before .length or import scala.language.unsafeNulls. diff --git a/tests/explicit-nulls/neg/nn-basic.scala b/tests/explicit-nulls/neg/nn-basic.scala new file mode 100644 index 000000000000..2eeab6ba2c84 --- /dev/null +++ b/tests/explicit-nulls/neg/nn-basic.scala @@ -0,0 +1,6 @@ +class Test { + val s: String | Null = ??? + val s2: String = s.trim // error + val s3: String | Null = s.nn.trim + val l: Int = s.length // error +} \ No newline at end of file diff --git a/tests/explicit-nulls/neg/nn.scala b/tests/explicit-nulls/neg/nn.scala new file mode 100644 index 000000000000..4ecaa1953b7c --- /dev/null +++ b/tests/explicit-nulls/neg/nn.scala @@ -0,0 +1,18 @@ +// `.nn` extension method only strips away the outer Null. + +class Test { + val s1: String | Null = ??? + val s2: String = s1 // error + val s3: String = s1.nn + + val l1: Int = s1.length // error + val l2: Int = s1.nn.length + + val ss1: Array[String | Null] | Null = ??? + val ss2: Array[String | Null] = ss1.nn + val ss3: Array[String] = ss1.nn // error + val ss4: Array[String] = ss1.asInstanceOf + + val a1: String | Null = ss1(0) // error + val a2: String | Null = ss1.nn(0) +} \ 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/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 7a45cb1d6199..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() { - override def handleNotification(n: Notification|Null, emitter: Object): Unit = { // error: method handleNotification overrides nothing - } - } - - val listener2 = new NotificationListener() { - override def handleNotification(n: Notification|Null, emitter: Object|Null): Unit = { // ok - } - } - - val listener3 = new NotificationListener() { - override def handleNotification(n: Notification, emitter: Object|Null): Unit = { // error: method handleNotification overrides nothing - } - } - } -} - diff --git a/tests/explicit-nulls/neg/override-java-object-arg2.scala b/tests/explicit-nulls/neg/override-java-object-arg2.scala deleted file mode 100644 index 1646ca37fc7c..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() { - 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/neg/varargs.scala b/tests/explicit-nulls/neg/varargs.scala new file mode 100644 index 000000000000..cc27a4c9fb26 --- /dev/null +++ b/tests/explicit-nulls/neg/varargs.scala @@ -0,0 +1,50 @@ +class Varargs { + 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 = ??? + + def f1(xs: String*): Unit = ??? + def f2(xs: (String | Null)*): Unit = ??? + + def test = { + f1() + f1(null) // error + f1("") + f1("", null) // error + f1(null: _*) // error + + f1(xs1: _*) + f1(xs2: _*) // error + f1(xs3: _*) // error + f1(xs4: _*) // error + + f1(ys1: _*) + f1(ys2: _*) // error + f1(ys3: _*) // error + f1(ys4: _*) // error + } + + def test2 = { + f2() + f2(null) + f2("") + f2("", null) + f2(null: _*) // error + + f2(xs1: _*) + f2(xs2: _*) + f2(xs3: _*) // error + f2(xs4: _*) // error + + f2(ys1: _*) + f2(ys2: _*) + f2(ys3: _*) // error + f2(ys4: _*) // 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/array.scala b/tests/explicit-nulls/pos/array.scala index f3146c8e8e2b..7b188356f921 100644 --- a/tests/explicit-nulls/pos/array.scala +++ b/tests/explicit-nulls/pos/array.scala @@ -2,4 +2,24 @@ class Foo { val x: Array[String] = Array("hello") val s: String = x(0) + + def test = { + // accept any array of string + def f(xs: Array[_ >: String <: String | Null] | Null): Unit = ??? + + val a1: Array[String] = ??? + val a2: Array[String] | Null = ??? + val a3: Array[String | Null] = ??? + val a4: Array[String | Null] | Null = ??? + + f(null) + f(Array()) + f(Array(null)) + f(Array("")) + f(Array("", null)) + f(a1) + f(a2) + f(a3) + f(a4) + } } 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-function.scala b/tests/explicit-nulls/unsafe-common/unsafe-cast-function.scala new file mode 100644 index 000000000000..fe87204e626a --- /dev/null +++ b/tests/explicit-nulls/unsafe-common/unsafe-cast-function.scala @@ -0,0 +1,15 @@ +class Test { + def f(g: String => String): Unit = ??? + + def g1(x: String): String = ??? + def g2(x: String | Null): String = ??? + def g3(x: String): String | Null = ??? + def g4(x: String | Null): String | Null = ??? + + def test = { + f(g1) // ok + f(g2) // ok: (String | Null => String) <:< (String => String) + f(g3) // error + f(g4) // error + } +} 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