diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index 8f0bc395879e..0ef2a804449a 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -365,8 +365,11 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint .setTyper(new Typer) .addMode(Mode.ImplicitsEnabled) .setTyperState(ctx.typerState.fresh(ctx.reporter)) - if ctx.settings.YexplicitNulls.value && !Feature.enabledBySetting(nme.unsafeNulls) then - start = start.addMode(Mode.SafeNulls) + if ctx.settings.YexplicitNulls.value then + if !Feature.enabledBySetting(nme.unsafeNulls) then + start = start.addMode(Mode.SafeNulls) + if Feature.enabledBySetting(Feature.unsafeJavaReturn) then + start = start.addMode(Mode.UnsafeJavaReturn) ctx.initialize()(using start) // re-initialize the base context with start // `this` must be unchecked for safe initialization because by being passed to setRun during diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 4a87f5b4a537..a08badbdbe25 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -28,6 +28,7 @@ object Feature: val symbolLiterals = deprecated("symbolLiterals") val fewerBraces = experimental("fewerBraces") val saferExceptions = experimental("saferExceptions") + val unsafeJavaReturn = experimental("unsafeJavaReturn") /** Is `feature` enabled by by a command-line setting? The enabling setting is * diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 3dfafe6837d0..41ec9b015e66 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -26,6 +26,7 @@ import scala.io.Codec import collection.mutable import printing._ import config.{JavaPlatform, SJSPlatform, Platform, ScalaSettings} +import config.Feature import classfile.ReusableDataReader import StdNames.nme @@ -642,12 +643,19 @@ object Contexts { def setProfiler(profiler: Profiler): this.type = updateStore(profilerLoc, profiler) def setNotNullInfos(notNullInfos: List[NotNullInfo]): this.type = updateStore(notNullInfosLoc, notNullInfos) def setImportInfo(importInfo: ImportInfo): this.type = - importInfo.mentionsFeature(nme.unsafeNulls) match - case Some(true) => - setMode(this.mode &~ Mode.SafeNulls) - case Some(false) if ctx.settings.YexplicitNulls.value => - setMode(this.mode | Mode.SafeNulls) - case _ => + if ctx.settings.YexplicitNulls.value then + importInfo.mentionsFeature(nme.unsafeNulls) match + case Some(true) => + setMode(this.mode &~ Mode.SafeNulls) + case Some(false) => + setMode(this.mode | Mode.SafeNulls) + case _ => + importInfo.mentionsFeature(Feature.unsafeJavaReturn) match + case Some(true) => + setMode(this.mode | Mode.UnsafeJavaReturn) + case Some(false) => + setMode(this.mode &~ Mode.UnsafeJavaReturn) + case _ => updateStore(importInfoLoc, importInfo) def setTypeAssigner(typeAssigner: TypeAssigner): this.type = updateStore(typeAssignerLoc, typeAssigner) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 3e2373d3bd4b..8a7f1c9a5988 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -989,6 +989,7 @@ class Definitions { @tu lazy val FunctionalInterfaceAnnot: ClassSymbol = requiredClass("java.lang.FunctionalInterface") @tu lazy val TargetNameAnnot: ClassSymbol = requiredClass("scala.annotation.targetName") @tu lazy val VarargsAnnot: ClassSymbol = requiredClass("scala.annotation.varargs") + @tu lazy val CanEqualNullAnnot: ClassSymbol = requiredClass("scala.annotation.CanEqualNull") @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") diff --git a/compiler/src/dotty/tools/dotc/core/Mode.scala b/compiler/src/dotty/tools/dotc/core/Mode.scala index d141cf7032ee..87b7ca1cbe08 100644 --- a/compiler/src/dotty/tools/dotc/core/Mode.scala +++ b/compiler/src/dotty/tools/dotc/core/Mode.scala @@ -129,4 +129,6 @@ object Mode { * Type `Null` becomes a subtype of non-primitive value types in TypeComparer. */ val RelaxedOverriding: Mode = newMode(30, "RelaxedOverriding") + + val UnsafeJavaReturn: Mode = newMode(31, "UnsafeJavaReturn") } diff --git a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala index e18271772ff1..152cf936c762 100644 --- a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala +++ b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala @@ -1,8 +1,12 @@ package dotty.tools.dotc package core +import Annotations._ import Contexts._ +import Flags._ +import Symbols._ import Types._ +import transform.SymUtils._ /** Defines operations on nullable types and tree. */ object NullOpsDecorator: @@ -42,6 +46,24 @@ object NullOpsDecorator: if ctx.explicitNulls then strip(self) else self } + /** Strips `|Null` from the return type of a Java method, + * replacing it with a `@CanEqualNull` annotation + */ + def replaceOrNull(using Context): Type = + // Since this method should only be called on types from Java, + // handling these cases is enough. + def recur(tp: Type): Type = tp match + case tp @ OrType(lhs, rhs) if rhs.isNullType => + AnnotatedType(recur(lhs), Annotation(defn.CanEqualNullAnnot)) + case tp: AndOrType => + tp.derivedAndOrType(recur(tp.tp1), recur(tp.tp2)) + case tp @ AppliedType(tycon, targs) => + tp.derivedAppliedType(tycon, targs.map(recur)) + case mptp: MethodOrPoly => + mptp.derivedLambdaType(resType = recur(mptp.resType)) + case _ => tp + if ctx.explicitNulls then recur(self) else self + /** Is self (after widening and dealiasing) a type of the form `T | Null`? */ def isNullableUnion(using Context): Boolean = { val stripped = self.stripNull @@ -51,10 +73,39 @@ object NullOpsDecorator: import ast.tpd._ - extension (self: Tree) + extension (tree: Tree) + // cast the type of the tree to a non-nullable type - def castToNonNullable(using Context): Tree = self.typeOpt match { - case OrNull(tp) => self.cast(tp) - case _ => self - } + def castToNonNullable(using Context): Tree = tree.typeOpt match + case OrNull(tp) => tree.cast(tp) + case _ => tree + + def tryToCastToCanEqualNull(using Context): Tree = + // return the tree directly if not at Typer phase + if !(ctx.explicitNulls && ctx.phase.isTyper) then return tree + + val sym = tree.symbol + val tp = tree.tpe + + if !ctx.mode.is(Mode.UnsafeJavaReturn) + || !sym.is(JavaDefined) + || sym.isNoValue + || !sym.isTerm + || tp.isError then + return tree + + tree match + case _: Apply if sym.is(Method) => + val tp2 = tp.replaceOrNull + if tp ne tp2 then + tree.cast(tp2) + else tree + case _: Select | _: Ident if !sym.is(Method) => + val tpw = tp.widen + val tp2 = tpw.replaceOrNull + if tpw ne tp2 then + tree.cast(tp2) + else tree + case _ => tree + end NullOpsDecorator diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 6d79f377c84e..f66462553131 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -756,8 +756,12 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling } compareTypeBounds case tp2: AnnotatedType if tp2.isRefining => - (tp1.derivesAnnotWith(tp2.annot.sameAnnotation) || tp1.isBottomType) && - recur(tp1, tp2.parent) + // `CanEqualNull` is a special refining annotation. + // An annotated type is equivalent to the original type. + (tp1.derivesAnnotWith(tp2.annot.sameAnnotation) + || tp2.annot.matches(defn.CanEqualNullAnnot) + || tp1.isBottomType) + && recur(tp1, tp2.parent) case ClassInfo(pre2, cls2, _, _, _) => def compareClassInfo = tp1 match { case ClassInfo(pre1, cls1, _, _, _) => diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 04f5bf034ac0..1e36f0ac9140 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -25,6 +25,7 @@ import reporting._ import transform.TypeUtils._ import transform.SymUtils._ import Nullables._ +import NullOpsDecorator._ import config.Feature import collection.mutable @@ -908,7 +909,7 @@ trait Applications extends Compatibility { def simpleApply(fun1: Tree, proto: FunProto)(using Context): Tree = methPart(fun1).tpe match { case funRef: TermRef => - val app = ApplyTo(tree, fun1, funRef, proto, pt) + val app = ApplyTo(tree, fun1, funRef, proto, pt).tryToCastToCanEqualNull convertNewGenericArray( widenEnumCase( postProcessByNameArgs(funRef, app).computeNullable(), diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index 8b0fa88ad5a9..f453453f0793 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -148,7 +148,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): } /** Is an `CanEqual[cls1, cls2]` instance assumed for predefined classes `cls1`, cls2`? */ - def canComparePredefinedClasses(cls1: ClassSymbol, cls2: ClassSymbol): Boolean = + def canComparePredefinedClasses(cls1: ClassSymbol, cls2: ClassSymbol)(using Context): Boolean = def cmpWithBoxed(cls1: ClassSymbol, cls2: ClassSymbol) = cls2 == defn.NothingClass @@ -164,15 +164,17 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): cmpWithBoxed(cls2, cls1) else if ctx.mode.is(Mode.SafeNulls) then // If explicit nulls is enabled, and unsafeNulls is not enabled, + // and the types don't have `@CanEqualNull` annotation, // we want to disallow comparison between Object and Null. // If we have to check whether a variable with a non-nullable type has null value // (for example, a NotNull java method returns null for some reasons), - // we can still cast it to a nullable type then compare its value. + // we can still use `eq/ne null` or cast it to a nullable type then compare its value. // // Example: // val x: String = null.asInstanceOf[String] // if (x == null) {} // error: x is non-nullable // if (x.asInstanceOf[String|Null] == null) {} // ok + // if (x eq null) {} // ok cls1 == defn.NullClass && cls1 == cls2 else if cls1 == defn.NullClass then cls1 == cls2 || cls2.derivesFrom(defn.ObjectClass) @@ -187,9 +189,20 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): * interpret. */ def canComparePredefined(tp1: Type, tp2: Type) = + // In explicit nulls, when one of type has `@CanEqualNull` annotation, + // we use unsafe nulls semantic to check, which allows reference types + // to be compared with `Null`. + // Example: + // val s1: String = ??? + // s1 == null // error + // val s2: String @CanEqualNull = ??? + // s2 == null // ok + val checkCtx = if ctx.explicitNulls + && (tp1.hasAnnotation(defn.CanEqualNullAnnot) || tp2.hasAnnotation(defn.CanEqualNullAnnot)) + then ctx.retractMode(Mode.SafeNulls) else ctx tp1.classSymbols.exists(cls1 => tp2.classSymbols.exists(cls2 => - canComparePredefinedClasses(cls1, cls2))) + canComparePredefinedClasses(cls1, cls2)(using checkCtx))) formal.argTypes match case args @ (arg1 :: arg2 :: Nil) => diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index bb7ca762d9f2..9f27b6ffd886 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -548,7 +548,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer ref(ownType).withSpan(tree.span) case _ => tree.withType(ownType) - val tree2 = toNotNullTermRef(tree1, pt) + val tree2 = toNotNullTermRef(tree1, pt).tryToCastToCanEqualNull checkLegalValue(tree2, pt) tree2 @@ -646,7 +646,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def typeSelectOnTerm(using Context): Tree = val qual = typedExpr(tree.qualifier, shallowSelectionProto(tree.name, pt, this)) - typedSelect(tree, pt, qual).withSpan(tree.span).computeNullable() + val sel = typedSelect(tree, pt, qual).withSpan(tree.span).computeNullable() + if pt != AssignProto then sel.tryToCastToCanEqualNull else sel def javaSelectOnType(qual: Tree)(using Context) = // semantic name conversion for `O$` in java code @@ -3679,7 +3680,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } simplify(typed(etaExpand(tree, wtp, arity), pt), pt, locked) else if (wtp.paramInfos.isEmpty && isAutoApplied(tree.symbol)) - readaptSimplified(tpd.Apply(tree, Nil)) + val app = tpd.Apply(tree, Nil).tryToCastToCanEqualNull + readaptSimplified(app) else if (wtp.isImplicitMethod) err.typeMismatch(tree, pt) else diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 5b18b6c81fe9..89380494112b 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -249,6 +249,7 @@ class CompilationTests { compileFilesInDir("tests/explicit-nulls/pos-separate", explicitNullsOptions), compileFilesInDir("tests/explicit-nulls/pos-patmat", explicitNullsOptions and "-Xfatal-warnings"), compileFilesInDir("tests/explicit-nulls/unsafe-common", explicitNullsOptions and "-language:unsafeNulls"), + compileFilesInDir("tests/explicit-nulls/unsafe-java", explicitNullsOptions), compileFile("tests/explicit-nulls/pos-special/i14682.scala", explicitNullsOptions and "-Ysafe-init"), compileFile("tests/explicit-nulls/pos-special/i14947.scala", explicitNullsOptions and "-Ytest-pickler" and "-Xprint-types"), ) diff --git a/library/src/scala/annotation/CanEqualNull.scala b/library/src/scala/annotation/CanEqualNull.scala new file mode 100644 index 000000000000..fca1292b22af --- /dev/null +++ b/library/src/scala/annotation/CanEqualNull.scala @@ -0,0 +1,22 @@ +package scala.annotation + +/** An annotation makes reference types comparable to `null` in explicit nulls. + * `CanEqualNull` is a special refining annotation. An annotated type is equivalent to the original type. + * + * For example: + * ```scala + * val s1: String = ??? + * s1 == null // error + * val s2: String @CanEqualNull = ??? + * s2 == null // ok + * + * // String =:= String @CanEqualNull + * val s3: String = s2 + * val s4: String @CanEqualNull = s1 + * + * val ss: Array[String @CanEqualNull] = ??? + * ss.map(_ == null) + * ``` + */ +@experimental +final class CanEqualNull extends RefiningAnnotation diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 2be4861b4cc2..63b075aeb5bb 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -60,6 +60,11 @@ object language: @compileTimeOnly("`saferExceptions` can only be used at compile time in import statements") object saferExceptions + /** Experimental support for unsafe Java return in explicit nulls + */ + @compileTimeOnly("`unsafeJavaReturn` can only be used at compile time in import statements") + object unsafeJavaReturn + end experimental /** The deprecated object contains features that are no longer officially suypported in Scala. diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 846856adc2c8..f1f9c3230687 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -11,8 +11,6 @@ object MiMaFilters { ProblemFilters.exclude[MissingClassProblem]("scala.runtime.TupleMirror"), ProblemFilters.exclude[MissingTypesProblem]("scala.Tuple$package$EmptyTuple$"), // we made the empty tuple a case object ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Scala3RunTime.nnFail"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Scala3RunTime.nnFail"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals.getOffsetStatic"), // Added for #14780 ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.LazyVals.getOffsetStatic"), // Added for #14780 ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language.3.2-migration"), ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language.3.2"), @@ -28,6 +26,8 @@ object MiMaFilters { ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.typeRef"), ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.termRef"), ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#TypeTreeModule.ref"), + ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.unsafeJavaReturn"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$unsafeJavaReturn$"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.since"), ) diff --git a/tests/explicit-nulls/unsafe-java/JavaStatic.scala b/tests/explicit-nulls/unsafe-java/JavaStatic.scala new file mode 100644 index 000000000000..52693545d218 --- /dev/null +++ b/tests/explicit-nulls/unsafe-java/JavaStatic.scala @@ -0,0 +1,14 @@ +import language.experimental.unsafeJavaReturn + +import java.math.MathContext, MathContext._ + +val x: MathContext = DECIMAL32 +val y: MathContext = MathContext.DECIMAL32 + +import java.io.File + +val s: String = File.separator +import java.time.ZoneId + +val zids: java.util.Set[String] = ZoneId.getAvailableZoneIds +val zarr: Array[String] = ZoneId.getAvailableZoneIds.toArray(Array.empty[String | Null]) diff --git a/tests/explicit-nulls/unsafe-java/UnaryCall.scala b/tests/explicit-nulls/unsafe-java/UnaryCall.scala new file mode 100644 index 000000000000..553453516f9f --- /dev/null +++ b/tests/explicit-nulls/unsafe-java/UnaryCall.scala @@ -0,0 +1,11 @@ +import scala.language.experimental.unsafeJavaReturn + +import java.lang.reflect.Method + +def getMethods(f: String): List[Method] = + val clazz = Class.forName(f) + val methods = clazz.getMethods + if methods == null then List() + else methods.toList + +def getClass(o: AnyRef): Class[?] = o.getClass diff --git a/tests/explicit-nulls/unsafe-java/java-chain/J.java b/tests/explicit-nulls/unsafe-java/java-chain/J.java new file mode 100644 index 000000000000..91dda8438a2b --- /dev/null +++ b/tests/explicit-nulls/unsafe-java/java-chain/J.java @@ -0,0 +1,7 @@ +class J1 { + J2 getJ2() { return new J2(); } +} + +class J2 { + J1 getJ1() { return new J1(); } +} diff --git a/tests/explicit-nulls/unsafe-java/java-chain/S.scala b/tests/explicit-nulls/unsafe-java/java-chain/S.scala new file mode 100644 index 000000000000..778009f2f401 --- /dev/null +++ b/tests/explicit-nulls/unsafe-java/java-chain/S.scala @@ -0,0 +1,6 @@ +import scala.language.experimental.unsafeJavaReturn + +def f = { + val j: J2 = new J2() + j.getJ1().getJ2().getJ1().getJ2().getJ1().getJ2() +} diff --git a/tests/explicit-nulls/unsafe-java/java-class/J.java b/tests/explicit-nulls/unsafe-java/java-class/J.java new file mode 100644 index 000000000000..b90fffde3f22 --- /dev/null +++ b/tests/explicit-nulls/unsafe-java/java-class/J.java @@ -0,0 +1,42 @@ +import java.util.List; + +public class J { + + public int a; + + public String b; + + public String[] c; + + public int f1() { + return 0; + } + + public int[] f2() { + return null; + } + + public String g1() { + return null; + } + + public List g2() { + return null; + } + + public String[] g3() { + return null; + } + + public T h1() { + return null; + } + + public List h2() { + return null; + } + + public T[] h3() { + return null; + } +} diff --git a/tests/explicit-nulls/unsafe-java/java-class/S.scala b/tests/explicit-nulls/unsafe-java/java-class/S.scala new file mode 100644 index 000000000000..215ae4bd895d --- /dev/null +++ b/tests/explicit-nulls/unsafe-java/java-class/S.scala @@ -0,0 +1,52 @@ +import scala.language.experimental.unsafeJavaReturn + +import scala.annotation.CanEqualNull +import java.{util => ju} + +def test[T <: AnyRef](j: J) = { + val a: Int = j.a + + val b = j.b // it returns String @CanEqualNull + val b2: String = b + val b3: String @CanEqualNull = j.b + val b4: String = j.b + val bb = j.b == null // it's ok to compare String @CanEqualNull with Null + val btl = j.b.trim().length() // String @CanEqualNull is just String, unsafe selecting + + val c = j.c + val cl = c.length + val c2: Array[String] = c + val c3: Array[String @CanEqualNull] @CanEqualNull = j.c + val c4: Array[String] = j.c + val cml: Array[Int] = c.map(_.length()) + + val f1: Int = j.f1() + + val f21: Array[Int] @CanEqualNull = j.f2() + val f22: Array[Int] = j.f2() + val f2n = j.f2() == null + + val g11: String @CanEqualNull = j.g1() + val g12: String = j.g1() + val g1n = j.g1() == null + val g1tl = j.g1().trim().length() + + val g21: ju.List[String] @CanEqualNull = j.g2() + val g22: ju.List[String] = j.g2() + + val g31: Array[String @CanEqualNull] @CanEqualNull = j.g3() + val g32: Array[String] = j.g3() + val g3n = j.g3() == null + val g3m: Array[Boolean] = j.g3().map(_ == null) + + val h11: T @CanEqualNull = j.h1[T]() + val h12: T = j.h1[T]() + val h1n = j.h1[T]() == null + + val h21: ju.List[T] @CanEqualNull = j.h2[T]() + val h22: ju.List[T] = j.h2[T]() + + val h31: Array[T @CanEqualNull] @CanEqualNull = j.h3[T]() + val h32: Array[T] = j.h3[T]() + val h3m = j.h3[T]().map(_ == null) +} diff --git a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala index a65a50ab54ad..dc5879a72a8b 100644 --- a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala @@ -46,6 +46,12 @@ val experimentalDefinitionInLibrary = Set( "scala.annotation.newMain.Help$", "scala.annotation.newMain.Names", + //// New feature: CanEqualNull annotation + // Can be stabilized when language feature is stabilized. + // Needs user feedback. + // This annotation makes it possible to use `==` and `!=` to compare reference types and null in explicit nulls. + "scala.annotation.CanEqualNull", + //// New APIs: Mirror // Can be stabilized in 3.3.0 or later. "scala.deriving.Mirror$.fromProductTyped", // This API is a bit convoluted. We may need some more feedback before we can stabilize it.