diff --git a/community-build/src/scala/dotty/communitybuild/projects.scala b/community-build/src/scala/dotty/communitybuild/projects.scala index 52df4be6d645..3acc53d2cfdb 100644 --- a/community-build/src/scala/dotty/communitybuild/projects.scala +++ b/community-build/src/scala/dotty/communitybuild/projects.scala @@ -261,19 +261,21 @@ object projects: sbtTestCommand = "test", sbtDocCommand = "doc", requiresExperimental = true, + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val algebra = SbtCommunityProject( project = "algebra", sbtTestCommand = "coreJVM/compile", - sbtDocCommand = forceDoc("coreJVM") + sbtDocCommand = forceDoc("coreJVM"), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val scalacheck = SbtCommunityProject( project = "scalacheck", sbtTestCommand = "jvm/test;js/test", sbtPublishCommand = "jvm/publishLocal;js/publishLocal", - sbtDocCommand = forceDoc("jvm") + sbtDocCommand = forceDoc("jvm"), ) lazy val scalatest: SbtCommunityProject = SbtCommunityProject( @@ -298,7 +300,7 @@ object projects: // Problem parsing scalatest.dotty/target/scala-3.0.0-M2/src_managed/main/org/scalatest/concurrent/ConductorFixture.scala:[602..624..3843], documentation may not be generated. // dotty.tools.dotc.core.MissingType: dependencies = List(scalaXml), - testOnlyDependencies = () => List(scalatestplusJunit, scalatestplusTestNG) + testOnlyDependencies = () => List(scalatestplusJunit, scalatestplusTestNG), ) lazy val scalatestplusScalacheck = SbtCommunityProject( @@ -306,40 +308,42 @@ object projects: sbtTestCommand = "scalatestPlusScalaCheckJVM/test", sbtPublishCommand = "scalatestPlusScalaCheckJVM/publishLocal", sbtDocCommand = "scalatestPlusScalaCheckJVM/doc", - dependencies = List(scalatest, scalacheck) + dependencies = List(scalatest, scalacheck), ) lazy val scalatestplusJunit = SbtCommunityProject( project = "scalatestplus-junit", sbtTestCommand = "scalatestplus-junit/test", sbtPublishCommand = "scalatestplus-junit/publishLocal", - dependencies = List(scalatest) + dependencies = List(scalatest), ) lazy val scalatestplusTestNG = SbtCommunityProject( project = "scalatestplus-testng", sbtTestCommand = "test", sbtPublishCommand = "publishLocal", - dependencies = List(scalatest) + dependencies = List(scalatest), ) lazy val scalaXml = SbtCommunityProject( project = "scala-xml", sbtTestCommand = "xml/test", sbtPublishCommand = "xml/publishLocal", - sbtDocCommand = "xml/doc" + sbtDocCommand = "xml/doc", ) lazy val scalap = SbtCommunityProject( project = "scalap", sbtTestCommand = "scalap/compile", - sbtDocCommand = "scalap/doc" + sbtDocCommand = "scalap/doc", + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val betterfiles = SbtCommunityProject( project = "betterfiles", sbtTestCommand = "dotty-community-build/compile", - sbtDocCommand = ";core/doc ;akka/doc ;shapelessScanner/doc" + sbtDocCommand = ";core/doc ;akka/doc ;shapelessScanner/doc", + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val scalaPB = SbtCommunityProject( @@ -349,14 +353,15 @@ object projects: // module class ScalaPbCodeGenerator$ has non-class parent: TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),module protocbridge),ProtocCodeGenerator) // Also it seems that we do not handle correctly aggreagation projects // sbtDocCommand = "dotty-community-build/doc" - sbtDocCommand = forceDoc("scalapbc", "grpcRuntime","runtimeJVM", "compilerPlugin") + sbtDocCommand = forceDoc("scalapbc", "grpcRuntime","runtimeJVM", "compilerPlugin"), ) lazy val minitest = SbtCommunityProject( project = "minitest", sbtTestCommand = "test", sbtDocCommand = aggregateDoc("lawsJVM")("minitestJVM"), - dependencies = List(scalacheck) + dependencies = List(scalacheck), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val fastparse = SbtCommunityProject( @@ -364,6 +369,7 @@ object projects: sbtTestCommand = "dotty-community-build/compile;dotty-community-build/test:compile", // Problem parsing perftests/bench2/src/perftests/PythonParse.scala:[0..18..694] // sbtDocCommand = "dotty-community-build/doc" + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val stdLib213 = SbtCommunityProject( @@ -379,13 +385,16 @@ object projects: project = "shapeless", sbtTestCommand = "test", sbtDocCommand = forceDoc("typeable", "deriving", "data"), - scalacOptions = Nil // disable -Ysafe-init, due to -Xfatal-warnings + // disable -Ysafe-init, due to -Xfatal-warnings + // shapeless already uses -Yexplicit-nulls itself + scalacOptions = Nil ) lazy val xmlInterpolator = SbtCommunityProject( project = "xml-interpolator", sbtTestCommand = "test", sbtDocCommand = "doc", // Again we've got problem with extensions + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val effpi = SbtCommunityProject( @@ -414,14 +423,15 @@ object projects: project = "sconfig", sbtTestCommand = "sconfigJVM/test", sbtDocCommand = "sconfigJVM/doc", - dependencies = List(scalaCollectionCompat) + dependencies = List(scalaCollectionCompat), ) lazy val zio = SbtCommunityProject( project = "zio", sbtTestCommand = "testJVMDotty", sbtDocCommand = forceDoc("coreJVM"), - dependencies = List(izumiReflect) + dependencies = List(izumiReflect), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val munit = SbtCommunityProject( @@ -429,7 +439,8 @@ object projects: sbtTestCommand = "testsJVM/test;testsJS/test;", sbtPublishCommand = "munitJVM/publishLocal; munitJS/publishLocal; munitScalacheckJVM/publishLocal; munitScalacheckJS/publishLocal; junit/publishLocal", sbtDocCommand = "junit/doc; munitJVM/doc", - dependencies = List(scalacheck) + dependencies = List(scalacheck), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val scodecBits = SbtCommunityProject( @@ -438,6 +449,7 @@ object projects: sbtPublishCommand = "coreJVM/publishLocal;coreJS/publishLocal", sbtDocCommand = "coreJVM/doc", dependencies = List(munit), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val scodec = SbtCommunityProject( @@ -446,12 +458,14 @@ object projects: // Adds package sbtDocCommand = "coreJVM/doc", dependencies = List(munit, scodecBits), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val scalaParserCombinators = SbtCommunityProject( project = "scala-parser-combinators", sbtTestCommand = "parserCombinatorsJVM/test", sbtDocCommand = forceDoc("parserCombinatorsJVM"), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val dottyCpsAsync = SbtCommunityProject( @@ -478,13 +492,14 @@ object projects: // [error] class scalaz.iteratee.Iteratee cannot be unpickled because no class file was found sbtDocCommand = forceDoc("effectJVM"), - dependencies = List(scalacheck) + dependencies = List(scalacheck), ) lazy val endpoints4s = SbtCommunityProject( project = "endpoints4s", sbtTestCommand = "json-schemaJVM/compile;algebraJVM/compile;openapiJVM/compile;http4s-server/compile;http4s-client/compile;play-server/compile;play-client/compile;akka-http-server/compile;akka-http-client/compile", sbtDocCommand = ";json-schemaJVM/doc ;algebraJVM/doc; openapiJVM/doc; http4s-server/doc ;http4s-client/doc ;play-server/doc ;play-client/doc ;akka-http-server/doc ;akka-http-client/doc", + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val catsEffect3 = SbtCommunityProject( @@ -492,20 +507,22 @@ object projects: sbtTestCommand = "test", sbtPublishCommand = "publishLocal", sbtDocCommand = ";coreJVM/doc ;lawsJVM/doc ;kernelJVM/doc", - dependencies = List(cats, coop, disciplineSpecs2, scalacheck) + dependencies = List(cats, coop, disciplineSpecs2, scalacheck), ) lazy val scalaParallelCollections = SbtCommunityProject( project = "scala-parallel-collections", sbtTestCommand = "test", sbtDocCommand = forceDoc("core"), - dependencies = List(scalacheck) + dependencies = List(scalacheck), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val scalaCollectionCompat = SbtCommunityProject( project = "scala-collection-compat", sbtTestCommand = "compat30/test", sbtPublishCommand = "compat30/publishLocal", + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val scalaJava8Compat = SbtCommunityProject( @@ -520,21 +537,24 @@ object projects: project = "verify", sbtTestCommand = "verifyJVM/test", sbtDocCommand = "verifyJVM/doc", - scalacOptions = SbtCommunityProject.scalacOptions.filter(_ != "-Xcheck-macros") // TODO enable -Xcheck-macros + scalacOptions = SbtCommunityProject.scalacOptions.filter(_ != "-Xcheck-macros") :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", + // TODO enable -Xcheck-macros ) lazy val discipline = SbtCommunityProject( project = "discipline", sbtTestCommand = "coreJVM/test;coreJS/test", sbtPublishCommand = "set every credentials := Nil;coreJVM/publishLocal;coreJS/publishLocal", - dependencies = List(scalacheck) + dependencies = List(scalacheck), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" , ) lazy val disciplineMunit = SbtCommunityProject( project = "discipline-munit", sbtTestCommand = "coreJVM/test;coreJS/test", sbtPublishCommand = "coreJVM/publishLocal;coreJS/publishLocal", - dependencies = List(discipline, munit) + dependencies = List(discipline, munit), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val disciplineSpecs2 = SbtCommunityProject( @@ -542,13 +562,14 @@ object projects: sbtTestCommand = "test", sbtPublishCommand = "coreJVM/publishLocal;coreJS/publishLocal", dependencies = List(discipline), - scalacOptions = SbtCommunityProject.scalacOptions.filter(_ != "-Ysafe-init") + scalacOptions = SbtCommunityProject.scalacOptions.filter(_ != "-Ysafe-init") :+ "-Yexplicit-nulls" , ) lazy val simulacrumScalafixAnnotations = SbtCommunityProject( project = "simulacrum-scalafix", sbtTestCommand = "annotation/test:compile;annotationJS/test:compile", sbtPublishCommand = "annotation/publishLocal;annotationJS/publishLocal", + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" , ) lazy val cats = SbtCommunityProject( @@ -564,14 +585,16 @@ object projects: project = "cats-mtl", sbtTestCommand = "testsJVM/test;testsJS/test", sbtPublishCommand = "coreJVM/publishLocal;coreJS/publishLocal;lawsJVM/publishLocal;lawsJS/publishLocal", - dependencies = List(cats, disciplineMunit) + dependencies = List(cats, disciplineMunit), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val coop = SbtCommunityProject( project = "coop", sbtTestCommand = "test", sbtPublishCommand = "coreJVM/publishLocal;coreJS/publishLocal", - dependencies = List(cats, catsMtl) + dependencies = List(cats, catsMtl), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) // 'Sciss/Lucre' with its dependencies: @@ -581,6 +604,7 @@ object projects: sbtTestCommand = "rootJVM/test", sbtPublishCommand = "rootJVM/publishLocal", dependencies = List(scalatest), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" , ) lazy val scissFingerTree = SbtCommunityProject( @@ -588,12 +612,14 @@ object projects: sbtTestCommand = "rootJVM/test", sbtPublishCommand = "rootJVM/publishLocal", dependencies = List(scalatest), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" , ) lazy val scissLog = SbtCommunityProject( project = "Log", sbtTestCommand = "rootJVM/test", sbtPublishCommand = "rootJVM/publishLocal", + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val scissModel = SbtCommunityProject( @@ -601,6 +627,7 @@ object projects: sbtTestCommand = "rootJVM/test", sbtPublishCommand = "rootJVM/publishLocal", dependencies = List(scalatest), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" , ) lazy val scissNumbers = SbtCommunityProject( @@ -608,6 +635,7 @@ object projects: sbtTestCommand = "rootJVM/test", sbtPublishCommand = "rootJVM/publishLocal", dependencies = List(scalatest), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" , ) lazy val scissSerial = SbtCommunityProject( @@ -615,6 +643,7 @@ object projects: sbtTestCommand = "rootJVM/test", sbtPublishCommand = "rootJVM/publishLocal", dependencies = List(scalatest), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val scissAsyncFile = SbtCommunityProject( @@ -622,6 +651,7 @@ object projects: sbtTestCommand = "rootJVM/test", sbtPublishCommand = "rootJVM/publishLocal", dependencies = List(scissLog, scalatest), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val scissSpan = SbtCommunityProject( @@ -629,6 +659,7 @@ object projects: sbtTestCommand = "rootJVM/test", sbtPublishCommand = "rootJVM/publishLocal", dependencies = List(scissSerial, scalatest), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" , ) lazy val scalaSTM = SbtCommunityProject( @@ -636,6 +667,7 @@ object projects: sbtTestCommand = "rootJVM/test", sbtPublishCommand = "rootJVM/publishLocal", dependencies = List(scalatestplusJunit), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val scissLucre = SbtCommunityProject( @@ -644,13 +676,15 @@ object projects: extraSbtArgs = List("-Dde.sciss.lucre.ShortTests=true"), sbtPublishCommand = "adjunctJVM/publishLocal;baseJVM/publishLocal;confluentJVM/publishLocal;coreJVM/publishLocal;dataJVM/publishLocal;expr0JVM/publishLocal;expr1JVM/publishLocal;exprJVM/publishLocal;geomJVM/publishLocal;lucre-bdb/publishLocal", dependencies = List(scalaSTM, scissAsyncFile, scissEqual, scissFingerTree, scissLog, scissModel, scissNumbers, scissSerial, scissSpan, scalatest), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val izumiReflect = SbtCommunityProject( project = "izumi-reflect", sbtTestCommand = "test", sbtPublishCommand = "publishLocal", - dependencies = List(scalatest) + dependencies = List(scalatest), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val perspective = SbtCommunityProject( @@ -658,20 +692,23 @@ object projects: // No library with easy typeclasses to verify data against exist for Dotty, so no tests yet // Until then I guess this mainly serves to check that it still compiles at all sbtTestCommand = "dottyPerspectiveExamples/compile", - dependencies = List(cats) + dependencies = List(cats), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val akka = SbtCommunityProject( project = "akka", extraSbtArgs = List(s"-Dakka.build.scalaVersion=$compilerVersion"), sbtTestCommand = "set every targetSystemJdk := true; akka-actor-tests/Test/compile", - dependencies = List(scalatest, scalatestplusJunit, scalatestplusScalacheck) + dependencies = List(scalatest, scalatestplusJunit, scalatestplusScalacheck), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" , ) lazy val monocle = SbtCommunityProject( project = "Monocle", sbtTestCommand = "coreJVM/test; macrosJVM/test; testJVM/test", - dependencies = List(cats, munit, discipline, disciplineMunit) + dependencies = List(cats, munit, discipline, disciplineMunit), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val protoquill = SbtCommunityProject( @@ -679,14 +716,15 @@ object projects: sbtTestCommand = "test", sbtPublishCommand = "publishLocal", dependencies = List(), // TODO add scalatest and pprint (see protoquill/build.sbt) - scalacOptions = List("-language:implicitConversions"), // disabled -Ysafe-init, due to bug in macro + scalacOptions = List("-language:implicitConversions") :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", + // disabled -Ysafe-init, due to bug in macro ) lazy val onnxScala = SbtCommunityProject( project = "onnx-scala", sbtTestCommand = "test", sbtPublishCommand = "publishLocal", - dependencies = List(scalatest) + dependencies = List(scalatest), ) lazy val playJson = SbtCommunityProject( @@ -694,55 +732,61 @@ object projects: sbtTestCommand = "test", sbtPublishCommand = "publishLocal", dependencies = List(scalatest, scalatestplusScalacheck), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val munitCatsEffect = SbtCommunityProject( project = "munit-cats-effect", sbtTestCommand = "ce3JVM/test; ce3JS/test", sbtPublishCommand = "ce3JVM/publishLocal; ce3JS/publishLocal", - dependencies = List(munit, catsEffect3) + dependencies = List(munit, catsEffect3), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val scalacheckEffect = SbtCommunityProject( project = "scalacheck-effect", sbtTestCommand = "test", sbtPublishCommand = "publishLocal", - dependencies = List(cats, catsEffect3, munit, scalacheck) + dependencies = List(cats, catsEffect3, munit, scalacheck), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val fs2 = SbtCommunityProject( project = "fs2", sbtTestCommand = "coreJVM/test; coreJS/test", // io/test requires JDK9+ sbtPublishCommand = "coreJVM/publishLocal; coreJS/publishLocal", - dependencies = List(cats, catsEffect3, munitCatsEffect, scalacheckEffect, scodecBits) + dependencies = List(cats, catsEffect3, munitCatsEffect, scalacheckEffect, scodecBits), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val libretto = SbtCommunityProject( project = "libretto", sbtTestCommand = "core/test; examples/compile", sbtPublishCommand = "core/publishLocal; examples/publishLocal", - dependencies = List(scalatest) + dependencies = List(scalatest), ) lazy val jacksonModuleScala = SbtCommunityProject( project = "jackson-module-scala", sbtTestCommand = "test", sbtPublishCommand = "publishLocal", - dependencies = List(scalaJava8Compat, scalatest) + dependencies = List(scalaJava8Compat, scalatest), ) lazy val specs2 = SbtCommunityProject( project = "specs2", sbtTestCommand = "core/testOnly -- exclude ci", sbtPublishCommand = "core/publishLocal", - dependencies = List() + dependencies = List(), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) lazy val spire = SbtCommunityProject( project = "spire", sbtTestCommand = "test", sbtPublishCommand = "publishLocal", - dependencies = List(cats, disciplineMunit) + dependencies = List(cats, disciplineMunit), + scalacOptions = SbtCommunityProject.scalacOptions :+ "-Yexplicit-nulls" :+ "-language:unsafeNulls", ) end projects diff --git a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala index d0799ca89d24..5c6d90fac2f8 100644 --- a/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala +++ b/compiler/src/dotty/tools/dotc/core/NullOpsDecorator.scala @@ -9,6 +9,43 @@ import Types._ /** Defines operations on nullable types and tree. */ object NullOpsDecorator: + private class StripNullsMap(isDeep: Boolean)(using Context) extends TypeMap: + def strip(tp: Type): Type = tp match + case tp @ OrType(lhs, rhs) => + val llhs = this(lhs) + val rrhs = this(rhs) + if rrhs.isNullType then llhs + else if llhs.isNullType then rrhs + else derivedOrType(tp, llhs, rrhs) + case tp @ AndType(tp1, tp2) => + // We cannot `tp.derivedAndType(strip(tp1), strip(tp2))` directly, + // since `stripNull((A | Null) & B)` would produce the wrong + // result `(A & B) | Null`. + val tp1s = this(tp1) + val tp2s = this(tp2) + if isDeep || (tp1s ne tp1) && (tp2s ne tp2) then + derivedAndType(tp, tp1s, tp2s) + else tp + case tp: TypeBounds => + mapOver(tp) + case _ => tp + + def stripOver(tp: Type): Type = tp match + case appTp @ AppliedType(tycon, targs) => + derivedAppliedType(appTp, tycon, targs.map(this)) + case ptp: PolyType => + derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType)) + case mtp: MethodType => + mapOver(mtp) + case _ => strip(tp) + + override def apply(tp: Type): Type = + val tpw = tp.widenDealias + val tpws = if isDeep then stripOver(tpw) else strip(tpw) + if tpws ne tpw then tpws else tp + + end StripNullsMap + extension (self: Type) /** Syntactically strips the nullability from this type. * If the type is `T1 | ... | Tn`, and `Ti` references to `Null`, @@ -17,31 +54,7 @@ object NullOpsDecorator: * The type will not be changed if explicit-nulls is not enabled. */ def stripNull(using Context): Type = { - def strip(tp: Type): Type = - val tpWiden = tp.widenDealias - val tpStripped = tpWiden match { - case tp @ OrType(lhs, rhs) => - val llhs = strip(lhs) - val rrhs = strip(rhs) - if rrhs.isNullType then llhs - else if llhs.isNullType then rrhs - else tp.derivedOrType(llhs, rrhs) - case tp @ AndType(tp1, tp2) => - // We cannot `tp.derivedAndType(strip(tp1), strip(tp2))` directly, - // since `stripNull((A | Null) & B)` would produce the wrong - // result `(A & B) | Null`. - val tp1s = strip(tp1) - val tp2s = strip(tp2) - if (tp1s ne tp1) && (tp2s ne tp2) then - tp.derivedAndType(tp1s, tp2s) - else tp - case tp @ TypeBounds(lo, hi) => - tp.derivedTypeBounds(strip(lo), strip(hi)) - case tp => tp - } - if tpStripped ne tpWiden then tpStripped else tp - - if ctx.explicitNulls then strip(self) else self + if ctx.explicitNulls then new StripNullsMap(false)(self) else self } /** Is self (after widening and dealiasing) a type of the form `T | Null`? */ @@ -49,6 +62,14 @@ object NullOpsDecorator: val stripped = self.stripNull stripped ne self } + + /** Strips nulls from this type deeply. + * Compaired to `stripNull`, `stripNullsDeep` will apply `stripNull` to + * each member of function types as well. + */ + def stripNullsDeep(using Context): Type = + if ctx.explicitNulls then new StripNullsMap(true)(self) else self + end extension import ast.tpd._ diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index b1007a32f5c4..e18b44554d01 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1112,8 +1112,10 @@ object Types { */ def matches(that: Type)(using Context): Boolean = { record("matches") + val thisTp1 = this.stripNullsDeep + val thatTp1 = that.stripNullsDeep withoutMode(Mode.SafeNulls)( - TypeComparer.matchesType(this, that, relaxed = !ctx.phase.erasedTypes)) + TypeComparer.matchesType(thisTp1, thatTp1, relaxed = !ctx.phase.erasedTypes)) } /** This is the same as `matches` except that it also matches => T with T and diff --git a/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala b/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala index 437dfea9f156..7d7b01b9927e 100644 --- a/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala +++ b/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala @@ -5,6 +5,7 @@ package transform import core._ import Flags._, Symbols._, Contexts._, Scopes._, Decorators._, Types.Type import NameKinds.DefaultGetterName +import NullOpsDecorator._ import collection.mutable import collection.immutable.BitSet import scala.annotation.tailrec @@ -215,15 +216,20 @@ object OverridingPairs: } ) else - // releaxed override check for explicit nulls if one of the symbols is Java defined, - // force `Null` being a subtype of reference types during override checking - val relaxedCtxForNulls = + def matchNullaryLoosely = member.matchNullaryLoosely || other.matchNullaryLoosely || fallBack + // default getters are not checked for compatibility + member.name.is(DefaultGetterName) || { if ctx.explicitNulls && (member.is(JavaDefined) || other.is(JavaDefined)) then - ctx.retractMode(Mode.SafeNulls) - else ctx - member.name.is(DefaultGetterName) // default getters are not checked for compatibility - || memberTp.overrides(otherTp, - member.matchNullaryLoosely || other.matchNullaryLoosely || fallBack - )(using relaxedCtxForNulls) + // relaxed override check for explicit nulls if one of the symbols is Java defined, + // force `Null` being a subtype of reference types during override checking. + // `stripNullsDeep` is used here because we may encounter type parameters + // (`T | Null` is not a subtype of `T` even if we retract Mode.SafeNulls). + val memberTp1 = memberTp.stripNullsDeep + val otherTp1 = otherTp.stripNullsDeep + withoutMode(Mode.SafeNulls)( + memberTp1.overrides(otherTp1, matchNullaryLoosely)) + else + memberTp.overrides(otherTp, matchNullaryLoosely) + } end OverridingPairs diff --git a/compiler/src/dotty/tools/dotc/transform/ResolveSuper.scala b/compiler/src/dotty/tools/dotc/transform/ResolveSuper.scala index 40c860bf3bdc..8b5b6dbdd50a 100644 --- a/compiler/src/dotty/tools/dotc/transform/ResolveSuper.scala +++ b/compiler/src/dotty/tools/dotc/transform/ResolveSuper.scala @@ -13,6 +13,7 @@ import Names._ import StdNames._ import NameOps._ import NameKinds._ +import NullOpsDecorator._ import ResolveSuper._ import reporting.IllegalSuperAccessor @@ -110,11 +111,13 @@ object ResolveSuper { // Since the super class can be Java defined, // we use releaxed overriding check for explicit nulls if one of the symbols is Java defined. // This forces `Null` being a subtype of reference types during override checking. - val relaxedCtxForNulls = - if ctx.explicitNulls && (sym.is(JavaDefined) || acc.is(JavaDefined)) then - ctx.retractMode(Mode.SafeNulls) - else ctx - if (!(otherTp.overrides(accTp, matchLoosely = true)(using relaxedCtxForNulls))) + val overridesSuper = if ctx.explicitNulls && (sym.is(JavaDefined) || acc.is(JavaDefined)) then + val otherTp1 = otherTp.stripNullsDeep + val accTp1 = accTp.stripNullsDeep + withoutMode(Mode.SafeNulls)(otherTp1.overrides(accTp1, matchLoosely = true)) + else + otherTp.overrides(accTp, matchLoosely = true) + if !overridesSuper then report.error(IllegalSuperAccessor(base, memberName, targetName, acc, accTp, other.symbol, otherTp), base.srcPos) bcs = bcs.tail diff --git a/tests/explicit-nulls/neg/opaque-nullable.scala b/tests/explicit-nulls/neg/opaque-nullable.scala new file mode 100644 index 000000000000..1d3a22249d29 --- /dev/null +++ b/tests/explicit-nulls/neg/opaque-nullable.scala @@ -0,0 +1,47 @@ +// Unboxed option type using unions + null + opaque. +// Relies on the fact that Null is not a subtype of AnyRef. +// Test suggested by Sébastien Doeraene. + +object Nullables { + opaque type Nullable[+A <: AnyRef] = A | Null // disjoint by construction! + + object Nullable: + def apply[A <: AnyRef](x: A | Null): Nullable[A] = x + + def some[A <: AnyRef](x: A): Nullable[A] = x + def none: Nullable[Nothing] = null + + extension [A <: AnyRef](x: Nullable[A]) + def isEmpty: Boolean = x == null + def get: A | Null = x + + extension [A <: AnyRef, B <: AnyRef](x: Nullable[A]) + def flatMap(f: A => Nullable[B]): Nullable[B] = + if (x == null) null + else f(x) + + def map(f: A => B): Nullable[B] = x.flatMap(f) + + def test1 = + val s1: Nullable[String] = Nullable("hello") + val s2: Nullable[String] = "world" + val s3: Nullable[String] = Nullable.none + val s4: Nullable[String] = null + + s1.isEmpty + s1.flatMap((x) => true) + + assert(s2 != null) +} + +def test2 = + import Nullables._ + + val s1: Nullable[String] = Nullable("hello") + val s2: Nullable[String] = Nullable.none + val s3: Nullable[String] = null // error: don't leak nullable union + + s1.isEmpty + s1.flatMap((x) => Nullable(true)) + + assert(s2 == null) // error diff --git a/tests/explicit-nulls/pos/opaque-nullable.scala b/tests/explicit-nulls/pos/opaque-nullable.scala deleted file mode 100644 index a7f626054ad3..000000000000 --- a/tests/explicit-nulls/pos/opaque-nullable.scala +++ /dev/null @@ -1,26 +0,0 @@ -// Unboxed option type using unions + null + opaque. -// Relies on the fact that Null is not a subtype of AnyRef. -// Test suggested by Sébastien Doeraene. - -opaque type Nullable[+A <: AnyRef] = A | Null // disjoint by construction! - -object Nullable { - def apply[A <: AnyRef](x: A | Null): Nullable[A] = x - - def some[A <: AnyRef](x: A): Nullable[A] = x - def none: Nullable[Nothing] = null - - extension [A <: AnyRef](x: Nullable[A]) - def isEmpty: Boolean = x == null - - extension [A <: AnyRef, B <: AnyRef](x: Nullable[A]) - def flatMap(f: A => Nullable[B]): Nullable[B] = - if (x == null) null - else f(x) - - val s1: Nullable[String] = "hello" - val s2: Nullable[String] = null - - s1.isEmpty - s1.flatMap((x) => true) -} diff --git a/tests/explicit-nulls/pos/override-type-params.scala b/tests/explicit-nulls/pos/override-type-params.scala new file mode 100644 index 000000000000..7f59409a4c3c --- /dev/null +++ b/tests/explicit-nulls/pos/override-type-params.scala @@ -0,0 +1,18 @@ +// Testing relaxed overriding check for explicit nulls. +// The relaxed check is only enabled if one of the members is Java defined. + +import java.util.Comparator + +class C1[T <: AnyRef] extends Ordering[T]: + override def compare(o1: T, o2: T): Int = 0 + +// The following overriding is not allowed, because `compare` +// has already been declared in Scala class `Ordering`. +// class C2[T <: AnyRef] extends Ordering[T]: +// override def compare(o1: T | Null, o2: T | Null): Int = 0 + +class D1[T <: AnyRef] extends Comparator[T]: + override def compare(o1: T, o2: T): Int = 0 + +class D2[T <: AnyRef] extends Comparator[T]: + override def compare(o1: T | Null, o2: T | Null): Int = 0