diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 2e48ca78258f..fc7e61c8ec71 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -417,6 +417,7 @@ private sealed trait YSettings: // Experimental language features val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-kind-polymorphism", "Disable kind polymorphism.") val YexplicitNulls: Setting[Boolean] = BooleanSetting(ForkSetting, "Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.") + val YnoFlexibleTypes: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-flexible-types", "Disable turning nullable Java return types and parameter types into flexible types, which behave like abstract types with a nullable lower bound and non-nullable upper bound.") val YcheckInit: Setting[Boolean] = BooleanSetting(ForkSetting, "Ysafe-init", "Ensure safe initialization of objects.") val YcheckInitGlobal: Setting[Boolean] = BooleanSetting(ForkSetting, "Ysafe-init-global", "Check safe initialization of global objects.") val YrequireTargetName: Setting[Boolean] = BooleanSetting(ForkSetting, "Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation.") diff --git a/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala b/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala index 1870956357d6..109929f0c6f5 100644 --- a/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala +++ b/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala @@ -696,9 +696,11 @@ trait ConstraintHandling { tp.rebind(tp.parent.hardenUnions) case tp: HKTypeLambda => tp.derivedLambdaType(resType = tp.resType.hardenUnions) + case tp: FlexibleType => + tp.derivedFlexibleType(tp.hi.hardenUnions) case tp: OrType => - val tp1 = tp.stripNull - if tp1 ne tp then tp.derivedOrType(tp1.hardenUnions, defn.NullType) + val tp1 = tp.stripNull(stripFlexibleTypes = false) + if tp1 ne tp then tp.derivedOrType(tp1.hardenUnions, defn.NullType, soft = false) else tp.derivedOrType(tp.tp1.hardenUnions, tp.tp2.hardenUnions, soft = false) case _ => tp diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index ae21c6fb8763..73fea84a640b 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -472,6 +472,9 @@ object Contexts { /** Is the explicit nulls option set? */ def explicitNulls: Boolean = base.settings.YexplicitNulls.value + /** Is the flexible types option set? */ + def flexibleTypes: Boolean = base.settings.YexplicitNulls.value && !base.settings.YnoFlexibleTypes.value + /** A fresh clone of this context embedded in this context. */ def fresh: FreshContext = freshOver(this) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index f187498da1fb..932a7d72d33e 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -648,7 +648,7 @@ class Definitions { @tu lazy val StringModule: Symbol = StringClass.linkedClass @tu lazy val String_+ : TermSymbol = enterMethod(StringClass, nme.raw.PLUS, methOfAny(StringType), Final) @tu lazy val String_valueOf_Object: Symbol = StringModule.info.member(nme.valueOf).suchThat(_.info.firstParamTypes match { - case List(pt) => pt.isAny || pt.stripNull.isAnyRef + case List(pt) => pt.isAny || pt.stripNull().isAnyRef case _ => false }).symbol @@ -660,13 +660,13 @@ class Definitions { @tu lazy val ClassCastExceptionClass: ClassSymbol = requiredClass("java.lang.ClassCastException") @tu lazy val ClassCastExceptionClass_stringConstructor: TermSymbol = ClassCastExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match { case List(pt) => - pt.stripNull.isRef(StringClass) + pt.stripNull().isRef(StringClass) case _ => false }).symbol.asTerm @tu lazy val ArithmeticExceptionClass: ClassSymbol = requiredClass("java.lang.ArithmeticException") @tu lazy val ArithmeticExceptionClass_stringConstructor: TermSymbol = ArithmeticExceptionClass.info.member(nme.CONSTRUCTOR).suchThat(_.info.firstParamTypes match { case List(pt) => - pt.stripNull.isRef(StringClass) + pt.stripNull().isRef(StringClass) case _ => false }).symbol.asTerm diff --git a/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala b/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala index 6244923cfb52..46ce0d2d7852 100644 --- a/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala @@ -78,11 +78,11 @@ object JavaNullInterop { * but the result type is not nullable. */ private def nullifyExceptReturnType(tp: Type)(using Context): Type = - new JavaNullMap(true)(tp) + new JavaNullMap(outermostLevelAlreadyNullable = true)(tp) /** Nullifies a Java type by adding `| Null` in the relevant places. */ private def nullifyType(tp: Type)(using Context): Type = - new JavaNullMap(false)(tp) + new JavaNullMap(outermostLevelAlreadyNullable = false)(tp) /** 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. @@ -96,25 +96,29 @@ object JavaNullInterop { * to `(A & B) | Null`, instead of `(A | Null & B | Null) | Null`. */ private class JavaNullMap(var outermostLevelAlreadyNullable: Boolean)(using Context) extends TypeMap { + def nullify(tp: Type): Type = if ctx.flexibleTypes then FlexibleType(tp) else OrNull(tp) + /** Should we nullify `tp` at the outermost level? */ def needsNull(tp: Type): Boolean = - !outermostLevelAlreadyNullable && (tp match { - case tp: TypeRef => + if outermostLevelAlreadyNullable then false + else tp match + case tp: TypeRef if // We don't modify value types because they're non-nullable even in Java. - !tp.symbol.isValueClass && + tp.symbol.isValueClass + // We don't modify unit types. + || tp.isRef(defn.UnitClass) // We don't modify `Any` because it's already nullable. - !tp.isRef(defn.AnyClass) && + || 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|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) + || !ctx.flexibleTypes && tp.isRef(defn.RepeatedParamClass) => false case _ => true - }) override def apply(tp: Type): Type = tp match { - case tp: TypeRef if needsNull(tp) => OrNull(tp) + case tp: TypeRef if needsNull(tp) => nullify(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. @@ -124,7 +128,7 @@ object JavaNullInterop { val targs2 = targs map this outermostLevelAlreadyNullable = oldOutermostNullable val appTp2 = derivedAppliedType(appTp, tycon, targs2) - if needsNull(tycon) then OrNull(appTp2) else appTp2 + if needsNull(tycon) then nullify(appTp2) else appTp2 case ptp: PolyType => derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType)) case mtp: MethodType => @@ -138,12 +142,12 @@ object JavaNullInterop { // 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 - OrNull(derivedAndType(tp, this(tp.tp1), this(tp.tp2))) - case tp: TypeParamRef if needsNull(tp) => OrNull(tp) + nullify(derivedAndType(tp, this(tp.tp1), this(tp.tp2))) + case tp: TypeParamRef if needsNull(tp) => nullify(tp) // In all other cases, return the type unchanged. // In particular, if the type is a ConstantType, then we don't nullify it because it is the // type of a final non-nullable field. case _ => tp } } -} +} \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala index 4f22f9d31e36..291498dbc558 100644 --- a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala +++ b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala @@ -14,7 +14,7 @@ object NullOpsDecorator: * If this type isn't (syntactically) nullable, then returns the type unchanged. * The type will not be changed if explicit-nulls is not enabled. */ - def stripNull(using Context): Type = { + def stripNull(stripFlexibleTypes: Boolean = true)(using Context): Type = { def strip(tp: Type): Type = val tpWiden = tp.widenDealias val tpStripped = tpWiden match { @@ -33,6 +33,9 @@ object NullOpsDecorator: if (tp1s ne tp1) && (tp2s ne tp2) then tp.derivedAndType(tp1s, tp2s) else tp + case tp: FlexibleType => + val hi1 = strip(tp.hi) + if stripFlexibleTypes then hi1 else tp.derivedFlexibleType(hi1) case tp @ TypeBounds(lo, hi) => tp.derivedTypeBounds(strip(lo), strip(hi)) case tp => tp @@ -44,7 +47,7 @@ object NullOpsDecorator: /** Is self (after widening and dealiasing) a type of the form `T | Null`? */ def isNullableUnion(using Context): Boolean = { - val stripped = self.stripNull + val stripped = self.stripNull() stripped ne self } end extension diff --git a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala index e11ac26ef93c..dd2319ed508b 100644 --- a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala @@ -562,11 +562,11 @@ class OrderingConstraint(private val boundsMap: ParamBounds, val underlying1 = recur(tp.underlying) if underlying1 ne tp.underlying then underlying1 else tp case CapturingType(parent, refs) => - val parent1 = recur(parent) - if parent1 ne parent then tp.derivedCapturingType(parent1, refs) else tp + tp.derivedCapturingType(recur(parent), refs) + case tp: FlexibleType => + tp.derivedFlexibleType(recur(tp.hi)) case tp: AnnotatedType => - val parent1 = recur(tp.parent) - if parent1 ne tp.parent then tp.derivedAnnotatedType(parent1, tp.annot) else tp + tp.derivedAnnotatedType(recur(tp.parent), tp.annot) case _ => val tp1 = tp.dealiasKeepAnnots if tp1 ne tp then diff --git a/compiler/src/dotty/tools/dotc/core/PatternTypeConstrainer.scala b/compiler/src/dotty/tools/dotc/core/PatternTypeConstrainer.scala index 7942bbaa3d45..6d6a47cf6a1e 100644 --- a/compiler/src/dotty/tools/dotc/core/PatternTypeConstrainer.scala +++ b/compiler/src/dotty/tools/dotc/core/PatternTypeConstrainer.scala @@ -163,7 +163,7 @@ trait PatternTypeConstrainer { self: TypeComparer => } } - def dealiasDropNonmoduleRefs(tp: Type) = tp.dealias match { + def dealiasDropNonmoduleRefs(tp: Type): Type = tp.dealias match { case tp: TermRef => // we drop TermRefs that don't have a class symbol, as they can't // meaningfully participate in GADT reasoning and just get in the way. @@ -172,6 +172,7 @@ trait PatternTypeConstrainer { self: TypeComparer => // additional trait - argument-less enum cases desugar to vals. // See run/enum-Tree.scala. if tp.classSymbol.exists then tp else tp.info + case tp: FlexibleType => dealiasDropNonmoduleRefs(tp.underlying) case tp => tp } diff --git a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala index efcad3307937..54636ff4ad58 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala @@ -541,6 +541,7 @@ class TypeApplications(val self: Type) extends AnyVal { */ final def argInfos(using Context): List[Type] = self.stripped match case AppliedType(tycon, args) => args + case tp: FlexibleType => tp.underlying.argInfos case _ => Nil /** If this is an encoding of a function type, return its arguments, otherwise return Nil. diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 8b6e099bfe41..a9b5a39c2a62 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -864,6 +864,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling false } compareClassInfo + case tp2: FlexibleType => + recur(tp1, tp2.lo) case _ => fourthTry } @@ -1059,6 +1061,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp1: ExprType if ctx.phaseId > gettersPhase.id => // getters might have converted T to => T, need to compensate. recur(tp1.widenExpr, tp2) + case tp1: FlexibleType => + recur(tp1.hi, tp2) case _ => false } @@ -3437,6 +3441,8 @@ class MatchReducer(initctx: Context) extends TypeComparer(initctx) { isConcrete(tp1.underlying) case tp1: AndOrType => isConcrete(tp1.tp1) && isConcrete(tp1.tp2) + case tp1: FlexibleType => + isConcrete(tp1.hi) case _ => val tp2 = tp1.stripped.stripLazyRef (tp2 ne tp) && isConcrete(tp2) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 029bd97fa3c1..3c9f7e05b6e2 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -307,6 +307,7 @@ object Types extends TypeUtils { isRef(defn.ObjectClass) && (typeSymbol eq defn.FromJavaObjectSymbol) def containsFromJavaObject(using Context): Boolean = this match + case tp: FlexibleType => tp.underlying.containsFromJavaObject case tp: OrType => tp.tp1.containsFromJavaObject || tp.tp2.containsFromJavaObject case tp: AndType => tp.tp1.containsFromJavaObject && tp.tp2.containsFromJavaObject case _ => isFromJavaObject @@ -345,6 +346,7 @@ object Types extends TypeUtils { /** Is this type guaranteed not to have `null` as a value? */ final def isNotNull(using Context): Boolean = this match { case tp: ConstantType => tp.value.value != null + case tp: FlexibleType => false case tp: ClassInfo => !tp.cls.isNullableClass && tp.cls != defn.NothingClass case tp: AppliedType => tp.superType.isNotNull case tp: TypeBounds => tp.lo.isNotNull @@ -374,6 +376,7 @@ object Types extends TypeUtils { case AppliedType(tycon, args) => tycon.unusableForInference || args.exists(_.unusableForInference) case RefinedType(parent, _, rinfo) => parent.unusableForInference || rinfo.unusableForInference case TypeBounds(lo, hi) => lo.unusableForInference || hi.unusableForInference + case tp: FlexibleType => tp.underlying.unusableForInference case tp: AndOrType => tp.tp1.unusableForInference || tp.tp2.unusableForInference case tp: LambdaType => tp.resultType.unusableForInference || tp.paramInfos.exists(_.unusableForInference) case WildcardType(optBounds) => optBounds.unusableForInference @@ -930,7 +933,7 @@ object Types extends TypeUtils { // Selecting `name` from a type `T | Null` is like selecting `name` from `T`, if // unsafeNulls is enabled and T is a subtype of AnyRef. // This can throw at runtime, but we trade soundness for usability. - tp1.findMember(name, pre.stripNull, required, excluded) + tp1.findMember(name, pre.stripNull(), required, excluded) case _ => searchAfterJoin else searchAfterJoin @@ -1354,13 +1357,13 @@ object Types extends TypeUtils { * then the top-level union isn't widened. This is needed so that type inference can infer nullable types. */ def widenUnion(using Context): Type = widen match - case tp: OrType => tp match - case OrNull(tp1) => - // Don't widen `T|Null`, since otherwise we wouldn't be able to infer nullable unions. + case tp: OrType => + val tp1 = tp.stripNull(stripFlexibleTypes = false) + if tp1 ne tp then val tp1Widen = tp1.widenUnionWithoutNull - if (tp1Widen.isRef(defn.AnyClass)) tp1Widen + if tp1Widen.isRef(defn.AnyClass) then tp1Widen else tp.derivedOrType(tp1Widen, defn.NullType) - case _ => + else tp.widenUnionWithoutNull case tp => tp.widenUnionWithoutNull @@ -1375,6 +1378,8 @@ object Types extends TypeUtils { tp.rebind(tp.parent.widenUnion) case tp: HKTypeLambda => tp.derivedLambdaType(resType = tp.resType.widenUnion) + case tp: FlexibleType => + tp.derivedFlexibleType(tp.hi.widenUnionWithoutNull) case tp => tp @@ -3453,6 +3458,50 @@ object Types extends TypeUtils { } } + // --- FlexibleType ----------------------------------------------------------------- + + /* A flexible type is a type with a custom subtyping relationship. + * It is used by explicit nulls to represent a type coming from Java which can be + * considered as nullable or non-nullable depending on the context, in a similar way to Platform + * Types in Kotlin. A `FlexibleType(T)` generally behaves like a type variable with special bounds + * `T | Null .. T`, so that `T | Null <: FlexibleType(T) <: T`. + * A flexible type will be erased to its original type `T`. + */ + case class FlexibleType(lo: Type, hi: Type) extends CachedProxyType with ValueType { + + override def underlying(using Context): Type = hi + + def derivedFlexibleType(hi: Type)(using Context): Type = + if hi eq this.hi then this else FlexibleType(hi) + + override def computeHash(bs: Binders): Int = doHash(bs, hi) + + override final def baseClasses(using Context): List[ClassSymbol] = hi.baseClasses + } + + object FlexibleType { + def apply(tp: Type)(using Context): Type = tp match { + case ft: FlexibleType => ft + case _ => + // val tp1 = tp.stripNull() + // if tp1.isNullType then + // // (Null)? =:= ? >: Null <: (Object & Null) + // FlexibleType(tp, AndType(defn.ObjectType, defn.NullType)) + // else + // // (T | Null)? =:= ? >: T | Null <: T + // // (T)? =:= ? >: T | Null <: T + // val hi = tp1 + // val lo = if hi eq tp then OrNull(hi) else tp + // FlexibleType(lo, hi) + // + // The commented out code does more work to analyze the original type to ensure the + // flexible type is always a subtype of the original type and the Object type. + // It is not necessary according to the use cases, so we choose to use a simpler + // rule. + FlexibleType(OrNull(tp), tp) + } + } + // --- AndType/OrType --------------------------------------------------------------- abstract class AndOrType extends CachedGroundType with ValueType { @@ -3707,7 +3756,8 @@ object Types extends TypeUtils { assert(!ctx.isAfterTyper, s"$tp in $where") // we check correct kinds at PostTyper throw TypeError(em"$tp is not a value type, cannot be used $where") - /** An extractor object to pattern match against a nullable union. + /** An extractor object to pattern match against a nullable union + * (including flexible types). * e.g. * * (tp: Type) match @@ -3718,7 +3768,7 @@ object Types extends TypeUtils { def apply(tp: Type)(using Context) = if tp.isNullType then tp else OrType(tp, defn.NullType, soft = false) def unapply(tp: Type)(using Context): Option[Type] = - val tp1 = tp.stripNull + val tp1 = tp.stripNull() if tp1 ne tp then Some(tp1) else None } @@ -5962,6 +6012,8 @@ object Types extends TypeUtils { samClass(tp.underlying) case tp: AnnotatedType => samClass(tp.underlying) + case tp: FlexibleType => + samClass(tp.underlying) case _ => NoSymbol @@ -6092,6 +6144,8 @@ object Types extends TypeUtils { tp.derivedJavaArrayType(elemtp) protected def derivedExprType(tp: ExprType, restpe: Type): Type = tp.derivedExprType(restpe) + protected def derivedFlexibleType(tp: FlexibleType, hi: Type): Type = + tp.derivedFlexibleType(hi) // note: currying needed because Scala2 does not support param-dependencies protected def derivedLambdaType(tp: LambdaType)(formals: List[tp.PInfo], restpe: Type): Type = tp.derivedLambdaType(tp.paramNames, formals, restpe) @@ -6215,6 +6269,9 @@ object Types extends TypeUtils { case tp: OrType => derivedOrType(tp, this(tp.tp1), this(tp.tp2)) + case tp: FlexibleType => + derivedFlexibleType(tp, this(tp.hi)) + case tp: MatchType => val bound1 = this(tp.bound) val scrut1 = atVariance(0)(this(tp.scrutinee)) @@ -6502,6 +6559,17 @@ object Types extends TypeUtils { if (underlying.isExactlyNothing) underlying else tp.derivedAnnotatedType(underlying, annot) } + + override protected def derivedFlexibleType(tp: FlexibleType, hi: Type): Type = + hi match { + case Range(lo, hi) => + // We know FlexibleType(t).hi = t and FlexibleType(t).lo = OrNull(t) + range(OrNull(lo), hi) + case _ => + if (hi.isExactlyNothing) hi + else tp.derivedFlexibleType(hi) + } + override protected def derivedCapturingType(tp: Type, parent: Type, refs: CaptureSet): Type = parent match // TODO ^^^ handle ranges in capture sets as well case Range(lo, hi) => @@ -6631,6 +6699,9 @@ object Types extends TypeUtils { case tp: TypeVar => this(x, tp.underlying) + case tp: FlexibleType => + this(x, tp.underlying) + case ExprType(restpe) => this(x, restpe) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index 7d2d95aa9601..0a8669292a74 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -272,6 +272,9 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { case tpe: OrType => writeByte(ORtype) withLength { pickleType(tpe.tp1, richTypes); pickleType(tpe.tp2, richTypes) } + case tpe: FlexibleType => + writeByte(FLEXIBLEtype) + withLength { pickleType(tpe.underlying, richTypes) } case tpe: ExprType => writeByte(BYNAMEtype) pickleType(tpe.underlying) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index a75cc6c666d0..5f04418bbe7f 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -442,6 +442,8 @@ class TreeUnpickler(reader: TastyReader, readTypeRef() match { case binder: LambdaType => binder.paramRefs(readNat()) } + case FLEXIBLEtype => + FlexibleType(readType()) } assert(currentAddr == end, s"$start $currentAddr $end ${astTagToString(tag)}") result diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index ac7b4ef39604..241bfb4f7c7b 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -294,6 +294,8 @@ class PlainPrinter(_ctx: Context) extends Printer { && !printDebug then atPrec(GlobalPrec)( Str("into ") ~ toText(tpe) ) else toTextLocal(tpe) ~ " " ~ toText(annot) + case FlexibleType(_, tpe) => + "(" ~ toText(tpe) ~ ")?" case tp: TypeVar => def toTextCaret(tp: Type) = if printDebug then toTextLocal(tp) ~ Str("^") else toText(tp) if (tp.isInstantiated) diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index d43a2f22a7fb..138cda099040 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -681,6 +681,8 @@ private class ExtractAPICollector(nonLocalClassSymbols: mutable.HashSet[Symbol]) case tp: OrType => val s = combineApiTypes(apiType(tp.tp1), apiType(tp.tp2)) withMarker(s, orMarker) + case tp: FlexibleType => + apiType(tp.underlying) case ExprType(resultType) => withMarker(apiType(resultType), byNameMarker) case MatchType(bound, scrut, cases) => diff --git a/compiler/src/dotty/tools/dotc/transform/ElimRepeated.scala b/compiler/src/dotty/tools/dotc/transform/ElimRepeated.scala index b98d7d525089..ae2fc578728f 100644 --- a/compiler/src/dotty/tools/dotc/transform/ElimRepeated.scala +++ b/compiler/src/dotty/tools/dotc/transform/ElimRepeated.scala @@ -293,7 +293,7 @@ class ElimRepeated extends MiniPhase with InfoTransformer { thisPhase => val element = array.elemType.hiBound // T if element <:< defn.AnyRefType - || ctx.mode.is(Mode.SafeNulls) && element.stripNull <:< defn.AnyRefType + || ctx.mode.is(Mode.SafeNulls) && element.stripNull() <:< defn.AnyRefType || element.typeSymbol.isPrimitiveValueClass then array else defn.ArrayOf(TypeBounds.upper(AndType(element, defn.AnyRefType))) // Array[? <: T & AnyRef] diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 44d5caba631a..f809fbd176ce 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -427,7 +427,7 @@ abstract class Recheck extends Phase, SymTransformer: TypeComparer.lub(bodyType :: casesTypes) def recheckSeqLiteral(tree: SeqLiteral, pt: Type)(using Context): Type = - val elemProto = pt.stripNull.elemType match + val elemProto = pt.stripNull().elemType match case NoType => WildcardType case bounds: TypeBounds => WildcardType(bounds) case elemtp => elemtp diff --git a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala index 6d2aedb9b47b..45606b0dbef5 100644 --- a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala +++ b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala @@ -252,7 +252,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 => - m.paramInfos.head.stripNull == defn.StringType + m.paramInfos.head.stripNull() == defn.StringType case _ => false } val constructor = ioob.typeSymbol.info.decls.find(filterStringConstructor _).asTerm diff --git a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index 74a4845424ea..509461c794f4 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -151,7 +151,8 @@ object TypeTestsCasts { // - T1 & T2 <:< T3 // See TypeComparer#either recur(tp1, P) && recur(tp2, P) - + case tpX: FlexibleType => + recur(tpX.underlying, P) case x => // always false test warnings are emitted elsewhere // provablyDisjoint wants fully applied types as input; because we're in the middle of erasure, we sometimes get raw types here diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 4cf0e6619772..bca832b0bfaf 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -971,7 +971,7 @@ trait Applications extends Compatibility { // one can imagine the original signature-polymorphic method as // being infinitely overloaded, with each individual overload only // being brought into existence as needed - val originalResultType = funRef.symbol.info.resultType.stripNull + val originalResultType = funRef.symbol.info.resultType.stripNull() val resultType = if !originalResultType.isRef(defn.ObjectClass) then originalResultType else AvoidWildcardsMap()(proto.resultType.deepenProtoTrans) match diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 914fc0acb89d..3f071dad2d03 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -33,20 +33,24 @@ object Nullables: && hi.isValueType // We cannot check if hi is nullable, because it can cause cyclic reference. + private def nullifiedHi(lo: Type, hi: Type)(using Context): Type = + if needNullifyHi(lo, hi) then + if ctx.flexibleTypes then FlexibleType(hi) else OrNull(hi) + else hi + /** Create a nullable type bound * If lo is `Null`, `| Null` is added to hi */ def createNullableTypeBounds(lo: Type, hi: Type)(using Context): TypeBounds = - val newHi = if needNullifyHi(lo, hi) then OrType(hi, defn.NullType, soft = false) else hi - TypeBounds(lo, newHi) + TypeBounds(lo, nullifiedHi(lo, hi)) /** Create a nullable type bound tree * If lo is `Null`, `| Null` is added to hi */ def createNullableTypeBoundsTree(lo: Tree, hi: Tree, alias: Tree = EmptyTree)(using Context): TypeBoundsTree = - val hiTpe = hi.typeOpt - val newHi = if needNullifyHi(lo.typeOpt, hiTpe) then TypeTree(OrType(hiTpe, defn.NullType, soft = false)) else hi - TypeBoundsTree(lo, newHi, alias) + val hiTpe = nullifiedHi(lo.typeOpt, hi.typeOpt) + val hiTree = if(hiTpe eq hi.typeOpt) hi else TypeTree(hiTpe) + TypeBoundsTree(lo, hiTree, alias) /** A set of val or var references that are known to be not null, plus a set of * variable references that are not known (anymore) to be not null diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 6b9afab06e33..8c80cdf3100b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -981,17 +981,23 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } if (untpd.isWildcardStarArg(tree)) { - def typedWildcardStarArgExpr = { - // A sequence argument `xs: _*` can be either a `Seq[T]` or an `Array[_ <: T]`, - // irrespective of whether the method we're calling is a Java or Scala method, - // so the expected type is the union `Seq[T] | Array[_ <: T]`. - val ptArg = - // FIXME(#8680): Quoted patterns do not support Array repeated arguments + + def fromRepeated(pt: Type): Type = pt match + case pt: FlexibleType => + pt.derivedFlexibleType(fromRepeated(pt.hi)) + case _ => if ctx.mode.isQuotedPattern then + // FIXME(#8680): Quoted patterns do not support Array repeated arguments pt.translateFromRepeated(toArray = false, translateWildcard = true) else pt.translateFromRepeated(toArray = false, translateWildcard = true) - | pt.translateFromRepeated(toArray = true, translateWildcard = true) + | pt.translateFromRepeated(toArray = true, translateWildcard = true) + + def typedWildcardStarArgExpr = { + // A sequence argument `xs: _*` can be either a `Seq[T]` or an `Array[_ <: T]`, + // irrespective of whether the method we're calling is a Java or Scala method, + // so the expected type is the union `Seq[T] | Array[_ <: T]`. + val ptArg = fromRepeated(pt) val expr0 = typedExpr(tree.expr, ptArg) val expr1 = if ctx.explicitNulls && (!ctx.mode.is(Mode.Pattern)) then if expr0.tpe.isNullType then @@ -1079,7 +1085,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer * with annotation contructor, as named arguments are not allowed anywhere else in Java. * Under explicit nulls, the pt could be nullable. We need to strip `Null` type first. */ - val arg1 = pt.stripNull match { + val arg1 = pt.stripNull() match { case AppliedType(a, typ :: Nil) if ctx.isJava && a.isRef(defn.ArrayClass) => tryAlternatively { typed(tree.arg, pt) } { val elemTp = untpd.TypedSplice(TypeTree(typ)) @@ -1906,7 +1912,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val case1 = typedCase(cas, sel, wideSelType, tpe)(using caseCtx) caseCtx = Nullables.afterPatternContext(sel, case1.pat) if !alreadyStripped && Nullables.matchesNull(case1) then - wideSelType = wideSelType.stripNull + wideSelType = wideSelType.stripNull() alreadyStripped = true case1 } @@ -1929,7 +1935,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val case1 = typedCase(cas, sel, wideSelType, pt)(using caseCtx) caseCtx = Nullables.afterPatternContext(sel, case1.pat) if !alreadyStripped && Nullables.matchesNull(case1) then - wideSelType = wideSelType.stripNull + wideSelType = wideSelType.stripNull() alreadyStripped = true case1 } @@ -2129,7 +2135,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer else res def typedSeqLiteral(tree: untpd.SeqLiteral, pt: Type)(using Context): SeqLiteral = { - val elemProto = pt.stripNull.elemType match { + val elemProto = pt.stripNull().elemType match { case NoType => WildcardType case bounds: TypeBounds => WildcardType(bounds) case elemtp => elemtp diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index a96a4ea09102..542ef1897b74 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -194,22 +194,24 @@ class CompilationTests { @Test def explicitNullsNeg: Unit = { implicit val testGroup: TestGroup = TestGroup("explicitNullsNeg") aggregateTests( - compileFilesInDir("tests/explicit-nulls/neg", defaultOptions and "-Yexplicit-nulls"), - compileFilesInDir("tests/explicit-nulls/unsafe-common", defaultOptions and "-Yexplicit-nulls"), + compileFilesInDir("tests/explicit-nulls/neg", explicitNullsOptions), + compileFilesInDir("tests/explicit-nulls/flexible-types-common", explicitNullsOptions and "-Yno-flexible-types"), + compileFilesInDir("tests/explicit-nulls/unsafe-common", explicitNullsOptions and "-Yno-flexible-types"), ) }.checkExpectedErrors() @Test def explicitNullsPos: Unit = { implicit val testGroup: TestGroup = TestGroup("explicitNullsPos") aggregateTests( - compileFilesInDir("tests/explicit-nulls/pos", defaultOptions and "-Yexplicit-nulls"), - compileFilesInDir("tests/explicit-nulls/unsafe-common", defaultOptions and "-Yexplicit-nulls" and "-language:unsafeNulls"), + compileFilesInDir("tests/explicit-nulls/pos", explicitNullsOptions), + compileFilesInDir("tests/explicit-nulls/flexible-types-common", explicitNullsOptions), + compileFilesInDir("tests/explicit-nulls/unsafe-common", explicitNullsOptions and "-language:unsafeNulls" and "-Yno-flexible-types"), ) }.checkCompile() @Test def explicitNullsRun: Unit = { implicit val testGroup: TestGroup = TestGroup("explicitNullsRun") - compileFilesInDir("tests/explicit-nulls/run", defaultOptions and "-Yexplicit-nulls") + compileFilesInDir("tests/explicit-nulls/run", explicitNullsOptions) }.checkRuns() // initialization tests diff --git a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala index 1defe3f4f53d..f5540304da89 100644 --- a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala +++ b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala @@ -89,6 +89,8 @@ object TestConfiguration { val picklingWithCompilerOptions = picklingOptions.withClasspath(withCompilerClasspath).withRunClasspath(withCompilerClasspath) + val explicitNullsOptions = defaultOptions and "-Yexplicit-nulls" + /** Default target of the generated class files */ private def defaultTarget: String = { import scala.util.Properties.isJavaAtLeast diff --git a/docs/_docs/reference/experimental/explicit-nulls.md b/docs/_docs/reference/experimental/explicit-nulls.md index 1925b0b3c925..bcbea34dd18d 100644 --- a/docs/_docs/reference/experimental/explicit-nulls.md +++ b/docs/_docs/reference/experimental/explicit-nulls.md @@ -111,17 +111,59 @@ y == x // ok (x: Any) == null // ok ``` -## Java Interoperability +## Java Interoperability and Flexible Types + +When dealing with reference types from Java, it's essential to address the implicit nullability of these types. +The most accurate way to represent them in Scala is to use nullable types, though working with lots of nullable types +directly can be annoying. +To streamline interactions with Java libraries, we introduce the concept of flexible types. + +The flexible type, denoted by `T?`, functions as an abstract type with unique bounds: `T | Null ... T`, +ensuring that `T | Null <: T? <: T`. +The subtyping rule treats a reference type coming from Java as either nullable or non-nullable depending on the context. +This concept draws inspiration from Kotlin's +[platform types](https://kotlinlang.org/docs/java-interop.html#null-safety-and-platform-types). +By relaxing null checks for such types, Scala aligns its safety guarantees with those of Java. +Notably, flexible types are non-denotable, meaning users cannot explicitly write them in the code; +only the compiler can construct or infer these types. + +Consequently, a value with a flexible type can serve as both a nullable and non-nullable value. +Additionally, both nullable and non-nullable values can be passed as parameters with flexible types during function calls. +Invoking the member functions of a flexible type is allowed, but it can trigger a `NullPointerException` +if the value is indeed `null` during runtime. -The Scala compiler can load Java classes in two ways: from source or from bytecode. In either case, -when a Java class is loaded, we "patch" the type of its members to reflect that Java types -remain implicitly nullable. - -Specifically, we patch +```scala +// Considering class J is from Java +class J { + // Translates to def f(s: String?): Unit + public void f(String s) { + } -- the type of fields + // Translates to def g(): String? + public String g() { + return ""; + } +} + +// Use J in Scala +def useJ(j: J) = + val x1: String = "" + val x2: String | Null = null + j.f(x1) // Passing String to String? + j.f(x2) // Passing String | Null to String? + j.f(null) // Passing Null to String? + + // Assign String? to String + val y1: String = j.g() + // Assign String? to String | Null + val y2: String | Null = j.g() + + // Calling member functions on flexible types + j.g().trim().length() +``` -- the argument type and return type of methods +Upon loading a Java class, whether from source or bytecode, the Scala compiler dynamically adjusts the type of its members to reflect nullability. +This adjustment involves adding flexible types to the reference types of fields, as well as the argument types and return types of methods We illustrate the rules with following examples: @@ -138,7 +180,7 @@ We illustrate the rules with following examples: ```scala class C: - val s: String | Null + val s: String? val x: Int ``` @@ -151,15 +193,7 @@ We illustrate the rules with following examples: ==> ```scala - class C[T] { def foo(): T | Null } - ``` - - Notice this is rule is sometimes too conservative, as witnessed by - - ```scala - class InScala: - val c: C[Bool] = ??? // C as above - val b: Bool = c.foo() // no longer typechecks, since foo now returns Bool | Null + class C[T] { def foo(): T? } ``` - We can reduce the number of redundant nullable types we need to add. Consider @@ -172,21 +206,21 @@ We illustrate the rules with following examples: ==> ```scala - class Box[T] { def get(): T | Null } - class BoxFactory[T] { def makeBox(): Box[T] | Null } + class Box[T] { def get(): T? } + class BoxFactory[T] { def makeBox(): Box[T]? } ``` Suppose we have a `BoxFactory[String]`. Notice that calling `makeBox()` on it returns a - `Box[String] | Null`, not a `Box[String | Null] | Null`. This seems at first + `Box[T]?`, not a `Box[T?]?`. This seems at first glance unsound ("What if the box itself has `null` inside?"), but is sound because calling - `get()` on a `Box[String]` returns a `String | Null`. + `get()` on a `Box[String]` returns a `String?`. Notice that we need to patch _all_ Java-defined classes that transitively appear in the argument or return type of a field or method accessible from the Scala code being compiled. Absent crazy reflection magic, we think that all such Java classes _must_ be visible to the Typer in the first place, so they will be patched. -- We will append `Null` to the type arguments if the generic class is defined in Scala. +- We will patch the type arguments if the generic class is defined in Scala. ```java class BoxFactory { @@ -199,16 +233,16 @@ We illustrate the rules with following examples: ```scala class BoxFactory[T]: - def makeBox(): Box[T | Null] | Null - def makeCrazyBoxes(): java.util.List[Box[java.util.List[T] | Null]] | Null + def makeBox(): Box[T?]? + def makeCrazyBoxes(): java.util.List[Box[java.util.List[T]?]]? ``` - In this case, since `Box` is Scala-defined, we will get `Box[T | Null] | Null`. + In this case, since `Box` is Scala-defined, we will get `Box[T?]?`. This is needed because our nullability function is only applied (modularly) to the Java classes, but not to the Scala ones, so we need a way to tell `Box` that it contains a nullable value. - The `List` is Java-defined, so we don't append `Null` to its type argument. But we + The `List` is Java-defined, so we don't patch its type argument. But we still need to nullify its inside. - We don't nullify _simple_ literal constant (`final`) fields, since they are known to be non-null @@ -234,7 +268,7 @@ We illustrate the rules with following examples: val NAME_GENERATED: String | Null = getNewName() ``` -- We don't append `Null` to a field nor to a return type of a method which is annotated with a +- We don't patch a field nor to a return type of a method which is annotated with a `NotNull` annotation. ```java @@ -250,8 +284,8 @@ We illustrate the rules with following examples: ```scala class C: val name: String - def getNames(prefix: String | Null): java.util.List[String] // we still need to nullify the paramter types - def getBoxedName(): Box[String | Null] // we don't append `Null` to the outmost level, but we still need to nullify inside + def getNames(prefix: String?): java.util.List[String] // we still need to nullify the paramter types + def getBoxedName(): Box[String?] // we don't append `Null` to the outmost level, but we still need to nullify inside ``` The annotation must be from the list below to be recognized as `NotNull` by the compiler. @@ -280,6 +314,9 @@ We illustrate the rules with following examples: "io.reactivex.annotations.NonNull" :: Nil map PreNamedString) ``` +Flexible types can be disabled by using `-Yno-flexible-types` flag. +The ordinary union type `| Null` will be used instead. + ### Override check When we check overriding between Scala classes and Java classes, the rules are relaxed for [`Null`](https://scala-lang.org/api/3.x/scala/Null.html) type with this feature, in order to help users to working with Java libraries. diff --git a/library/src/scala/runtime/stdLibPatches/Predef.scala b/library/src/scala/runtime/stdLibPatches/Predef.scala index 09feaf11c31d..2146254a9467 100644 --- a/library/src/scala/runtime/stdLibPatches/Predef.scala +++ b/library/src/scala/runtime/stdLibPatches/Predef.scala @@ -1,5 +1,7 @@ package scala.runtime.stdLibPatches +import scala.annotation.experimental + object Predef: import compiletime.summonFrom @@ -60,5 +62,4 @@ object Predef: * `eq` or `ne` methods, only `==` and `!=` inherited from `Any`. */ inline def ne(inline y: AnyRef | Null): Boolean = !(x eq y) - end Predef diff --git a/project/Build.scala b/project/Build.scala index 336d576c7207..fef7a2bcb60b 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -908,6 +908,13 @@ object Build { "-Ddotty.tests.classes.dottyTastyInspector=" + jars("scala3-tasty-inspector"), ) }, + // For compatibility at this moment, both the bootstrapped and the non-bootstrapped + // compilers are compiled without flexible types. + // We should move the flag to commonDottyCompilerSettings once the reference + // compiler is updated. + // Then, the next step is to enable flexible types by default and reduce the use of + // `unsafeNulls`. + scalacOptions ++= Seq("-Yno-flexible-types"), packageAll := { (`scala3-compiler` / packageAll).value ++ Seq( "scala3-compiler" -> (Compile / packageBin).value.getAbsolutePath, @@ -1290,6 +1297,10 @@ object Build { .asScala3PresentationCompiler(NonBootstrapped) lazy val `scala3-presentation-compiler-bootstrapped` = project.in(file("presentation-compiler")) .asScala3PresentationCompiler(Bootstrapped) + .settings( + // Add `-Yno-flexible-types` flag for bootstrap, see comments for `bootstrappedDottyCompilerSettings` + Compile / scalacOptions += "-Yno-flexible-types" + ) def scala3PresentationCompiler(implicit mode: Mode): Project = mode match { case NonBootstrapped => `scala3-presentation-compiler` diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index e45e7a81904b..0051d744f787 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -72,6 +72,7 @@ object MiMaFilters { val ForwardsBreakingChanges: Map[String, Seq[ProblemFilter]] = Map( // Additions that require a new minor version of tasty core Build.previousDottyVersion -> Seq( + ProblemFilters.exclude[DirectMissingMethodProblem]("dotty.tools.tasty.TastyFormat.FLEXIBLEtype") ), // Additions since last LTS diff --git a/tasty/src/dotty/tools/tasty/TastyFormat.scala b/tasty/src/dotty/tools/tasty/TastyFormat.scala index e17c98234691..6cd63d0d8f01 100644 --- a/tasty/src/dotty/tools/tasty/TastyFormat.scala +++ b/tasty/src/dotty/tools/tasty/TastyFormat.scala @@ -176,6 +176,7 @@ Standard-Section: "ASTs" TopLevelStat* ORtype Length left_Type right_Type -- lefgt | right MATCHtype Length bound_Type sel_Type case_Type* -- sel match {cases} with optional upper `bound` MATCHCASEtype Length pat_type rhs_Type -- match cases are MATCHCASEtypes or TYPELAMBDAtypes over MATCHCASEtypes + FLEXIBLEtype Length underlying_Type -- (underlying)? BIND Length boundName_NameRef bounds_Type Modifier* -- boundName @ bounds, for type-variables defined in a type pattern BYNAMEtype underlying_Type -- => underlying PARAMtype Length binder_ASTRef paramNum_Nat -- A reference to parameter # paramNum in lambda type `binder` @@ -617,6 +618,7 @@ object TastyFormat { final val MATCHtype = 190 final val MATCHtpt = 191 final val MATCHCASEtype = 192 + final val FLEXIBLEtype = 193 final val HOLE = 255 @@ -648,7 +650,7 @@ object TastyFormat { firstNatTreeTag <= tag && tag <= RENAMED || firstASTTreeTag <= tag && tag <= BOUNDED || firstNatASTTreeTag <= tag && tag <= NAMEDARG || - firstLengthTreeTag <= tag && tag <= MATCHCASEtype || + firstLengthTreeTag <= tag && tag <= FLEXIBLEtype || tag == HOLE def isParamTag(tag: Int): Boolean = tag == PARAM || tag == TYPEPARAM @@ -850,6 +852,7 @@ object TastyFormat { case MATCHCASEtype => "MATCHCASEtype" case MATCHtpt => "MATCHtpt" case PARAMtype => "PARAMtype" + case FLEXIBLEtype => "FLEXIBLEtype" case ANNOTATION => "ANNOTATION" case PRIVATEqualified => "PRIVATEqualified" case PROTECTEDqualified => "PROTECTEDqualified" diff --git a/tests/explicit-nulls/flexible-types-common/i7883.scala b/tests/explicit-nulls/flexible-types-common/i7883.scala new file mode 100644 index 000000000000..9ee92553b60d --- /dev/null +++ b/tests/explicit-nulls/flexible-types-common/i7883.scala @@ -0,0 +1,9 @@ +import scala.util.matching.Regex + +object Test extends App { + def head(s: String, r: Regex): Option[(String, String)] = + s.trim match { + case r(hd, tl) => Some((hd, tl)) // error // error // error + case _ => None + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/neg/interop-array-src/J.java b/tests/explicit-nulls/flexible-types-common/interop-array-src/J.java similarity index 100% rename from tests/explicit-nulls/neg/interop-array-src/J.java rename to tests/explicit-nulls/flexible-types-common/interop-array-src/J.java diff --git a/tests/explicit-nulls/neg/interop-array-src/S.scala b/tests/explicit-nulls/flexible-types-common/interop-array-src/S.scala similarity index 100% rename from tests/explicit-nulls/neg/interop-array-src/S.scala rename to tests/explicit-nulls/flexible-types-common/interop-array-src/S.scala diff --git a/tests/explicit-nulls/flexible-types-common/interop-chain.scala b/tests/explicit-nulls/flexible-types-common/interop-chain.scala new file mode 100644 index 000000000000..27a2d507801e --- /dev/null +++ b/tests/explicit-nulls/flexible-types-common/interop-chain.scala @@ -0,0 +1,9 @@ +// With flexible types, we can select a member of its underlying type. + +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/pos/interop-enum-src/Day.java b/tests/explicit-nulls/flexible-types-common/interop-enum-src/Day.java similarity index 100% rename from tests/explicit-nulls/pos/interop-enum-src/Day.java rename to tests/explicit-nulls/flexible-types-common/interop-enum-src/Day.java diff --git a/tests/explicit-nulls/neg/interop-enum-src/Planet.java b/tests/explicit-nulls/flexible-types-common/interop-enum-src/Planet.java similarity index 100% rename from tests/explicit-nulls/neg/interop-enum-src/Planet.java rename to tests/explicit-nulls/flexible-types-common/interop-enum-src/Planet.java diff --git a/tests/explicit-nulls/pos/interop-enum-src/S.scala b/tests/explicit-nulls/flexible-types-common/interop-enum-src/S.scala similarity index 60% rename from tests/explicit-nulls/pos/interop-enum-src/S.scala rename to tests/explicit-nulls/flexible-types-common/interop-enum-src/S.scala index 75e4654869a4..ce0935271d11 100644 --- a/tests/explicit-nulls/pos/interop-enum-src/S.scala +++ b/tests/explicit-nulls/flexible-types-common/interop-enum-src/S.scala @@ -3,4 +3,5 @@ class S { val d: Day = Day.MON val p: Planet = Planet.MARS + 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/flexible-types-common/interop-generics/J.java similarity index 100% rename from tests/explicit-nulls/neg/interop-generics/J.java rename to tests/explicit-nulls/flexible-types-common/interop-generics/J.java diff --git a/tests/explicit-nulls/neg/interop-generics/S.scala b/tests/explicit-nulls/flexible-types-common/interop-generics/S.scala similarity index 100% rename from tests/explicit-nulls/neg/interop-generics/S.scala rename to tests/explicit-nulls/flexible-types-common/interop-generics/S.scala diff --git a/tests/explicit-nulls/flexible-types-common/interop-implicit.scala b/tests/explicit-nulls/flexible-types-common/interop-implicit.scala new file mode 100644 index 000000000000..4bbba8f11cab --- /dev/null +++ b/tests/explicit-nulls/flexible-types-common/interop-implicit.scala @@ -0,0 +1,10 @@ +class S { + locally { + // OfType Implicits + + import java.nio.charset.StandardCharsets + import scala.io.Codec + + val c: Codec = StandardCharsets.UTF_8 // error + } +} \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-types-common/interop-java-call/J.java b/tests/explicit-nulls/flexible-types-common/interop-java-call/J.java new file mode 100644 index 000000000000..554b91749889 --- /dev/null +++ b/tests/explicit-nulls/flexible-types-common/interop-java-call/J.java @@ -0,0 +1,17 @@ +public class J { + public String f1() { + return ""; + } + + public int f2() { + return 0; + } + + public T g1() { + return null; + } +} + +class J2 { + public T x = null; +} \ No newline at end of file diff --git a/tests/explicit-nulls/flexible-types-common/interop-java-call/S.scala b/tests/explicit-nulls/flexible-types-common/interop-java-call/S.scala new file mode 100644 index 000000000000..acdbbafc3fab --- /dev/null +++ b/tests/explicit-nulls/flexible-types-common/interop-java-call/S.scala @@ -0,0 +1,37 @@ +// Check Java calls have been cast to non-nullable. + +val j: J = new J + +val s1: String = j.f1() // error + +val s1n: String | Null = j.f1() + +val i1: Int = j.f2() + +val s2: String = j.g1[String]() // error + +val s2n: String | Null = j.g1[String]() + +// val s3: String = j.g1[String | Null]() error + +val s3n: String | Null = j.g1[String | Null]() + +val i2: Int = j.g1[Int]() // error + +val a1: Any = j.g1[Any]() + +val ar1: AnyRef = j.g1[AnyRef]() // error + +val n1: Null = j.g1[Null]() + +// val ar2: AnyRef = j.g1[Null]() error + +def clo1[T]: T = j.g1[T]() // error + +def clo2[T <: AnyRef]: T = j.g1[T]() // error + +def clo3[T >: Null <: AnyRef | Null]: T = j.g1[T]() + +def testJ2[T]: T = + val j2: J2[T] = new J2 + j2.x // error diff --git a/tests/explicit-nulls/flexible-types-common/interop-java-chain/J.java b/tests/explicit-nulls/flexible-types-common/interop-java-chain/J.java new file mode 100644 index 000000000000..bd266bae13d9 --- /dev/null +++ b/tests/explicit-nulls/flexible-types-common/interop-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/flexible-types-common/interop-java-chain/S.scala b/tests/explicit-nulls/flexible-types-common/interop-java-chain/S.scala new file mode 100644 index 000000000000..9fe5aa3f08ce --- /dev/null +++ b/tests/explicit-nulls/flexible-types-common/interop-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/pos/interop-java-varargs-src/Names.java b/tests/explicit-nulls/flexible-types-common/interop-java-varargs-src/Names.java similarity index 100% rename from tests/explicit-nulls/pos/interop-java-varargs-src/Names.java rename to tests/explicit-nulls/flexible-types-common/interop-java-varargs-src/Names.java diff --git a/tests/explicit-nulls/pos/interop-java-varargs-src/S.scala b/tests/explicit-nulls/flexible-types-common/interop-java-varargs-src/S.scala similarity index 64% rename from tests/explicit-nulls/pos/interop-java-varargs-src/S.scala rename to tests/explicit-nulls/flexible-types-common/interop-java-varargs-src/S.scala index e867202e506d..ef0b702b0006 100644 --- a/tests/explicit-nulls/pos/interop-java-varargs-src/S.scala +++ b/tests/explicit-nulls/flexible-types-common/interop-java-varargs-src/S.scala @@ -16,4 +16,14 @@ class S { // Multiple arguments, some null. Names.setNames(null, null, "hello", "world", null) + + val arg1: Array[String] = ??? + val arg2: Array[String | Null] = ??? + val arg3: Array[String] | Null = ??? + val arg4: Array[String | Null] | Null = ??? + + Names.setNames(arg1*) + Names.setNames(arg2*) + Names.setNames(arg3*) // error + Names.setNames(arg4*) // error } diff --git a/tests/explicit-nulls/flexible-types-common/interop-java-varargs.scala b/tests/explicit-nulls/flexible-types-common/interop-java-varargs.scala new file mode 100644 index 000000000000..9ec27cb090a1 --- /dev/null +++ b/tests/explicit-nulls/flexible-types-common/interop-java-varargs.scala @@ -0,0 +1,38 @@ +import java.nio.file.Paths + +def test1 = { + Paths.get("") + Paths.get("", null) + Paths.get("", "") + Paths.get("", "", null) + + val x1: String = ??? + val x2: String | Null = ??? + + Paths.get("", x1) + Paths.get("", x2) +} + +def test2 = { + val xs1: Seq[String] = ??? + val xs2: Seq[String | Null] = ??? + val xs3: Seq[String | Null] | Null = ??? + val xs4: Seq[String] | Null = ??? + + val ys1: Array[String] = ??? + val ys2: Array[String | Null] = ??? + val ys3: Array[String | Null] | Null = ??? + val ys4: Array[String] | Null = ??? + + Paths.get("", xs1*) + Paths.get("", xs2*) + Paths.get("", xs3*) // error + Paths.get("", xs4*) // error + + Paths.get("", ys1*) + Paths.get("", ys2*) + Paths.get("", ys3*) // error + Paths.get("", ys4*) // error + + Paths.get("", null*) // error +} \ No newline at end of file diff --git a/tests/explicit-nulls/neg/interop-method-src/J.java b/tests/explicit-nulls/flexible-types-common/interop-method-src/J.java similarity index 100% rename from tests/explicit-nulls/neg/interop-method-src/J.java rename to tests/explicit-nulls/flexible-types-common/interop-method-src/J.java diff --git a/tests/explicit-nulls/neg/interop-method-src/S.scala b/tests/explicit-nulls/flexible-types-common/interop-method-src/S.scala similarity index 100% rename from tests/explicit-nulls/neg/interop-method-src/S.scala rename to tests/explicit-nulls/flexible-types-common/interop-method-src/S.scala diff --git a/tests/explicit-nulls/flexible-types-common/interop-propagate.scala b/tests/explicit-nulls/flexible-types-common/interop-propagate.scala new file mode 100644 index 000000000000..40eb12dd287c --- /dev/null +++ b/tests/explicit-nulls/flexible-types-common/interop-propagate.scala @@ -0,0 +1,18 @@ + class Foo { + import java.util.ArrayList + + // Test that type mapping works with flexible types. + val ll: ArrayList[ArrayList[ArrayList[String]]] = new ArrayList[ArrayList[ArrayList[String]]] + val level1: ArrayList[ArrayList[String]] = ll.get(0) // error + val level2: ArrayList[String] = ll.get(0).get(0) // error + val level3: String = ll.get(0).get(0).get(0) // error + + val lb = new ArrayList[ArrayList[ArrayList[String]]] + val levelA = lb.get(0) + val levelB = lb.get(0).get(0) // error + val levelC = lb.get(0).get(0).get(0) // error + + val x = levelA.get(0) // error + val y = levelB.get(0) + val z: String = levelA.get(0).get(0) // error +} diff --git a/tests/explicit-nulls/flexible-types-common/interop-select-type-member.scala b/tests/explicit-nulls/flexible-types-common/interop-select-type-member.scala new file mode 100644 index 000000000000..ddd402545edb --- /dev/null +++ b/tests/explicit-nulls/flexible-types-common/interop-select-type-member.scala @@ -0,0 +1,7 @@ +import java.util.ArrayList + +def f[T]: ArrayList[T] = { + val cz = Class.forName("java.util.ArrayList") + val o = cz.newInstance() // error: T of Class[?] | Null + o.asInstanceOf[ArrayList[T]] +} \ No newline at end of file diff --git a/tests/explicit-nulls/neg/i7883.check b/tests/explicit-nulls/neg/i7883.check index e37285332359..f14e5d4e7481 100644 --- a/tests/explicit-nulls/neg/i7883.check +++ b/tests/explicit-nulls/neg/i7883.check @@ -1,19 +1,19 @@ --- [E134] Type Error: tests/explicit-nulls/neg/i7883.scala:6:11 -------------------------------------------------------- -6 | case r(hd, tl) => Some((hd, tl)) // error // error // error +-- [E134] Type Error: tests/explicit-nulls/neg/i7883.scala:8:11 -------------------------------------------------------- +8 | case r(hd, tl) => Some((hd, tl)) // error // error // error | ^ | None of the overloaded alternatives of method unapplySeq in class Regex with types | (m: scala.util.matching.Regex.Match): Option[List[String]] | (c: Char): Option[List[Char]] | (s: CharSequence): Option[List[String]] | match arguments (String | Null) --- [E006] Not Found Error: tests/explicit-nulls/neg/i7883.scala:6:30 --------------------------------------------------- -6 | case r(hd, tl) => Some((hd, tl)) // error // error // error +-- [E006] Not Found Error: tests/explicit-nulls/neg/i7883.scala:8:30 --------------------------------------------------- +8 | case r(hd, tl) => Some((hd, tl)) // error // error // error | ^^ | Not found: hd | | longer explanation available when compiling with `-explain` --- [E006] Not Found Error: tests/explicit-nulls/neg/i7883.scala:6:34 --------------------------------------------------- -6 | case r(hd, tl) => Some((hd, tl)) // error // error // error +-- [E006] Not Found Error: tests/explicit-nulls/neg/i7883.scala:8:34 --------------------------------------------------- +8 | case r(hd, tl) => Some((hd, tl)) // error // error // error | ^^ | Not found: tl | diff --git a/tests/explicit-nulls/neg/i7883.scala b/tests/explicit-nulls/neg/i7883.scala index 7938c92dce1e..10d2a6231dca 100644 --- a/tests/explicit-nulls/neg/i7883.scala +++ b/tests/explicit-nulls/neg/i7883.scala @@ -1,3 +1,5 @@ +//> using options -Yno-flexible-types + import scala.util.matching.Regex object Test extends App { diff --git a/tests/explicit-nulls/neg/interop-enum-src/S.scala b/tests/explicit-nulls/neg/interop-enum-src/S.scala deleted file mode 100644 index 99e92cedc68d..000000000000 --- a/tests/explicit-nulls/neg/interop-enum-src/S.scala +++ /dev/null @@ -1,6 +0,0 @@ -// 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-propagate.scala b/tests/explicit-nulls/neg/interop-propagate.scala deleted file mode 100644 index 6af7ee182cac..000000000000 --- a/tests/explicit-nulls/neg/interop-propagate.scala +++ /dev/null @@ -1,10 +0,0 @@ - class Foo { - import java.util.ArrayList - - // Test that the nullability is propagated to nested containers. - val ll = new ArrayList[ArrayList[ArrayList[String]]] - val level1: ArrayList[ArrayList[String]] = ll.get(0) // error - val level2: ArrayList[String] = ll.get(0).get(0) // error - val level3: String = ll.get(0).get(0).get(0) // error - val ok: String = ll.get(0).get(0).get(0) // error -} diff --git a/tests/explicit-nulls/neg/interop-return.scala b/tests/explicit-nulls/neg/interop-return.scala index 1d6df4da93bc..422d37882179 100644 --- a/tests/explicit-nulls/neg/interop-return.scala +++ b/tests/explicit-nulls/neg/interop-return.scala @@ -1,3 +1,5 @@ +//> using options -Yno-flexible-types + // Test that the return type of Java methods as well as the type of Java fields is marked as nullable. class Foo { diff --git a/tests/explicit-nulls/neg/notnull/S.scala b/tests/explicit-nulls/neg/notnull/S.scala index eada60eea6e7..a10bdaabc77c 100644 --- a/tests/explicit-nulls/neg/notnull/S.scala +++ b/tests/explicit-nulls/neg/notnull/S.scala @@ -1,3 +1,5 @@ +//> using options -Yno-flexible-types + // Test that NotNull annotations not in the list are not working in Java files. class S { diff --git a/tests/explicit-nulls/pos/interop-applied-types/J.java b/tests/explicit-nulls/pos/interop-applied-types/J.java new file mode 100644 index 000000000000..c85a921a81b9 --- /dev/null +++ b/tests/explicit-nulls/pos/interop-applied-types/J.java @@ -0,0 +1,3 @@ +public class J { + public J j = this; +} \ No newline at end of file diff --git a/tests/explicit-nulls/pos/interop-applied-types/S.scala b/tests/explicit-nulls/pos/interop-applied-types/S.scala new file mode 100644 index 000000000000..8ff50ab63840 --- /dev/null +++ b/tests/explicit-nulls/pos/interop-applied-types/S.scala @@ -0,0 +1,14 @@ +def test1[T](x: J[T]): J[T] = + x match { + case y: J[_] => y + } + +def test2[T](x: J[T]): J[T] = + x match { + case y: J[_] => y.j + } + +def test3[T](x: J[T]): J[T] = + x.j match { + case y: J[_] => y.j + } \ 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 3defd73f3945..be87b6052699 100644 --- a/tests/explicit-nulls/pos/interop-constructor-src/S.scala +++ b/tests/explicit-nulls/pos/interop-constructor-src/S.scala @@ -3,4 +3,5 @@ class S { val x1: J = new J("hello") val x2: J = new J(null) val x3: J = new J(null, null, null) + val x4: J = new J("hello", null, "world") } diff --git a/tests/explicit-nulls/pos/interop-enum-src/Planet.java b/tests/explicit-nulls/pos/interop-enum-src/Planet.java deleted file mode 100644 index 287aed6aecc5..000000000000 --- a/tests/explicit-nulls/pos/interop-enum-src/Planet.java +++ /dev/null @@ -1,19 +0,0 @@ -public enum Planet { - MERCURY (3.303e+23, 2.4397e6), - VENUS (4.869e+24, 6.0518e6), - EARTH (5.976e+24, 6.37814e6), - MARS (6.421e+23, 3.3972e6), - JUPITER (1.9e+27, 7.1492e7), - SATURN (5.688e+26, 6.0268e7), - URANUS (8.686e+25, 2.5559e7), - NEPTUNE (1.024e+26, 2.4746e7); - - private final double mass; // in kilograms - private final double radius; // in meters - Planet(double mass, double radius) { - this.mass = mass; - this.radius = radius; - } - private double mass() { return mass; } - private double radius() { return radius; } -} diff --git a/tests/explicit-nulls/pos/interop-generics/J.java b/tests/explicit-nulls/pos/interop-generics/J.java deleted file mode 100644 index 4bbdbd4cf319..000000000000 --- a/tests/explicit-nulls/pos/interop-generics/J.java +++ /dev/null @@ -1,13 +0,0 @@ - -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/pos/interop-generics/S.scala b/tests/explicit-nulls/pos/interop-generics/S.scala deleted file mode 100644 index 10a0572b0edf..000000000000 --- a/tests/explicit-nulls/pos/interop-generics/S.scala +++ /dev/null @@ -1,6 +0,0 @@ -class S { - val j = new J() - // Check that the inside of a Java generic isn't nullified - 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/interop-nn-src/S.scala b/tests/explicit-nulls/pos/interop-nn-src/S.scala index 6250c4c3c961..3f6cddb4731b 100644 --- a/tests/explicit-nulls/pos/interop-nn-src/S.scala +++ b/tests/explicit-nulls/pos/interop-nn-src/S.scala @@ -1,7 +1,7 @@ class S { val j = new J() - // Test that the `nn` extension method can be used to strip away - // nullability from a type. + + // Test that the `nn` extension method should work with flexible types. val s: String = j.foo.nn val a: Array[String | Null] = j.bar.nn diff --git a/tests/explicit-nulls/pos/interop-ortype-src/J.java b/tests/explicit-nulls/pos/interop-ortype-src/J.java new file mode 100644 index 000000000000..b0d767bccf3e --- /dev/null +++ b/tests/explicit-nulls/pos/interop-ortype-src/J.java @@ -0,0 +1,3 @@ +class J { + public static T foo(T t) { return null; } +} diff --git a/tests/explicit-nulls/pos/interop-ortype-src/S.scala b/tests/explicit-nulls/pos/interop-ortype-src/S.scala new file mode 100644 index 000000000000..8576ee0895ed --- /dev/null +++ b/tests/explicit-nulls/pos/interop-ortype-src/S.scala @@ -0,0 +1,7 @@ +// Tests that member finding works on (FlexibleType(T) | S) +class S { + def foo(a: J | String) = (a match { + case x: J => J.foo(x: J) + case y: String => "" + }).asInstanceOf[J] +} diff --git a/tests/explicit-nulls/pos/interop-poly-src/S.scala b/tests/explicit-nulls/pos/interop-poly-src/S.scala index 1fea277efe90..8aed9e99b689 100644 --- a/tests/explicit-nulls/pos/interop-poly-src/S.scala +++ b/tests/explicit-nulls/pos/interop-poly-src/S.scala @@ -9,12 +9,29 @@ class Test { // because JavaCat, being a Java class, _already_ nullifies its // fields. val jc: JavaCat[String]|Null = J.getJavaCat[String]() + val jc2: JavaCat[String] = J.getJavaCat[String]() // ScalaCat is Scala-defined, so we need the inner |Null. val sc: ScalaCat[String|Null]|Null = J.getScalaCat[String]() + val sc2: ScalaCat[String]|Null = J.getScalaCat[String]() + val sc3: ScalaCat[String|Null] = J.getScalaCat[String]() + val sc4: ScalaCat[String] = J.getScalaCat[String]() import java.util.List val las: List[Array[String|Null]]|Null = J.getListOfStringArray() + val las2: List[Array[String|Null]] = J.getListOfStringArray() + val las3: List[Array[String]]|Null = J.getListOfStringArray() + val las4: List[Array[String]] = J.getListOfStringArray() val als: Array[List[String]|Null]|Null = J.getArrayOfStringList() + val als2: Array[List[String]|Null] = J.getArrayOfStringList() + val als3: Array[List[String]]|Null = J.getArrayOfStringList() + val als4: Array[List[String]] = J.getArrayOfStringList() val css: List[Array[List[Array[String|Null]]|Null]]|Null = J.getComplexStrings() + val css2: List[Array[List[Array[String]]|Null]]|Null = J.getComplexStrings() + val css3: List[Array[List[Array[String|Null]]]]|Null = J.getComplexStrings() + val css4: List[Array[List[Array[String|Null]]|Null]] = J.getComplexStrings() + val css5: List[Array[List[Array[String|Null]]]] = J.getComplexStrings() + val css6: List[Array[List[Array[String]]]]|Null = J.getComplexStrings() + val css7: List[Array[List[Array[String]]|Null]] = J.getComplexStrings() + val css8: List[Array[List[Array[String]]]] = J.getComplexStrings() } diff --git a/tests/explicit-nulls/pos/interop-static-src/S.scala b/tests/explicit-nulls/pos/interop-static-src/S.scala index 3db9c3f6d281..7e0e4a34898e 100644 --- a/tests/explicit-nulls/pos/interop-static-src/S.scala +++ b/tests/explicit-nulls/pos/interop-static-src/S.scala @@ -1,5 +1,7 @@ class S { // Java static methods are also nullified val x: Int = J.foo(null) + val x2: Int = J.foo("hello") val y: String | Null = J.bar(0) + val y2: String = J.bar(0) } diff --git a/tests/explicit-nulls/pos/match-with-applied-types.scala.scala b/tests/explicit-nulls/pos/match-with-applied-types.scala.scala new file mode 100644 index 000000000000..7b9886ca60ed --- /dev/null +++ b/tests/explicit-nulls/pos/match-with-applied-types.scala.scala @@ -0,0 +1,7 @@ +class A + +def test = + val xs: java.util.LinkedHashMap[String, A | List[A]] = ??? + xs.get("a") match + case a: A => ??? + case as: List[A] => ??? \ No newline at end of file diff --git a/tests/explicit-nulls/pos/sam-parameter-javadefined/injava.java b/tests/explicit-nulls/pos/sam-parameter-javadefined/injava.java new file mode 100644 index 000000000000..28925b3c492a --- /dev/null +++ b/tests/explicit-nulls/pos/sam-parameter-javadefined/injava.java @@ -0,0 +1,6 @@ +class injava { + static void overloaded(Runnable r) {} + static void overloaded(int i) {} + + static void notoverloaded(Runnable r) {} +} diff --git a/tests/explicit-nulls/pos/sam-parameter-javadefined/sam-test.scala b/tests/explicit-nulls/pos/sam-parameter-javadefined/sam-test.scala new file mode 100644 index 000000000000..d3573f590713 --- /dev/null +++ b/tests/explicit-nulls/pos/sam-parameter-javadefined/sam-test.scala @@ -0,0 +1,23 @@ +def foo = { + def unit: Unit = () + + injava.overloaded({ () => unit } : Runnable ) + injava.overloaded({ () => unit } ) + + injava.notoverloaded({ () => unit } : Runnable ) + injava.notoverloaded({ () => unit } ) + + val list = new java.util.Vector[Int]() + java.util.Collections.sort[Int](list, { (a,b) => a - b } : java.util.Comparator[Int] ) + java.util.Collections.sort[Int](list, { (a,b) => a - b }) + + new Thread({ () => unit } : Runnable ) + new Thread({ () => unit } ) + + // See cats.effect.kernel.AsyncPlatform + val cf = new java.util.concurrent.CompletableFuture[String] + cf.handle[Unit]({ + case (string, null) => unit + case (string, throwable) => unit + }) +} diff --git a/tests/explicit-nulls/pos/widen-nullable-union.scala b/tests/explicit-nulls/pos/widen-nullable-union.scala index 9ffa767b84e5..f87b61f781ae 100644 --- a/tests/explicit-nulls/pos/widen-nullable-union.scala +++ b/tests/explicit-nulls/pos/widen-nullable-union.scala @@ -39,4 +39,16 @@ class Test { val y = x val _: (A & B) | Null = y } + + def test1(s: String): String = + val ss = if !s.isEmpty() then s.trim() else s + ss + "!" + + def test2(s: String): String = + val ss = if !s.isEmpty() then s.trim().nn else s + ss + "!" + + def test3(s: String): String = + val ss: String = if !s.isEmpty() then s.trim().nn else s + ss + "!" } diff --git a/tests/explicit-nulls/unsafe-common/unsafe-java-varargs-src/S.scala b/tests/explicit-nulls/unsafe-common/unsafe-java-varargs-src/S.scala index e27b0dcaacbf..67fa583a7b66 100644 --- a/tests/explicit-nulls/unsafe-common/unsafe-java-varargs-src/S.scala +++ b/tests/explicit-nulls/unsafe-common/unsafe-java-varargs-src/S.scala @@ -12,8 +12,8 @@ class S { 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 + j.foo(arg1*) + j.foo(arg2*) + j.foo(arg3*) // error + j.foo(arg4*) // error } \ No newline at end of file diff --git a/tests/neg-deep-subtype/interop-polytypes.scala b/tests/neg-deep-subtype/interop-polytypes.scala index 90922b63f7d0..987e4720bf13 100644 --- a/tests/neg-deep-subtype/interop-polytypes.scala +++ b/tests/neg-deep-subtype/interop-polytypes.scala @@ -1,4 +1,4 @@ -//> using options -Yexplicit-nulls +//> using options -Yexplicit-nulls -Yno-flexible-types class Foo { import java.util.ArrayList