diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index ff731f171dc4..750966ba793d 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -461,22 +461,6 @@ class Definitions { } 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 @@ -653,14 +637,14 @@ 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 + val pt1 = if (ctx.explicitNulls) pt.stripNull else pt pt1.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 + val pt1 = if (ctx.explicitNulls) pt.stripNull else pt pt1.isRef(StringClass) case _ => false }).symbol.asTerm @@ -1622,8 +1606,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, AnyRefAlias, AnyKindClass, @@ -1636,9 +1620,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..5e12d0b0c1a1 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 @@ -35,6 +37,12 @@ import NullOpsDecorator._ */ object JavaNullInterop { + /** Should we try to convert values ignoring Null type at this moment? */ + def convertUnsafeNulls(using Context): Boolean = + ctx.explicitNulls && ( + config.Feature.enabled(nme.unsafeNulls) || + ctx.mode.is(Mode.UnsafeNullConversion)) + /** Transforms the type `tp` of Java member `sym` to be explicitly nullable. * `tp` is needed because the type inside `sym` might not be set when this method is called. * @@ -43,10 +51,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 * @@ -81,20 +88,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 +114,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 +122,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 +132,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)) OrNull(appTp2) else appTp2 case ptp: PolyType => derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType)) case mtp: MethodType => @@ -136,11 +143,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 219099adcfb1..2b4544fdedc8 100644 --- a/compiler/src/dotty/tools/dotc/core/Mode.scala +++ b/compiler/src/dotty/tools/dotc/core/Mode.scala @@ -112,4 +112,7 @@ object Mode { /** Are we in a quote in a pattern? */ val QuotedPattern: Mode = newMode(25, "QuotedPattern") + + /** Should we try to convert values ignoring Null type? */ + val UnsafeNullConversion: Mode = newMode(26, "UnsafeNullConversion") } diff --git a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala index 878c52d5f32b..83c27772a1f1 100644 --- a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala +++ b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala @@ -1,86 +1,84 @@ package dotty.tools.dotc.core -import dotty.tools.dotc.core.Contexts._ -import dotty.tools.dotc.core.Symbols.defn -import dotty.tools.dotc.core.Types._ +import Contexts.{Context, ctx} +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) - } /** 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 */ - def stripNull(onlyUncheckedNull: Boolean = false)(using Context): Type = { - assert(ctx.explicitNulls) - - def isNull(tp: Type) = - if (onlyUncheckedNull) tp.isUncheckedNullType - else tp.isNullType + def stripNull(using Context): Type = { + def strip(tp: Type): Type = + val tpWiden = tp.widenDealias + val tpStriped = 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 tpStriped ne tpWiden then tpStriped else tp - 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 + strip(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 = { + def stripAllNulls(using Context): Type = { object RemoveNulls extends TypeMap { - override def apply(tp: Type): Type = mapOver(tp.stripNull(true)) + override def apply(tp: Type): Type = + val mapped = mapOver(tp.widenTermRefExpr.stripNull) + tp match { + case tr: TermRef => + if tp eq mapped then tp + else AndType(tr, mapped) + case _ => + mapped + } } val rem = RemoveNulls(self) - if (rem ne self) rem else self + if rem ne self then rem 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 } - /** 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 + /** Can the type have null value after erasure? + */ + def isNullableAfterErasure(using Context): Boolean = self match { + case tp: ClassInfo => tp.cls.isNullableClassAfterErasure + case tp: TypeProxy => tp.underlying.isNullableAfterErasure + case OrType(lhs, rhs) => + lhs.isNullableAfterErasure || rhs.isNullableAfterErasure + case _ => + self.isNullType || self <:< defn.ObjectType } + + /** Can we convert a tree with type `self` to type `pt` unsafely. + */ + def isUnsafeConvertable(pt: Type)(using Context): Boolean = + (self.isNullType && pt.isNullableAfterErasure) || + (self.stripAllNulls <:< pt.stripAllNulls) } } diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 12c8d4ea817b..34076a410553 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" @@ -612,6 +611,7 @@ object StdNames { val unapplySeq: N = "unapplySeq" val unbox: N = "unbox" val universe: N = "universe" + val unsafeNulls: N = "unsafeNulls" val update: N = "update" val updateDynamic: N = "updateDynamic" val using: N = "using" diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 64f97f8a7c1f..76ff7a683137 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -11,6 +11,7 @@ import transform.ValueClasses._ import transform.TypeUtils._ import transform.ContextFunctionResults._ import Decorators._ +import NullOpsDecorator._ import Definitions.MaxImplementedFunctionArity import scala.annotation.tailrec @@ -514,8 +515,11 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean private def eraseArray(tp: Type)(using Context) = { val defn.ArrayOf(elemtp) = tp if (classify(elemtp).derivesFrom(defn.NullClass)) JavaArrayType(defn.ObjectType) - else if (isUnboundedGeneric(elemtp) && !isJava) defn.ObjectType - else JavaArrayType(erasureFn(isJava, semiEraseVCs = false, isConstructor, wildcardOK)(elemtp)) + else { + val elemtp1 = if (ctx.explicitNulls) elemtp.stripNull else elemtp + if (isUnboundedGeneric(elemtp1) && !isJava) defn.ObjectType + else JavaArrayType(erasureFn(isJava, semiEraseVCs = false, isConstructor, wildcardOK)(elemtp1)) + } } private def erasePair(tp: Type)(using Context): Type = { diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 8c3883cfd2f6..bc3d7fd07c4d 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -739,12 +739,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 config.Feature.enabled(nme.unsafeNulls) => + // 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 @@ -989,7 +987,12 @@ object Types { */ def matches(that: Type)(using Context): Boolean = { record("matches") - TypeComparer.matchesType(this, that, relaxed = !ctx.phase.erasedTypes) + TypeComparer.matchesType(this, that, relaxed = !ctx.phase.erasedTypes) || + (ctx.explicitNulls && + // TODO: optimize, for example, add a parameter to ignore Null type? + TypeComparer.matchesType( + this.stripAllNulls, that.stripAllNulls, + relaxed = !ctx.phase.erasedTypes)) } /** This is the same as `matches` except that it also matches => T with T and @@ -3054,25 +3057,7 @@ object Types { OrType(tp, defn.NullType) 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) - def unapply(tp: Type)(using Context): Option[Type] = - if (ctx.explicitNulls) { - val tp1 = tp.stripUncheckedNull + val tp1 = tp.stripNull if tp1 ne tp then Some(tp1) else None } else None diff --git a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala index 374d938e5523..2f899d906b1a 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 @@ -45,7 +46,8 @@ class ExpandSAMs extends MiniPhase { checkRefinements(tpe, fn) tree case tpe => - val tpe1 = checkRefinements(tpe, fn) + val tpe0 = if ctx.explicitNulls then tpe.stripNull else tpe + val tpe1 = checkRefinements(tpe0, 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 643f62933629..2b75b2d14fa4 100644 --- a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala +++ b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala @@ -51,21 +51,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.derivesFrom(tree.symbol.owner) || tree.symbol.is(JavaStatic) && qualTpe.derivesFrom(tree.symbol.enclosingClass), diff --git a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala index bbd2f13395a0..ac92b0f4ea65 100644 --- a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala +++ b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala @@ -202,7 +202,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 + val pinfo = if (ctx.explicitNulls) m.paramInfos.head.stripNull else m.paramInfos.head pinfo == defn.StringType case _ => false } diff --git a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala index 32be8fc1dd18..b747bbda0af5 100644 --- a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala @@ -13,6 +13,7 @@ import core.NameKinds.{DocArtifactName, OuterSelectName} import core.Decorators._ import core.Phases._ import core.Mode +import core.NullOpsDecorator._ import typer._ import typer.ErrorReporting._ import reporting._ @@ -400,7 +401,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}""") } checkNotRepeated(super.typedSelect(tree, pt)) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index ad0d7406369c..02ccdb74bc08 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 @@ -767,7 +768,7 @@ trait Checking { * - it is defined in Predef * - it is the scala.reflect.Selectable.reflectiveSelectable conversion */ - def checkImplicitConversionUseOK(sym: Symbol, pos: SrcPos)(using Context): Unit = + def checkImplicitConversionUseOK(sym: Symbol, tpe: Type, pos: SrcPos)(using Context): Unit = if (sym.exists) { val conv = if (sym.isOneOf(GivenOrImplicit) || sym.info.isErroneous) sym @@ -775,9 +776,10 @@ trait Checking { assert(sym.name == nme.apply || ctx.reporter.errorsReported) sym.owner } + val tpe1 = if ctx.explicitNulls then tpe.stripNull else tpe val conversionOK = conv.is(Synthetic) || - sym.info.finalResultType.classSymbols.exists(_.isLinkedWith(conv.owner)) || + tpe1.classSymbols.exists(_.isLinkedWith(conv.owner)) || defn.isPredefClass(conv.owner) || conv.name == nme.reflectiveSelectable && conv.maybeOwner.maybeOwner.maybeOwner == defn.ScalaPackageClass if (!conversionOK) @@ -1244,7 +1246,7 @@ trait NoChecking extends ReChecking { override def checkStable(tp: Type, pos: SrcPos, kind: String)(using Context): Unit = () override def checkClassType(tp: Type, pos: SrcPos, traitReq: Boolean, stablePrefixReq: Boolean)(using Context): Type = tp override def checkImplicitConversionDefOK(sym: Symbol)(using Context): Unit = () - override def checkImplicitConversionUseOK(sym: Symbol, pos: SrcPos)(using Context): Unit = () + override def checkImplicitConversionUseOK(sym: Symbol, tpe: Type, pos: SrcPos)(using Context): Unit = () override def checkFeasibleParent(tp: Type, pos: SrcPos, where: => String = "")(using Context): Type = tp override def checkInlineConformant(tpt: Tree, tree: Tree, sym: Symbol)(using Context): Unit = () override def checkNoAlphaConflict(stats: List[Tree])(using Context): Unit = () diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 07791b9d046b..4b61dfbba8d0 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -314,11 +314,11 @@ object Implicits: } /** The implicit references that are eligible for type `tp`. */ - def eligible(tp: Type): List[Candidate] = + def eligible(tp: Type, enableUnsafeNulls: Boolean = false): List[Candidate] = if (tp.hash == NotCached) Stats.record(i"compute eligible not cached ${tp.getClass}") Stats.record(i"compute eligible not cached") - computeEligible(tp) + computeEligible(tp, enableUnsafeNulls) else { val eligibles = eligibleCache.lookup(tp) if (eligibles != null) { @@ -328,15 +328,16 @@ object Implicits: else if (irefCtx eq NoContext) Nil else { Stats.record(i"compute eligible cached") - val result = computeEligible(tp) + val result = computeEligible(tp, enableUnsafeNulls) eligibleCache(tp) = result result } } - private def computeEligible(tp: Type): List[Candidate] = /*>|>*/ trace(i"computeEligible $tp in $refs%, %", implicitsDetailed) /*<|<*/ { + private def computeEligible(tp: Type, enableUnsafeNulls: Boolean): List[Candidate] = /*>|>*/ trace(i"computeEligible $tp in $refs%, %", implicitsDetailed) /*<|<*/ { if (monitored) record(s"check eligible refs in irefCtx", refs.length) - val ownEligible = filterMatching(tp) + val c = if enableUnsafeNulls then ctx.addMode(Mode.UnsafeNullConversion) else ctx + val ownEligible = filterMatching(tp)(using c) if (isOuterMost) ownEligible else if ownEligible.isEmpty then outerImplicits.eligible(tp) else @@ -1297,7 +1298,7 @@ trait Implicits: private def searchImplicit(contextual: Boolean): SearchResult = val eligible = - if contextual then ctx.implicits.eligible(wildProto) + if contextual then ctx.implicits.eligible(wildProto, ctx.mode.is(Mode.UnsafeNullConversion)) else implicitScope(wildProto).eligible searchImplicit(eligible, contextual) match case result: SearchSuccess => diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 0686ff0d4eb5..6a889a313e9f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -242,7 +242,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/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index 08485bf9799e..04a2caf129f7 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -10,6 +10,7 @@ import Trees._ import Constants._ import util.{Stats, SimpleIdentityMap} import Decorators._ +import NullOpsDecorator._ import Uniques._ import config.Printers.typr import util.SourceFile @@ -36,7 +37,17 @@ object ProtoTypes { * If `pt` is a by-name type, we compare against the underlying type instead. */ def isCompatible(tp: Type, pt: Type)(using Context): Boolean = - (tp.widenExpr relaxed_<:< pt.widenExpr) || viewExists(tp, pt) + val tpw = tp.widenExpr + val ptw = pt.widenExpr + (tpw relaxed_<:< ptw) || + ctx.explicitNulls && + // If unsafeNulls is enabled, we relax the condition by striping all nulls from the types + // before subtype check. We use Feature to check language feature. However, when we search implicits, + // the context is from ContextualImplicits; hence, we don't know whether unsafeNulls is enabled. + // We have to add Mode.UnsafeNullConversion before implicit search. + (config.Feature.enabled(nme.unsafeNulls) || ctx.mode.is(Mode.UnsafeNullConversion)) && + (tpw.stripAllNulls relaxed_<:< ptw.stripAllNulls) || + viewExists(tp, pt) /** Like isCompatibe, but using a subtype comparison with necessary eithers * that don't unnecessarily truncate the constraint space, returning false instead. @@ -44,7 +55,15 @@ object ProtoTypes { def necessarilyCompatible(tp: Type, pt: Type)(using Context): Boolean = val tpw = tp.widenExpr val ptw = pt.widenExpr - necessarySubType(tpw, ptw) || tpw.isValueSubType(ptw) || viewExists(tp, pt) + necessarySubType(tpw, ptw) || tpw.isValueSubType(ptw) || + ctx.explicitNulls && { + val tpwsn = tpw.stripAllNulls + val ptwsn = ptw.stripAllNulls + // See comments in `isCompatible` + (config.Feature.enabled(nme.unsafeNulls) || ctx.mode.is(Mode.UnsafeNullConversion)) && + (necessarySubType(tpwsn, ptwsn) || tpwsn.isValueSubType(ptwsn)) + } || + viewExists(tp, pt) /** Test compatibility after normalization. * Do this in a fresh typerstate unless `keepConstraint` is true. @@ -132,7 +151,7 @@ object ProtoTypes { // equals comes from case class; no need to redefine end IgnoredProto - + final class CachedIgnoredProto(ignored: Type) extends IgnoredProto(ignored) object IgnoredProto: diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index c85aee7aa65b..9a591f4fb33f 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -21,6 +21,7 @@ import config.Feature.{warnOnMigration, migrateTo3} import reporting._ import scala.util.matching.Regex._ import Constants.Constant +import NullOpsDecorator._ object RefChecks { import tpd.{Tree, MemberDef, Literal, Template, DefDef} @@ -292,7 +293,10 @@ object RefChecks { memberTp.overrides(otherTp, member.matchNullaryLoosely || other.matchNullaryLoosely || warnOnMigration(overrideErrorMsg("no longer has compatible type"), - (if (member.owner == clazz) member else clazz).srcPos)) + (if (member.owner == clazz) member else clazz).srcPos)) || + // releaxed override check for explicit nulls + (ctx.explicitNulls && (member.is(JavaDefined) || other.is(JavaDefined)) && + memberTp.stripAllNulls.overrides(otherTp.stripAllNulls, true)) catch { case ex: MissingType => // can happen when called with upwardsSelf as qualifier of memberTp and otherTp, diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index bb34a230bf49..b199ddd74851 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -123,10 +123,11 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): cmpWithBoxed(cls1, cls2) else if cls2.isPrimitiveValueClass then cmpWithBoxed(cls2, cls1) - else if ctx.explicitNulls then - // If explicit nulls is enabled, we want to disallow comparison between Object and Null. - // If a nullable value has a non-nullable type, we can still cast it to nullable type - // then compare. + else if ctx.explicitNulls && !config.Feature.enabled(nme.unsafeNulls) then + // If explicit nulls is enabled, and unsafeNulls is not enabled, + // we want to disallow comparison between Object and Null. + // If a nullable value has a non-nullable type, we can still cast it to + // nullable type then compare. // // Example: // val x: String = null.asInstanceOf[String] diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index d40ec154e12d..f2778f7fc2ea 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -422,7 +422,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 @@ -554,8 +554,13 @@ class Typer extends Namer record("typedSelect") def typeSelectOnTerm(using Context): Tree = - typedSelect(tree, pt, typedExpr(tree.qualifier, selectionProto(tree.name, pt, this))) - .computeNullable() + val qual = typedExpr(tree.qualifier, selectionProto(tree.name, pt, this)) + val qual1 = qual.tpe match { + case OrNull(tpe1) if config.Feature.enabled(nme.unsafeNulls) => + qual.cast(AndType(qual.tpe, tpe1)) + case _ => qual + } + typedSelect(tree, pt, qual1).computeNullable() def typeSelectOnType(qual: untpd.Tree)(using Context) = typedSelect(untpd.cpy.Select(tree)(qual, tree.name.toTypeName), pt) @@ -1248,26 +1253,28 @@ class Typer extends Namer if (tree.tpt.isEmpty) meth1.tpe.widen match { case mt: MethodType => - pt match { + val pt1 = if ctx.explicitNulls then pt.stripNull else pt + pt1 match { case SAMType(sam) - if !defn.isFunctionType(pt) && mt <:< sam => + if !defn.isFunctionType(pt1) && mt <:< sam || + JavaNullInterop.convertUnsafeNulls && mt.isUnsafeConvertable(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, // since type arguments to `C`'s super constructor cannot be constructed. def isWildcardClassSAM = - !pt.classSymbol.is(Trait) && pt.argInfos.exists(_.isInstanceOf[TypeBounds]) + !pt1.classSymbol.is(Trait) && pt1.argInfos.exists(_.isInstanceOf[TypeBounds]) val targetTpe = - if isFullyDefined(pt, ForceDegree.all) && !isWildcardClassSAM then - pt - else if pt.isRef(defn.PartialFunctionClass) then + if isFullyDefined(pt1, ForceDegree.all) && !isWildcardClassSAM then + pt1 + else if pt1.isRef(defn.PartialFunctionClass) then // Replace the underspecified expected type by one based on the closure method type defn.PartialFunctionOf(mt.firstParamTypes.head, mt.resultType) else - report.error(ex"result type of lambda is an underspecified SAM type $pt", tree.srcPos) - pt - if (pt.classSymbol.isOneOf(FinalOrSealed)) { - val offendingFlag = pt.classSymbol.flags & FinalOrSealed - report.error(ex"lambda cannot implement $offendingFlag ${pt.classSymbol}", tree.srcPos) + report.error(ex"result type of lambda is an underspecified SAM type $pt1", tree.srcPos) + pt1 + if (pt1.classSymbol.isOneOf(FinalOrSealed)) { + val offendingFlag = pt1.classSymbol.flags & FinalOrSealed + report.error(ex"lambda cannot implement $offendingFlag ${pt1.classSymbol}", tree.srcPos) } TypeTree(targetTpe) case _ => @@ -3433,9 +3440,13 @@ class Typer extends Namer tree match { case closure(Nil, id @ Ident(nme.ANON_FUN), _) if defn.isFunctionType(wtp) && !defn.isFunctionType(pt) => - pt match { + // Strip the Null type from the pt before check SAM type + val pt1 = if ctx.explicitNulls then pt.stripNull else pt + pt1 match { case SAMType(sam) - if wtp <:< sam.toFunctionType() => + if wtp <:< sam.toFunctionType() || + (JavaNullInterop.convertUnsafeNulls && + wtp.isUnsafeConvertable(sam.toFunctionType())) => // was ... && isFullyDefined(pt, ForceDegree.flipBottom) // but this prevents case blocks from implementing polymorphic partial functions, // since we do not know the result parameter a priori. Have to wait until the @@ -3492,26 +3503,57 @@ class Typer extends Namer ctx.typerState.constraint.ne(prevConstraint)) readapt(tree) else err.typeMismatch(tree, pt, failure) - if ctx.mode.is(Mode.ImplicitsEnabled) && tree.typeOpt.isValueType then - if pt.isRef(defn.AnyValClass) || pt.isRef(defn.ObjectClass) then - report.error(em"the result of an implicit conversion must be more specific than $pt", tree.srcPos) - inferView(tree, pt) match { + def searchTree(t: Tree)(fail: SearchFailure => Tree)(using Context) = { + inferView(t, pt) match { case SearchSuccess(found: ExtMethodApply, _, _) => found // nothing to check or adapt for extension method applications - case SearchSuccess(found, _, _) => - checkImplicitConversionUseOK(found.symbol, tree.srcPos) + case SearchSuccess(found, ref, _) => + checkImplicitConversionUseOK(ref.symbol, found.typeOpt, t.srcPos) withoutMode(Mode.ImplicitsEnabled)(readapt(found)) case failure: SearchFailure => - if (pt.isInstanceOf[ProtoType] && !failure.isAmbiguous) then - // don't report the failure but return the tree unchanged. This - // will cause a failure at the next level out, which usually gives - // a better error message. To compensate, store the encountered failure - // as an attachment, so that it can be reported later as an addendum. - rememberSearchFailure(tree, failure) - tree - else recover(failure.reason) + fail(failure) } - else recover(NoMatchingImplicits) + } + + def tryUnsafeNullConver(fail: => Tree)(using Context): Tree = + // If explicitNulls and unsafeNulls are enabled, and + if ctx.mode.is(Mode.UnsafeNullConversion) && pt.isValueType && + tree.tpe.isUnsafeConvertable(pt) + then tree.cast(pt) + else fail + + def cannotFind(failure: SearchFailure) = + if (pt.isInstanceOf[ProtoType] && !failure.isAmbiguous) then + // don't report the failure but return the tree unchanged. This + // will cause a failure at the next level out, which usually gives + // a better error message. To compensate, store the encountered failure + // as an attachment, so that it can be reported later as an addendum. + rememberSearchFailure(tree, failure) + tree + else recover(failure.reason) + + val searchCtx = + if ctx.explicitNulls && config.Feature.enabled(nme.unsafeNulls) then + ctx.addMode(Mode.UnsafeNullConversion) + else ctx + + inContext(searchCtx) { + if ctx.mode.is(Mode.ImplicitsEnabled) && tree.typeOpt.isValueType then + if pt.isRef(defn.AnyValClass) || pt.isRef(defn.ObjectClass) then + report.error(em"the result of an implicit conversion must be more specific than $pt", tree.srcPos) + def normalSearch = + searchTree(tree)(failure => tryUnsafeNullConver(cannotFind(failure))) + tree.tpe match { + case OrNull(tpe1) if ctx.mode.is(Mode.UnsafeNullConversion) => + // If the type of the tree is nullable, and unsafeNullConversion is enabled, + // then we search the tree without the `Null` type first. + // If this fails, we search the original tree. + searchTree(tree.cast(tpe1)) { _ => normalSearch } + case _ => + normalSearch + } + else tryUnsafeNullConver(recover(NoMatchingImplicits)) + } } def adaptType(tp: Type): Tree = { diff --git a/library/src/dotty/DottyPredef.scala b/library/src/dotty/DottyPredef.scala index f06730568ec3..b98dc658d64f 100644 --- a/library/src/dotty/DottyPredef.scala +++ b/library/src/dotty/DottyPredef.scala @@ -75,14 +75,18 @@ object DottyPredef { // 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) def nn: x.type & T = - if (x == null) throw new NullPointerException("tried to cast away nullability, but value is null") + if x == null then + throw new NullPointerException("tried to cast away nullability, but value is null") else x.asInstanceOf[x.type & T] } diff --git a/library/src/scalaShadowing/language.scala b/library/src/scalaShadowing/language.scala index 3c2ab45c2b0d..c6029dc8bc83 100644 --- a/library/src/scalaShadowing/language.scala +++ b/library/src/scalaShadowing/language.scala @@ -250,4 +250,6 @@ object language { object `3.0` object `3.1-migration` object `3.1` + + object unsafeNulls } diff --git a/tests/explicit-nulls/neg-patmat/patmat1.scala b/tests/explicit-nulls/neg-patmat/patmat1.scala index 6e9710a56dec..a73b1fca2e1e 100644 --- a/tests/explicit-nulls/neg-patmat/patmat1.scala +++ b/tests/explicit-nulls/neg-patmat/patmat1.scala @@ -1,4 +1,3 @@ - class Foo { val s: String = ??? s match { diff --git a/tests/explicit-nulls/neg/alias.scala b/tests/explicit-nulls/neg/alias.scala index f8dea4864027..c84fa594b842 100644 --- a/tests/explicit-nulls/neg/alias.scala +++ b/tests/explicit-nulls/neg/alias.scala @@ -1,6 +1,6 @@ - // Test that nullability is correctly detected // in the presence of a type alias. + class Base { type T >: Null <: AnyRef|Null } @@ -13,7 +13,7 @@ object foo { } class Derived extends Base { - type Nullable[X] = X|Null + type Nullable[X] = X | Null type Foo = Nullable[foo.Foo] def fun(foo: Foo): Unit = { @@ -21,4 +21,3 @@ class Derived extends Base { foo.doFoo() // error: foo is nullable } } - diff --git a/tests/explicit-nulls/neg/basic.scala b/tests/explicit-nulls/neg/basic.scala deleted file mode 100644 index 7c652887590b..000000000000 --- a/tests/explicit-nulls/neg/basic.scala +++ /dev/null @@ -1,11 +0,0 @@ -// Test that reference types are no longer nullable. - -class Foo { - val s: String = null // error - val s1: String|Null = null // ok - val b: Boolean = null // error - val ar: AnyRef = null // error - val a: Any = null // ok - val n: Null = null // ok -} - diff --git a/tests/explicit-nulls/neg/default.scala b/tests/explicit-nulls/neg/default.scala deleted file mode 100644 index fe115861e926..000000000000 --- a/tests/explicit-nulls/neg/default.scala +++ /dev/null @@ -1,13 +0,0 @@ - -class Foo { - val x: String = null // error: String is non-nullable - - def foo(x: String): String = "x" - - val y = foo(null) // error: String argument is non-nullable - - val z: String = foo("hello") - - class Bar - val b: Bar = null // error: user-created classes are also non-nullable -} diff --git a/tests/explicit-nulls/neg/eq2.scala b/tests/explicit-nulls/neg/eq2.scala index 8c730407daa4..32cf21d918ac 100644 --- a/tests/explicit-nulls/neg/eq2.scala +++ b/tests/explicit-nulls/neg/eq2.scala @@ -1,18 +1,39 @@ -// Test that we can't compare for equality `null` and -// classes that derive from AnyVal. -class Foo(x: Int) extends AnyVal +// Test that we can't compare for equality `null` with classes. +// This rule is for both regular classes and value classes. -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 +class Foo(x: Int) +class Bar(x: Int) extends AnyVal - // 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) {} +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/nullable-types.scala b/tests/explicit-nulls/neg/nullable-types.scala new file mode 100644 index 000000000000..570d3cc53676 --- /dev/null +++ b/tests/explicit-nulls/neg/nullable-types.scala @@ -0,0 +1,19 @@ +// Test that reference types are no longer nullable. + +class Foo { + val s: String = null // error + val s1: String | Null = null // ok + val b: Boolean = null // error + val ar: AnyRef = null // error + val a: Any = null // ok + val n: Null = null // ok + + def foo(x: String): String = "x" + + val y = foo(null) // error: String argument is non-nullable + + val z: String = foo("hello") + + class Bar + val bar: Bar = null // error: user-created classes are also non-nullable +} diff --git a/tests/explicit-nulls/neg/nullnull.scala b/tests/explicit-nulls/neg/nullnull.scala index c5710e17e78a..e1a1cf74914e 100644 --- a/tests/explicit-nulls/neg/nullnull.scala +++ b/tests/explicit-nulls/neg/nullnull.scala @@ -1,7 +1,5 @@ // Test that `Null | Null | ... | Null` will not cause crash during typing. // We want to strip `Null`s from the type after the `if` statement. -// After `normNullableUnion`, `Null | Null | ... | Null` should become -// `Null | Null`, and `stripNull` will return type `Null`. class Foo { def foo1: Unit = { @@ -11,7 +9,7 @@ class Foo { } def foo2: Unit = { - val x: UncheckedNull | String | Null = ??? + val x: Null | String | Null = ??? if (x == null) return () val y = x.length // ok: x: String is inferred } diff --git a/tests/explicit-nulls/neg/override-java-object-arg.scala b/tests/explicit-nulls/neg/override-java-object-arg.scala deleted file mode 100644 index ccce4af660c7..000000000000 --- a/tests/explicit-nulls/neg/override-java-object-arg.scala +++ /dev/null @@ -1,26 +0,0 @@ - -// Test that we can properly override Java methods where an argument has type 'Object'. -// See pos/override-java-object-arg.scala for context. - -import javax.management.{Notification, NotificationEmitter, NotificationListener} - -class Foo { - - def bar(): Unit = { - val listener = new NotificationListener() { // error: object creation impossible - override def handleNotification(n: Notification|Null, emitter: Object): Unit = { // error: method handleNotification overrides nothing - } - } - - val listener2 = new NotificationListener() { - override def handleNotification(n: Notification|Null, emitter: Object|Null): Unit = { // ok - } - } - - val listener3 = new NotificationListener() { // error: object creation impossible - override def handleNotification(n: Notification, emitter: Object|Null): Unit = { // error: method handleNotification overrides nothing - } - } - } -} - diff --git a/tests/explicit-nulls/neg/override-java-object-arg2.scala b/tests/explicit-nulls/neg/override-java-object-arg2.scala deleted file mode 100644 index 9dae93be404f..000000000000 --- a/tests/explicit-nulls/neg/override-java-object-arg2.scala +++ /dev/null @@ -1,13 +0,0 @@ - -import javax.management.{Notification, NotificationEmitter, NotificationListener} - -class Foo { - - def bar(): Unit = { - val listener4 = new NotificationListener() { // error: duplicate symbol error - def handleNotification(n: Notification|Null, emitter: Object): Unit = { // error - } - } - } - -} diff --git a/tests/explicit-nulls/neg/type-arg.scala b/tests/explicit-nulls/neg/type-arg.scala index c145ce562e6e..0bde5380bec9 100644 --- a/tests/explicit-nulls/neg/type-arg.scala +++ b/tests/explicit-nulls/neg/type-arg.scala @@ -1,12 +1,12 @@ - // Test that reference types being non-nullable // is checked when lower bound of a type argument // is Null. + object Test { type Untyped = Null class TreeInstances[T >: Untyped] class Type - + object untpd extends TreeInstances[Null] // There are two errors reported for the line below (don't know why). object tpd extends TreeInstances[Type] // error // error diff --git a/tests/explicit-nulls/neg/unsafe-cast.scala b/tests/explicit-nulls/neg/unsafe-cast.scala new file mode 100644 index 000000000000..8c23c5d2b414 --- /dev/null +++ b/tests/explicit-nulls/neg/unsafe-cast.scala @@ -0,0 +1,74 @@ +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(null)) // error + n1(Array(null)) + n2(Array(null)) + + n0(Array("a", null)) // error + n1(Array("a", null)) + n2(Array("a", null)) + } + + def test[T <: AnyRef](x: T | Null): T = { + val y: T = x // error + val z: T = null // error + x // error + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/neg/unsafe-chain.scala b/tests/explicit-nulls/neg/unsafe-chain.scala new file mode 100644 index 000000000000..f087e22d50f9 --- /dev/null +++ b/tests/explicit-nulls/neg/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/neg/unsafe-eq-null.scala b/tests/explicit-nulls/neg/unsafe-eq-null.scala new file mode 100644 index 000000000000..493aaebfbff2 --- /dev/null +++ b/tests/explicit-nulls/neg/unsafe-eq-null.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/neg/unsafe-extensions.scala b/tests/explicit-nulls/neg/unsafe-extensions.scala new file mode 100644 index 000000000000..cc21a0f0a0a8 --- /dev/null +++ b/tests/explicit-nulls/neg/unsafe-extensions.scala @@ -0,0 +1,18 @@ +class Extensions { + def (s: String).ext(ss: String): String = s + ss + + def f = { + val x: String | Null = ??? + val y: String = ??? + + x.ext(y) // error + x.ext(x) // error + y.ext(x) // error + y.ext(y) + } + + // i7828 + def g = { + val x = "hello, world!".split(" ").map(_.length) // error + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/neg/unsafe-implicit.scala b/tests/explicit-nulls/neg/unsafe-implicit.scala new file mode 100644 index 000000000000..b00f99240562 --- /dev/null +++ b/tests/explicit-nulls/neg/unsafe-implicit.scala @@ -0,0 +1,107 @@ +class S { + 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 + } + + locally { + implicit val x: String | Null = ??? + + val y1: String = summon // error + val y2: String | Null = summon + } + + def test3(implicit x: String | Null) = { + val y1: String = summon // error + val y2: String | Null = summon + } + + def test4(using String | Null) = { + val y1: String = summon // error + val y2: String | Null = summon + } + + import scala.language.implicitConversions + + locally { + implicit def f(x: String): Array[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 + } + + locally { + given Conversion[String, Array[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 + } + + 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 <: AnyRef] = { + given Conversion[T, Array[T]] = _ => ??? + + val y1: T = ??? + val y2: T | Null = ??? + + val z1: Array[T] = y1 + val z2: Array[T | Null] = y1 // error + val z3: Array[T] | Null = y1 + val z4: Array[T | Null] | Null = y1 // error + + val z5: Array[T] = y2 // error + val z6: Array[T | Null] = y2 // error + val z7: Array[T] | Null = y2 // error + val z8: Array[T | Null] | Null = y2 // error + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/interop-javanull-src/J.java b/tests/explicit-nulls/neg/unsafe-java-chain/J.java similarity index 60% rename from tests/explicit-nulls/pos/interop-javanull-src/J.java rename to tests/explicit-nulls/neg/unsafe-java-chain/J.java index a85afa17c859..bd266bae13d9 100644 --- a/tests/explicit-nulls/pos/interop-javanull-src/J.java +++ b/tests/explicit-nulls/neg/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/neg/unsafe-java-chain/S.scala b/tests/explicit-nulls/neg/unsafe-java-chain/S.scala new file mode 100644 index 000000000000..9fe5aa3f08ce --- /dev/null +++ b/tests/explicit-nulls/neg/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/neg/unsafe-java-varargs-src/J.java b/tests/explicit-nulls/neg/unsafe-java-varargs-src/J.java new file mode 100644 index 000000000000..21ba08be66c9 --- /dev/null +++ b/tests/explicit-nulls/neg/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/neg/unsafe-java-varargs-src/S.scala b/tests/explicit-nulls/neg/unsafe-java-varargs-src/S.scala new file mode 100644 index 000000000000..e27b0dcaacbf --- /dev/null +++ b/tests/explicit-nulls/neg/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/neg/unsafe-java-varargs.scala b/tests/explicit-nulls/neg/unsafe-java-varargs.scala new file mode 100644 index 000000000000..0edcfcbf3dba --- /dev/null +++ b/tests/explicit-nulls/neg/unsafe-java-varargs.scala @@ -0,0 +1,13 @@ +import java.nio.file.Paths + +class S { + val arg1: Array[String] = ??? + val arg2: Array[String | Null] = ??? + val arg3: Array[String] | Null = ??? + val arg4: Array[String | Null] | Null = ??? + + Paths.get("", arg1: _*) + Paths.get("", arg2: _*) + Paths.get("", arg3: _*) // error + Paths.get("", arg4: _*) // error +} \ No newline at end of file diff --git a/tests/explicit-nulls/neg/unsafe-overload.scala b/tests/explicit-nulls/neg/unsafe-overload.scala new file mode 100644 index 000000000000..e7e551f1bda1 --- /dev/null +++ b/tests/explicit-nulls/neg/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/neg/unsafe-path.scala b/tests/explicit-nulls/neg/unsafe-path.scala new file mode 100644 index 000000000000..52f4e3c0bf21 --- /dev/null +++ b/tests/explicit-nulls/neg/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/neg/unsafe-select.scala b/tests/explicit-nulls/neg/unsafe-select.scala new file mode 100644 index 000000000000..e90b63b1bbe5 --- /dev/null +++ b/tests/explicit-nulls/neg/unsafe-select.scala @@ -0,0 +1,29 @@ +class C { + var x: String = "" + var y: String | Null = null + var child: C | Null = null +} + +class S { + val c: C = new 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 + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos-separate/notnull/S_3.scala b/tests/explicit-nulls/pos-separate/notnull/S_3.scala index cc55ef54e7e2..49964b369d87 100644 --- a/tests/explicit-nulls/pos-separate/notnull/S_3.scala +++ b/tests/explicit-nulls/pos-separate/notnull/S_3.scala @@ -10,6 +10,6 @@ class S_3 { def ff(i: Int): String = J_2.f(i) def gg(i: Int): String = J_2.g(i) def hh(i: Int): String = (new J_2).h(i) - def genericff(a: String | Null): Array[String | UncheckedNull] = (new J_2).genericf(a) + def genericff(a: String | Null): Array[String | Null] = (new J_2).genericf(a) def genericgg(a: String | Null): java.util.List[String] = (new J_2).genericg(a) } diff --git a/tests/explicit-nulls/pos/dont-widen-src/S.scala b/tests/explicit-nulls/pos/dont-widen-src/S.scala index 0fbca30fac0a..5414e418e79d 100644 --- a/tests/explicit-nulls/pos/dont-widen-src/S.scala +++ b/tests/explicit-nulls/pos/dont-widen-src/S.scala @@ -3,5 +3,5 @@ class S { val x = j.foo() // Check that the type of `x` is inferred to be `String|Null`. // i.e. the union isn't collapsed. - val y: String|Null = x + val y: String | Null = x } diff --git a/tests/explicit-nulls/pos/flow.scala b/tests/explicit-nulls/pos/flow-basic.scala similarity index 100% rename from tests/explicit-nulls/pos/flow.scala rename to tests/explicit-nulls/pos/flow-basic.scala diff --git a/tests/explicit-nulls/pos/flow2.scala b/tests/explicit-nulls/pos/flow-condition.scala similarity index 100% rename from tests/explicit-nulls/pos/flow2.scala rename to tests/explicit-nulls/pos/flow-condition.scala diff --git a/tests/explicit-nulls/pos/flow4.scala b/tests/explicit-nulls/pos/flow-inline.scala similarity index 99% rename from tests/explicit-nulls/pos/flow4.scala rename to tests/explicit-nulls/pos/flow-inline.scala index 994b76065d8c..e64258ba988f 100644 --- a/tests/explicit-nulls/pos/flow4.scala +++ b/tests/explicit-nulls/pos/flow-inline.scala @@ -2,6 +2,7 @@ // and it tests that we can use an inline method to "abstract" a more complicated // isInstanceOf check, while at the same time getting the flow inference to know // that `isRedTree(tree) => tree ne null`. + class TreeOps { abstract class Tree[A, B](val key: A, val value: B) class RedTree[A, B](override val key: A, override val value: B) extends Tree[A, B](key, value) diff --git a/tests/explicit-nulls/pos/match.scala b/tests/explicit-nulls/pos/flow-match.scala similarity index 71% rename from tests/explicit-nulls/pos/match.scala rename to tests/explicit-nulls/pos/flow-match.scala index 0e65b9584328..9e3806c97363 100644 --- a/tests/explicit-nulls/pos/match.scala +++ b/tests/explicit-nulls/pos/flow-match.scala @@ -1,4 +1,4 @@ -// Test NotNullInfo from non-null cases +// Test flow-typing when NotNullInfos are from non-null cases object MatchTest { locally { diff --git a/tests/explicit-nulls/pos/stable-path.scala b/tests/explicit-nulls/pos/flow-stable-path.scala similarity index 100% rename from tests/explicit-nulls/pos/stable-path.scala rename to tests/explicit-nulls/pos/flow-stable-path.scala diff --git a/tests/explicit-nulls/pos/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/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 8f2e48d287a3..5938ecc44337 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-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/option-transform.scala b/tests/explicit-nulls/pos/option-transform.scala new file mode 100644 index 000000000000..f9d0d6f898b1 --- /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 + */ + def[T <: AnyRef] (x: T | Null) 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/unsafe-cast.scala b/tests/explicit-nulls/pos/unsafe-cast.scala new file mode 100644 index 000000000000..adf63432ecc2 --- /dev/null +++ b/tests/explicit-nulls/pos/unsafe-cast.scala @@ -0,0 +1,76 @@ +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 + + import scala.language.unsafeNulls + + def test1 = { + val s: String = ??? + + val a1: String | Null = s // safe + val a2: String = a1 // String | Null to String + + val b1 = s.trim() // String | Null + val b2 = b1.trim() + val b3 = b2.length() + + val c1: String | Null = null // safe + val c2: String = null // Null to String + val c3: Int | String = null + + val d1: Array[String | Null] | Null = Array(s) // Array[String] to Array[String | Null] + val d2: Array[String] = d1 + val d3: Array[String | Null] = d2 + val d4: Array[String] = Array(null) + } + + def test2 = { + m0("") + m0(null) + + val a: String | Null = ??? + val b: String = m0(a) + val c: String = m1(a).trim() + + val x: Array[String | Null] | Null = ??? + val y: Array[String] = x + val z: Array[String | Null] = y + + n0(x) + n0(y) + n0(z) + + n1(x) + n1(y) + n1(z) + + n2(x) + n2(y) + n2(z) + + n0(Array("a", "b")) + n1(Array("a", "b")) + n2(Array("a", "b")) + + n0(Array(null)) + n1(Array(null)) + n2(Array(null)) + + n0(Array("a", null)) + n1(Array("a", null)) + n2(Array("a", null)) + } + + def test[T <: AnyRef](x: T | Null): T = { + val y: T = x + val z: T = null + x + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/interop-javanull.scala b/tests/explicit-nulls/pos/unsafe-chain.scala similarity index 51% rename from tests/explicit-nulls/pos/interop-javanull.scala rename to tests/explicit-nulls/pos/unsafe-chain.scala index d771b96e5507..6557f8c3352c 100644 --- a/tests/explicit-nulls/pos/interop-javanull.scala +++ b/tests/explicit-nulls/pos/unsafe-chain.scala @@ -1,10 +1,11 @@ +// Test that we can select through "| Null" is unsafeNulls is enabled (unsoundly). -// 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). + import scala.language.unsafeNulls + 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/unsafe-eq-null.scala b/tests/explicit-nulls/pos/unsafe-eq-null.scala new file mode 100644 index 000000000000..0dcc8f0f604c --- /dev/null +++ b/tests/explicit-nulls/pos/unsafe-eq-null.scala @@ -0,0 +1,51 @@ +class S { + import scala.language.unsafeNulls + + val s1: String | Null = ??? + val s2: String = ??? + val n: Null = ??? + val ss1: Array[String] = ??? + val ss2: Array[String | Null] = ??? + + locally { + s1 == null // safe + s1 != null // safe + null == s1 + null != s1 + + s2 == null + s2 != null + null == s2 + null != s2 + + s1 == s2 // safe + s1 != s2 // safe + s2 == s1 + s2 != s1 + + n == null + n != null + null == n + null != n + + s1 == n // safe + s2 == n + n != s1 + n != s2 + } + + locally { + ss1 == null + ss1 != null + null == ss1 + null != ss1 + + ss1 == n + ss1 != n + n == ss1 + n != ss1 + + ss1 == ss2 + ss2 != ss1 + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/unsafe-extensions.scala b/tests/explicit-nulls/pos/unsafe-extensions.scala new file mode 100644 index 000000000000..1658f28cfc86 --- /dev/null +++ b/tests/explicit-nulls/pos/unsafe-extensions.scala @@ -0,0 +1,20 @@ +import scala.language.unsafeNulls + +class Extensions { + def (s: String).ext(ss: String): String = s + ss + + def f = { + val x: String | Null = ??? + val y: String = ??? + + x.ext(y) + x.ext(x) + y.ext(x) + y.ext(y) + } + + // i7828 + def g = { + val x = "hello, world!".split(" ").map(_.length) + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/unsafe-implicit.scala b/tests/explicit-nulls/pos/unsafe-implicit.scala new file mode 100644 index 000000000000..2a63f99eb35b --- /dev/null +++ b/tests/explicit-nulls/pos/unsafe-implicit.scala @@ -0,0 +1,109 @@ +class S { + import scala.language.unsafeNulls + + 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 + } + + locally { + implicit val x: String | Null = ??? + + val y1: String = summon + val y2: String | Null = summon + } + + def test3(implicit x: String | Null) = { + val y1: String = summon + val y2: String | Null = summon + } + + def test4(using String | Null) = { + val y1: String = summon + val y2: String | Null = summon + } + + import scala.language.implicitConversions + + locally { + implicit def f(x: String): Array[String] = ??? + + val y1: String = ??? + val y2: String | Null = ??? + + val z1: Array[String] = y1 + val z2: Array[String | Null] = y1 + val z3: Array[String] | Null = y1 + val z4: Array[String | Null] | Null = y1 + + val z5: Array[String] = y2 + val z6: Array[String | Null] = y2 + val z7: Array[String] | Null = y2 + val z8: Array[String | Null] | Null = y2 + } + + locally { + given Conversion[String, Array[String]] = _ => ??? + + val y1: String = ??? + val y2: String | Null = ??? + + val z1: Array[String] = y1 + val z2: Array[String | Null] = y1 + val z3: Array[String] | Null = y1 + val z4: Array[String | Null] | Null = y1 + + val z5: Array[String] = y2 + val z6: Array[String | Null] = y2 + val z7: Array[String] | Null = y2 + val z8: Array[String | Null] | Null = y2 + } + + 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 + val z3: Array[String] | Null = y1 + val z4: Array[String | Null] | Null = y1 + + val z5: Array[String] = y2 + val z6: Array[String | Null] = y2 + val z7: Array[String] | Null = y2 + val z8: Array[String | Null] | Null = y2 + } + + def test5[T <: AnyRef] = { + 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/unsafe-java-chain/J.java b/tests/explicit-nulls/pos/unsafe-java-chain/J.java new file mode 100644 index 000000000000..bd266bae13d9 --- /dev/null +++ b/tests/explicit-nulls/pos/unsafe-java-chain/J.java @@ -0,0 +1,7 @@ +class J1 { + J2 getJ2() { return new J2(); } +} + +class J2 { + J1 getJ1() { return new J1(); } +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/interop-javanull-src/S.scala b/tests/explicit-nulls/pos/unsafe-java-chain/S.scala similarity index 66% rename from tests/explicit-nulls/pos/interop-javanull-src/S.scala rename to tests/explicit-nulls/pos/unsafe-java-chain/S.scala index 42693066bf14..6eba711a1ae3 100644 --- a/tests/explicit-nulls/pos/interop-javanull-src/S.scala +++ b/tests/explicit-nulls/pos/unsafe-java-chain/S.scala @@ -1,5 +1,5 @@ +import scala.language.unsafeNulls -// 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/unsafe-java-varargs-src/J.java b/tests/explicit-nulls/pos/unsafe-java-varargs-src/J.java new file mode 100644 index 000000000000..21ba08be66c9 --- /dev/null +++ b/tests/explicit-nulls/pos/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/pos/unsafe-java-varargs-src/S.scala b/tests/explicit-nulls/pos/unsafe-java-varargs-src/S.scala new file mode 100644 index 000000000000..b5242e865c2b --- /dev/null +++ b/tests/explicit-nulls/pos/unsafe-java-varargs-src/S.scala @@ -0,0 +1,21 @@ +import scala.language.unsafeNulls + +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: _*) + j.foo(arg4: _*) +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/unsafe-java-varargs.scala b/tests/explicit-nulls/pos/unsafe-java-varargs.scala new file mode 100644 index 000000000000..60eddb0cb25a --- /dev/null +++ b/tests/explicit-nulls/pos/unsafe-java-varargs.scala @@ -0,0 +1,15 @@ +import java.nio.file.Paths + +import scala.language.unsafeNulls + +class S { + val arg1: Array[String] = ??? + val arg2: Array[String | Null] = ??? + val arg3: Array[String] | Null = ??? + val arg4: Array[String | Null] | Null = ??? + + Paths.get("", arg1: _*) + Paths.get("", arg2: _*) + Paths.get("", arg3: _*) + Paths.get("", arg4: _*) +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/unsafe-overload.scala b/tests/explicit-nulls/pos/unsafe-overload.scala new file mode 100644 index 000000000000..ded8ff9d7f9e --- /dev/null +++ b/tests/explicit-nulls/pos/unsafe-overload.scala @@ -0,0 +1,61 @@ +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 = ??? + } + + import scala.language.unsafeNulls + + 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) + h1(o.f) + + h2(f2) + h2(o.f) + } + + 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) + h1(o.g) + + h2(g2) + h2(o.g) + } + + locally { + def f1(x: String | Null): String | Null = ??? + def f2(x: Array[String | Null]): Array[String | Null] = ??? + + o.h(f1) + o.h(f2) + } + + locally { + def g1(x: String): String = ??? + def g2(x: Array[String]): Array[String] = ??? + + o.i(g1) + o.i(g2) + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/unsafe-path.scala b/tests/explicit-nulls/pos/unsafe-path.scala new file mode 100644 index 000000000000..5179faaae101 --- /dev/null +++ b/tests/explicit-nulls/pos/unsafe-path.scala @@ -0,0 +1,21 @@ +class S { + class O { + type I = Int + val a: I = 1 + + type S = String | Null + val s: S = "" + } + + locally { + import scala.language.unsafeNulls + + val m: O | Null = new O + val n: m.I = m.a + + val s1: m.S = m.s + val s2: m.S | Null = m.s + val ss1: String = s1 + val ss2: String = s2 + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/unsafe-select.scala b/tests/explicit-nulls/pos/unsafe-select.scala new file mode 100644 index 000000000000..58315619ded9 --- /dev/null +++ b/tests/explicit-nulls/pos/unsafe-select.scala @@ -0,0 +1,31 @@ +class C { + var x: String = "" + var y: String | Null = null + var child: C | Null = null +} + +class S { + import scala.language.unsafeNulls + + val c: C | Null = new C + + def test1 = { + val x1: String = c.x + val x2: String | Null = c.x + val y1: String = c.y + val y2: String | Null = c.y + val c1: C = c.child + val c2: C | Null = c.child + + val yy: String = c.child.child.y + } + + def test2 = { + c.x = "" + c.x = null + c.y = "" + c.y = null + c.child = c + c.child = null + } +} \ 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/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..5400a2546b1a --- /dev/null +++ b/tests/explicit-nulls/run/unsafe-nulls.scala @@ -0,0 +1,21 @@ +// 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. + +import scala.language.unsafeNulls + +object Test { + def main(args: Array[String]): Unit = { + val s: String | Null = "hello" + assert(s.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. + } + } +} 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