diff --git a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala index 4025446c8cd8..0322e56b2f41 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala @@ -59,6 +59,8 @@ final class JSDefinitions()(using Context) { @threadUnsafe lazy val PseudoUnion_fromTypeConstructorR = PseudoUnionModule.requiredMethodRef("fromTypeConstructor") def PseudoUnion_fromTypeConstructor(using Context) = PseudoUnion_fromTypeConstructorR.symbol + @threadUnsafe lazy val UnionOpsModuleRef = requiredModuleRef("scala.scalajs.js.internal.UnitOps") + @threadUnsafe lazy val JSArrayType: TypeRef = requiredClassRef("scala.scalajs.js.Array") def JSArrayClass(using Context) = JSArrayType.symbol.asClass @threadUnsafe lazy val JSDynamicType: TypeRef = requiredClassRef("scala.scalajs.js.Dynamic") diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index ccf344fa8b49..707ed053f02a 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -6,6 +6,7 @@ import Symbols._, Types._, Contexts._, Flags._, Names._, StdNames._, Phases._ import Flags.JavaDefined import Uniques.unique import TypeOps.makePackageObjPrefixExplicit +import backend.sjs.JSDefinitions import transform.ExplicitOuter._ import transform.ValueClasses._ import transform.TypeUtils._ @@ -142,29 +143,31 @@ object TypeErasure { } } - private def erasureIdx(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean) = + private def erasureIdx(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, isSymbol: Boolean, wildcardOK: Boolean) = extension (b: Boolean) def toInt = if b then 1 else 0 wildcardOK.toInt - + (isConstructor.toInt << 1) - + (semiEraseVCs.toInt << 2) - + (sourceLanguage.ordinal << 3) + + (isSymbol.toInt << 1) + + (isConstructor.toInt << 2) + + (semiEraseVCs.toInt << 3) + + (sourceLanguage.ordinal << 4) - private val erasures = new Array[TypeErasure](1 << (SourceLanguage.bits + 3)) + private val erasures = new Array[TypeErasure](1 << (SourceLanguage.bits + 4)) for sourceLanguage <- SourceLanguage.values semiEraseVCs <- List(false, true) isConstructor <- List(false, true) + isSymbol <- List(false, true) wildcardOK <- List(false, true) do - erasures(erasureIdx(sourceLanguage, semiEraseVCs, isConstructor, wildcardOK)) = - new TypeErasure(sourceLanguage, semiEraseVCs, isConstructor, wildcardOK) + erasures(erasureIdx(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, wildcardOK)) = + new TypeErasure(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, wildcardOK) /** Produces an erasure function. See the documentation of the class [[TypeErasure]] * for a description of each parameter. */ - private def erasureFn(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean): TypeErasure = - erasures(erasureIdx(sourceLanguage, semiEraseVCs, isConstructor, wildcardOK)) + private def erasureFn(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, isSymbol: Boolean, wildcardOK: Boolean): TypeErasure = + erasures(erasureIdx(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, wildcardOK)) /** The current context with a phase no later than erasure */ def preErasureCtx(using Context) = @@ -175,7 +178,7 @@ object TypeErasure { * @param tp The type to erase. */ def erasure(tp: Type)(using Context): Type = - erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = false, isConstructor = false, wildcardOK = false)(tp)(using preErasureCtx) + erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = false, isConstructor = false, isSymbol = false, wildcardOK = false)(tp)(using preErasureCtx) /** The value class erasure of a Scala type, where value classes are semi-erased to * ErasedValueType (they will be fully erased in [[ElimErasedValueType]]). @@ -183,11 +186,11 @@ object TypeErasure { * @param tp The type to erase. */ def valueErasure(tp: Type)(using Context): Type = - erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = true, isConstructor = false, wildcardOK = false)(tp)(using preErasureCtx) + erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = true, isConstructor = false, isSymbol = false, wildcardOK = false)(tp)(using preErasureCtx) /** The erasure that Scala 2 would use for this type. */ def scala2Erasure(tp: Type)(using Context): Type = - erasureFn(sourceLanguage = SourceLanguage.Scala2, semiEraseVCs = true, isConstructor = false, wildcardOK = false)(tp)(using preErasureCtx) + erasureFn(sourceLanguage = SourceLanguage.Scala2, semiEraseVCs = true, isConstructor = false, isSymbol = false, wildcardOK = false)(tp)(using preErasureCtx) /** Like value class erasure, but value classes erase to their underlying type erasure */ def fullErasure(tp: Type)(using Context): Type = @@ -197,7 +200,7 @@ object TypeErasure { def sigName(tp: Type, sourceLanguage: SourceLanguage)(using Context): TypeName = { val normTp = tp.translateFromRepeated(toArray = sourceLanguage.isJava) - val erase = erasureFn(sourceLanguage, semiEraseVCs = !sourceLanguage.isJava, isConstructor = false, wildcardOK = true) + val erase = erasureFn(sourceLanguage, semiEraseVCs = !sourceLanguage.isJava, isConstructor = false, isSymbol = false, wildcardOK = true) erase.sigName(normTp)(using preErasureCtx) } @@ -227,7 +230,7 @@ object TypeErasure { def transformInfo(sym: Symbol, tp: Type)(using Context): Type = { val sourceLanguage = SourceLanguage(sym) val semiEraseVCs = !sourceLanguage.isJava // Java sees our value classes as regular classes. - val erase = erasureFn(sourceLanguage, semiEraseVCs, sym.isConstructor, wildcardOK = false) + val erase = erasureFn(sourceLanguage, semiEraseVCs, sym.isConstructor, isSymbol = true, wildcardOK = false) def eraseParamBounds(tp: PolyType): Type = tp.derivedLambdaType( @@ -446,10 +449,11 @@ import TypeErasure._ * (they will be fully erased in [[ElimErasedValueType]]). * If false, they are erased like normal classes. * @param isConstructor Argument forms part of the type of a constructor + * @param isSymbol If true, the type being erased is the info of a symbol. * @param wildcardOK Wildcards are acceptable (true when using the erasure * for computing a signature name). */ -class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean) { +class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, isSymbol: Boolean, wildcardOK: Boolean) { /** The erasure |T| of a type T. This is: * @@ -520,10 +524,22 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst else erasedGlb(this(tp1), this(tp2), isJava = sourceLanguage.isJava) case OrType(tp1, tp2) => - TypeComparer.orType(this(tp1), this(tp2), isErased = true) + if isSymbol && sourceLanguage.isScala2 && ctx.settings.scalajs.value then + // In Scala2Unpickler we unpickle Scala.js pseudo-unions as if they were + // real unions, but we must still erase them as Scala 2 would to emit + // the correct signatures in SJSIR. + // We only do this when `isSymbol` is true since in other situations we + // cannot distinguish a Scala.js pseudo-union from a Scala 3 union that + // has been substituted into a Scala 2 type (e.g., via `asSeenFrom`), + // erasing these unions as if they were pseudo-unions could have an + // impact on overriding relationships so it's best to leave them + // alone (and this doesn't impact the SJSIR we generate). + JSDefinitions.jsdefn.PseudoUnionType + else + TypeComparer.orType(this(tp1), this(tp2), isErased = true) case tp: MethodType => def paramErasure(tpToErase: Type) = - erasureFn(sourceLanguage, semiEraseVCs, isConstructor, wildcardOK)(tpToErase) + erasureFn(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, wildcardOK)(tpToErase) val (names, formals0) = if (tp.isErasedMethod) (Nil, Nil) else (tp.paramNames, tp.paramInfos) val formals = formals0.mapConserve(paramErasure) eraseResult(tp.resultType) match { @@ -567,7 +583,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst val defn.ArrayOf(elemtp) = tp if (classify(elemtp).derivesFrom(defn.NullClass)) JavaArrayType(defn.ObjectType) else if (isUnboundedGeneric(elemtp) && !sourceLanguage.isJava) defn.ObjectType - else JavaArrayType(erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, wildcardOK)(elemtp)) + else JavaArrayType(erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, wildcardOK)(elemtp)) } private def erasePair(tp: Type)(using Context): Type = { @@ -608,7 +624,9 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst val genericUnderlying = unbox.info.resultType val underlying = tp.select(unbox).widen.resultType - val erasedUnderlying = erasure(underlying) + // The underlying part of an ErasedValueType cannot be an ErasedValueType itself + val erase = erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, wildcardOK) + val erasedUnderlying = erase(underlying) // Ideally, we would just use `erasedUnderlying` as the erasure of `tp`, but to // be binary-compatible with Scala 2 we need two special cases for polymorphic @@ -646,7 +664,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst // correctly (see SIP-15 and [[Erasure.Boxing.adaptToType]]), so the result type of a // constructor method should not be semi-erased. if semiEraseVCs && isConstructor && !tp.isInstanceOf[MethodOrPoly] then - erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, wildcardOK).eraseResult(tp) + erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, wildcardOK).eraseResult(tp) else tp match case tp: TypeRef => val sym = tp.symbol diff --git a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala index 7738bda6a800..54a7ad91e434 100644 --- a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala @@ -13,6 +13,7 @@ import NameKinds.{Scala2MethodNameKinds, SuperAccessorName, ExpandedName} import util.Spans._ import dotty.tools.dotc.ast.{tpd, untpd}, ast.tpd._ import ast.untpd.Modifiers +import backend.sjs.JSDefinitions import printing.Texts._ import printing.Printer import io.AbstractFile @@ -675,6 +676,10 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas def removeSingleton(tp: Type): Type = if (tp isRef defn.SingletonClass) defn.AnyType else tp + def mapArg(arg: Type) = arg match { + case arg: TypeRef if isBound(arg) => arg.symbol.info + case _ => arg + } def elim(tp: Type): Type = tp match { case tp @ RefinedType(parent, name, rinfo) => val parent1 = elim(tp.parent) @@ -690,12 +695,11 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas } case tp @ AppliedType(tycon, args) => val tycon1 = tycon.safeDealias - def mapArg(arg: Type) = arg match { - case arg: TypeRef if isBound(arg) => arg.symbol.info - case _ => arg - } if (tycon1 ne tycon) elim(tycon1.appliedTo(args)) else tp.derivedAppliedType(tycon, args.map(mapArg)) + case tp: AndOrType => + // scalajs.js.|.UnionOps has a type parameter upper-bounded by `_ | _` + tp.derivedAndOrType(mapArg(tp.tp1).bounds.hi, mapArg(tp.tp2).bounds.hi) case _ => tp } @@ -777,6 +781,12 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas val tycon = select(pre, sym) val args = until(end, () => readTypeRef()) if (sym == defn.ByNameParamClass2x) ExprType(args.head) + else if (ctx.settings.scalajs.value && args.length == 2 && + sym.owner == JSDefinitions.jsdefn.ScalaJSJSPackageClass && sym == JSDefinitions.jsdefn.PseudoUnionClass) { + // Treat Scala.js pseudo-unions as real unions, this requires a + // special-case in erasure, see TypeErasure#eraseInfo. + OrType(args(0), args(1), soft = false) + } else if (args.nonEmpty) tycon.safeAppliedTo(EtaExpandIfHK(sym.typeParams, args.map(translateTempPoly))) else if (sym.typeParams.nonEmpty) tycon.EtaExpand(sym.typeParams) else tycon diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index dab952004325..7c17cf198d57 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -2126,7 +2126,7 @@ import transform.SymUtils._ val addendum = if (scrutTp != testTp) s" is a subtype of ${testTp.show}" else " is the same as the tested type" - s"The highlighted type test will always succeed since the scrutinee type ($scrutTp.show)" + addendum + s"The highlighted type test will always succeed since the scrutinee type ${scrutTp.show}" + addendum } def explain = "" } diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 2969d7f4804f..ffb1d403230a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -2,6 +2,7 @@ package dotty.tools package dotc package typer +import backend.sjs.JSDefinitions import core._ import ast.{Trees, TreeTypeMap, untpd, tpd, DesugarEnums} import util.Spans._ @@ -634,6 +635,14 @@ trait ImplicitRunInfo: else pre.member(sym.name.toTermName) .suchThat(companion => companion.is(Module) && companion.owner == sym.owner) .symbol) + + // The companion of `js.|` defines an implicit conversions from + // `A | Unit` to `js.UndefOrOps[A]`. To keep this conversion in scope + // in Scala 3, where we re-interpret `js.|` as a real union, we inject + // it in the scope of `Unit`. + if t.isRef(defn.UnitClass) && ctx.settings.scalajs.value then + companions += JSDefinitions.jsdefn.UnionOpsModuleRef + if sym.isClass then for p <- t.parents do companions ++= iscopeRefs(p) else diff --git a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala index 20564f56e490..7c11508ece65 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala @@ -2,11 +2,13 @@ package dotty.tools package dotc package typer +import backend.sjs.JSDefinitions import core._ import Contexts._, Types._, Symbols._, Names._, Decorators._, ProtoTypes._ import Flags._, SymDenotations._ import NameKinds.FlatName import NameOps._ +import StdNames._ import config.Printers.{implicits, implicitsDetailed} import util.Spans.Span import ast.{untpd, tpd} @@ -64,6 +66,8 @@ trait ImportSuggestions: else !root.name.is(FlatName) && !root.name.lastPart.contains('$') && root.is(ModuleVal, butNot = JavaDefined) + // The implicits in `scalajs.js.|` are implementation details and shouldn't be suggested + && !(root.name == nme.raw.BAR && ctx.settings.scalajs.value && root == JSDefinitions.jsdefn.PseudoUnionModule) } def nestedRoots(site: Type)(using Context): List[Symbol] = diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index a8a638d365d2..85780105840c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2,6 +2,7 @@ package dotty.tools package dotc package typer +import backend.sjs.JSDefinitions import core._ import ast._ import Trees._ @@ -219,7 +220,13 @@ class Typer extends Namer denot = denot.filterWithPredicate { mbr => mbr.matchesImportBound(if mbr.symbol.is(Given) then imp.givenBound else imp.wildcardBound) } - if reallyExists(denot) then + def isScalaJsPseudoUnion = + denot.name == tpnme.raw.BAR && ctx.settings.scalajs.value && denot.symbol == JSDefinitions.jsdefn.PseudoUnionClass + // Just like Scala2Unpickler reinterprets Scala.js pseudo-unions + // as real union types, we want references to `A | B` in sources + // to be typed as a real union even if `js.|` has been imported, + // so we ignore that import. + if reallyExists(denot) && !isScalaJsPseudoUnion then if unimported.isEmpty || !unimported.contains(pre.termSymbol) then return pre.select(name, denot) case _ => diff --git a/library-js/src/scala/scalajs/js/internal/UnitOps.scala b/library-js/src/scala/scalajs/js/internal/UnitOps.scala new file mode 100644 index 000000000000..e2abc2f83c7e --- /dev/null +++ b/library-js/src/scala/scalajs/js/internal/UnitOps.scala @@ -0,0 +1,8 @@ +package scala.scalajs.js.internal + +import scala.scalajs.js + +/** Under -scalajs, this object is part of the implicit scope of `scala.Unit` */ +object UnitOps: + implicit def unitOrOps[A](x: A | Unit): js.UndefOrOps[A] = + new js.UndefOrOps(x) diff --git a/project/Build.scala b/project/Build.scala index e5aa08994797..3a4816737e7b 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -698,7 +698,7 @@ object Build { settings( libraryDependencies += ("org.scala-js" %% "scalajs-library" % scalaJSVersion).withDottyCompat(scalaVersion.value), - Compile / unmanagedSourceDirectories := + Compile / unmanagedSourceDirectories ++= (`scala3-library-bootstrapped` / Compile / unmanagedSourceDirectories).value, // Configure the source maps to point to GitHub for releases @@ -1105,9 +1105,13 @@ object Build { -- "ObjectTest.scala" // compile errors caused by #9588 -- "StackTraceTest.scala" // would require `npm install source-map-support` -- "UnionTypeTest.scala" // requires the Scala 2 macro defined in Typechecking*.scala + -- "PromiseMock.scala" // TODO: Enable once we use a Scala.js with https://github.com/scala-js/scala-js/pull/4451 in + // and remove copy in tests/sjs-junit )).get - ++ (dir / "js/src/test/require-2.12" ** "*.scala").get + ++ (dir / "js/src/test/require-2.12" ** (("*.scala": FileFilter) + -- "JSOptionalTest212.scala" // TODO: Enable once we use a Scala.js with https://github.com/scala-js/scala-js/pull/4451 in + )).get ++ (dir / "js/src/test/require-sam" ** "*.scala").get ++ (dir / "js/src/test/scala-new-collections" ** "*.scala").get ) diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/build.sbt b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/build.sbt new file mode 100644 index 000000000000..4186b8167e93 --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/build.sbt @@ -0,0 +1,16 @@ +lazy val scala2Lib = project.in(file("scala2Lib")) + .enablePlugins(ScalaJSPlugin) + .settings( + // TODO: switch to 2.13.5 once we've upgrade sbt-scalajs to 1.5.0 + scalaVersion := "2.13.4" + ) + +lazy val dottyApp = project.in(file("dottyApp")) + .dependsOn(scala2Lib) + .enablePlugins(ScalaJSPlugin) + .settings( + scalaVersion := sys.props("plugin.scalaVersion"), + + scalaJSUseMainModuleInitializer := true, + scalaJSLinkerConfig ~= (_.withCheckIR(true)), + ) diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/dottyApp/Main.scala b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/dottyApp/Main.scala new file mode 100644 index 000000000000..47774c732650 --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/dottyApp/Main.scala @@ -0,0 +1,13 @@ +object Main { + def main(args: Array[String]): Unit = { + val a = new scala2Lib.A + assert(a.foo(1) == "1") + assert(a.foo("") == "1") + assert(a.foo(Array(1)) == "2") + + val b = new scala2Lib.B + assert(b.foo(1) == "1") + assert(b.foo("") == "1") + assert(b.foo(Array(1)) == "2") + } +} diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/project/plugins.sbt b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/project/plugins.sbt new file mode 100644 index 000000000000..f69df9c39cc7 --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % sys.props("plugin.version")) +addSbtPlugin("org.scala-js" % "sbt-scalajs" % sys.props("plugin.scalaJSVersion")) diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/scala2Lib/Api.scala b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/scala2Lib/Api.scala new file mode 100644 index 000000000000..5c7388496f59 --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/scala2Lib/Api.scala @@ -0,0 +1,15 @@ +// Keep synchronized with dottyApp/Api.scala +package scala2Lib + +import scala.scalajs.js +import js.| + +class A { + def foo(x: Int | String): String = "1" + def foo(x: Array[Int]): String = "2" +} + +class B extends js.Object { + def foo(x: Int | String): String = "1" + def foo(x: Array[Int]): String = "2" +} diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/test b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/test new file mode 100644 index 000000000000..a67688f8dc37 --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/erasure-scalajs/test @@ -0,0 +1 @@ +> dottyApp/run diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt b/sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt index 1582924e0f41..794d78b308c0 100644 --- a/sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt +++ b/sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt @@ -1,15 +1,10 @@ lazy val scala2Lib = project.in(file("scala2Lib")) .settings( - scalaVersion := "2.13.2" + scalaVersion := "2.13.5" ) lazy val dottyApp = project.in(file("dottyApp")) .dependsOn(scala2Lib) .settings( - scalaVersion := sys.props("plugin.scalaVersion"), - // https://github.com/sbt/sbt/issues/5369 - projectDependencies := { - projectDependencies.value.map(_.withDottyCompat(scalaVersion.value)) - } + scalaVersion := sys.props("plugin.scalaVersion") ) - diff --git a/sbt-dotty/sbt-test/scala2-compat/i8001/build.sbt b/sbt-dotty/sbt-test/scala2-compat/i8001/build.sbt index 4f4ae373d2b1..27c7d9ea9b3c 100644 --- a/sbt-dotty/sbt-test/scala2-compat/i8001/build.sbt +++ b/sbt-dotty/sbt-test/scala2-compat/i8001/build.sbt @@ -7,9 +7,5 @@ lazy val lib = (project in file ("lib")) lazy val test = (project in file ("main")) .dependsOn(lib) .settings( - scalaVersion := scala3Version, - // https://github.com/sbt/sbt/issues/5369 - projectDependencies := { - projectDependencies.value.map(_.withDottyCompat(scalaVersion.value)) - } + scalaVersion := scala3Version ) diff --git a/sbt-dotty/sbt-test/scala2-compat/i8847/build.sbt b/sbt-dotty/sbt-test/scala2-compat/i8847/build.sbt index 2ed3b7a520b9..6a579f2be966 100644 --- a/sbt-dotty/sbt-test/scala2-compat/i8847/build.sbt +++ b/sbt-dotty/sbt-test/scala2-compat/i8847/build.sbt @@ -7,9 +7,5 @@ lazy val `i8847-lib` = (project in file ("lib")) lazy val `i8847-test` = (project in file ("main")) .dependsOn(`i8847-lib`) .settings( - scalaVersion := scala3Version, - // https://github.com/sbt/sbt/issues/5369 - projectDependencies := { - projectDependencies.value.map(_.withDottyCompat(scalaVersion.value)) - } + scalaVersion := scala3Version ) diff --git a/sbt-dotty/sbt-test/scala2-compat/i9916a/build.sbt b/sbt-dotty/sbt-test/scala2-compat/i9916a/build.sbt index 733b45e1b379..1f5c07900161 100644 --- a/sbt-dotty/sbt-test/scala2-compat/i9916a/build.sbt +++ b/sbt-dotty/sbt-test/scala2-compat/i9916a/build.sbt @@ -7,9 +7,5 @@ lazy val `i9916a-lib` = (project in file ("lib")) lazy val `i9916a-test` = (project in file ("main")) .dependsOn(`i9916a-lib`) .settings( - scalaVersion := scala3Version, - // https://github.com/sbt/sbt/issues/5369 - projectDependencies := { - projectDependencies.value.map(_.withDottyCompat(scalaVersion.value)) - } + scalaVersion := scala3Version ) diff --git a/sbt-dotty/sbt-test/scala2-compat/i9916b/build.sbt b/sbt-dotty/sbt-test/scala2-compat/i9916b/build.sbt index 9a4e81163911..f597a8610607 100644 --- a/sbt-dotty/sbt-test/scala2-compat/i9916b/build.sbt +++ b/sbt-dotty/sbt-test/scala2-compat/i9916b/build.sbt @@ -7,9 +7,5 @@ lazy val `i9916b-lib` = (project in file ("lib")) lazy val `i9916b-test` = (project in file ("main")) .dependsOn(`i9916b-lib`) .settings( - scalaVersion := scala3Version, - // https://github.com/sbt/sbt/issues/5369 - projectDependencies := { - projectDependencies.value.map(_.withDottyCompat(scalaVersion.value)) - } + scalaVersion := scala3Version ) diff --git a/tests/neg-scalajs/js-trait-members-wrong-kind.check b/tests/neg-scalajs/js-trait-members-wrong-kind.check index e6c20f1b2cce..d76b37b84680 100644 --- a/tests/neg-scalajs/js-trait-members-wrong-kind.check +++ b/tests/neg-scalajs/js-trait-members-wrong-kind.check @@ -2,15 +2,15 @@ 5 | lazy val a1: js.UndefOr[Int] = js.undefined // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | A non-native JS trait cannot contain lazy vals --- Error: tests/neg-scalajs/js-trait-members-wrong-kind.scala:7:29 ----------------------------------------------------- +-- Error: tests/neg-scalajs/js-trait-members-wrong-kind.scala:7:32 ----------------------------------------------------- 7 | def a(): js.UndefOr[Int] = js.undefined // error | ^^^^^^^^^^^^ | In non-native JS traits, defs with parentheses must be abstract. --- Error: tests/neg-scalajs/js-trait-members-wrong-kind.scala:8:35 ----------------------------------------------------- +-- Error: tests/neg-scalajs/js-trait-members-wrong-kind.scala:8:38 ----------------------------------------------------- 8 | def b(x: Int): js.UndefOr[Int] = js.undefined // error | ^^^^^^^^^^^^ | In non-native JS traits, defs with parentheses must be abstract. --- Error: tests/neg-scalajs/js-trait-members-wrong-kind.scala:9:26 ----------------------------------------------------- +-- Error: tests/neg-scalajs/js-trait-members-wrong-kind.scala:9:29 ----------------------------------------------------- 9 | def c_=(v: Int): Unit = js.undefined // error | ^^^^^^^^^^^^ | In non-native JS traits, defs with parentheses must be abstract. diff --git a/tests/neg-scalajs/type-mismatch.check b/tests/neg-scalajs/type-mismatch.check new file mode 100644 index 000000000000..2ea5f1f23945 --- /dev/null +++ b/tests/neg-scalajs/type-mismatch.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg-scalajs/type-mismatch.scala:6:17 ---------------------------------------------- +6 | val n: Int = msg // error message shouldn't mention implicits from `scalajs.js.|` + | ^^^ + | Found: (msg : String) + | Required: Int + +longer explanation available when compiling with `-explain` diff --git a/tests/neg-scalajs/type-mismatch.scala b/tests/neg-scalajs/type-mismatch.scala new file mode 100644 index 000000000000..09a10237ff1d --- /dev/null +++ b/tests/neg-scalajs/type-mismatch.scala @@ -0,0 +1,8 @@ +import scala.scalajs.js._ + +object HelloWorld { + def main(args: Array[String]): Unit = { + val msg = "hello" + val n: Int = msg // error message shouldn't mention implicits from `scalajs.js.|` + } +} diff --git a/tests/sjs-junit/test/org/scalajs/testsuite/jsinterop/PromiseMock.scala b/tests/sjs-junit/test/org/scalajs/testsuite/jsinterop/PromiseMock.scala new file mode 100644 index 000000000000..6764cdc7ac83 --- /dev/null +++ b/tests/sjs-junit/test/org/scalajs/testsuite/jsinterop/PromiseMock.scala @@ -0,0 +1,260 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.testsuite.jsinterop + +import scala.scalajs.js +import scala.scalajs.js.annotation._ +import scala.scalajs.js.| + +import js.Thenable + +object PromiseMock { + + @noinline + def withMockedPromise[A](body: (() => Unit) => A): A = { + val global = org.scalajs.testsuite.utils.JSUtils.globalObject + + val oldPromise = + if (global.hasOwnProperty("Promise").asInstanceOf[Boolean]) Some(global.Promise) + else None + + global.Promise = js.constructorOf[MockPromise[_]] + try { + body(MockPromise.processQueue _) + } finally { + oldPromise.fold { + js.special.delete(global, "Promise") + } { old => + global.Promise = old + } + } + } + + @noinline + def withMockedPromiseIfExists[A](body: (Option[() => Unit]) => A): A = { + val global = org.scalajs.testsuite.utils.JSUtils.globalObject + + val oldPromise = global.Promise + + if (js.isUndefined(oldPromise)) { + body(None) + } else { + global.Promise = js.constructorOf[MockPromise[_]] + try { + body(Some(MockPromise.processQueue _)) + } finally { + global.Promise = oldPromise + } + } + } + + private object MockPromise { + private val queue = js.Array[js.Function0[Any]]() + + @JSExportStatic + def resolve[A](value: A | js.Thenable[A]): MockPromise[A] = { + new MockPromise[A]({ + (resolve: js.Function1[A | js.Thenable[A], _], + reject: js.Function1[Any, _]) => + resolve(value) + }) + } + + @JSExportStatic + def reject(reason: Any): MockPromise[Nothing] = { + new MockPromise[Nothing]({ + (resolve: js.Function1[Nothing | js.Thenable[Nothing], _], + reject: js.Function1[Any, _]) => + reject(reason) + }) + } + + def enqueue(f: js.Function0[Any]): Unit = + queue.push(f) + + def processQueue(): Unit = { + while (queue.nonEmpty) + queue.shift()() + } + + private sealed abstract class State[+A] + + private case object Pending extends State[Nothing] + private case class Fulfilled[+A](value: A) extends State[A] + private case class Rejected(reason: Any) extends State[Nothing] + + private def isNotAnObject(x: Any): Boolean = x match { + case null | () | _:Double | _:Boolean | _:String => true + case _ => false + } + + private def isCallable(x: Any): Boolean = + js.typeOf(x.asInstanceOf[js.Any]) == "function" + + private def throwAny(e: Any): Nothing = { + throw (e match { + case th: Throwable => th + case _ => js.JavaScriptException(e) + }) + } + + private def tryCatchAny[A](tryBody: => A)(catchBody: Any => A): A = { + try { + tryBody + } catch { + case th: Throwable => + catchBody(th match { + case js.JavaScriptException(e) => e + case _ => th + }) + } + } + } + + private class MockPromise[+A]( + executor: js.Function2[js.Function1[A | Thenable[A], _], js.Function1[scala.Any, _], _]) + extends js.Object with js.Thenable[A] { + + import MockPromise._ + + private[this] var state: State[A] = Pending + + private[this] var fulfillReactions = js.Array[js.Function1[A, Any]]() + private[this] var rejectReactions = js.Array[js.Function1[Any, Any]]() + + init(executor) + + // 25.4.3.1 Promise(executor) + private[this] def init( + executor: js.Function2[js.Function1[A | Thenable[A], _], js.Function1[scala.Any, _], _]) = { + tryCatchAny[Unit] { + executor(resolve _, reject _) + } { e => + reject(e) + } + } + + private[this] def fulfill(value: A): Unit = { + assert(state == Pending) + state = Fulfilled(value) + clearAndTriggerReactions(fulfillReactions, value) + } + + private[this] def clearAndTriggerReactions[A]( + reactions: js.Array[js.Function1[A, Any]], + argument: A): Unit = { + + assert(state != Pending) + + fulfillReactions = null + rejectReactions = null + + for (reaction <- reactions) + enqueue(() => reaction(argument)) + } + + // 25.4.1.3.2 Promise Resolve Functions + private[this] def resolve(resolution: A | Thenable[A]): Unit = { + if (state == Pending) { + if (resolution.asInstanceOf[AnyRef] eq this) { + reject(new js.TypeError("Self resolution")) + } else if (isNotAnObject(resolution)) { + fulfill(resolution.asInstanceOf[A]) + } else { + tryCatchAny { + val thenAction = resolution.asInstanceOf[js.Dynamic].`then` + if (!isCallable(thenAction)) { + fulfill(resolution.asInstanceOf[A]) + } else { + val thenable = resolution.asInstanceOf[Thenable[A]] + val thenActionFun = thenAction.asInstanceOf[js.Function] + enqueue(() => promiseResolveThenableJob(thenable, thenActionFun)) + } + } { e => + reject(e) + } + } + } + } + + // 25.4.2.2 PromiseResolveThenableJob + private[this] def promiseResolveThenableJob(thenable: Thenable[A], + thenAction: js.Function): Unit = { + thenAction.call(thenable, resolve _, reject _) + } + + // 25.4.1.3.1 Promise Reject Functions + private[this] def reject(reason: Any): Unit = { + if (state == Pending) { + state = Rejected(reason) + clearAndTriggerReactions(rejectReactions, reason) + } + } + + // 25.4.5.3 Promise.prototype.then + def `then`[B]( + onFulfilled: js.Function1[A, B | Thenable[B]], + onRejected: js.UndefOr[js.Function1[scala.Any, B | Thenable[B]]]): MockPromise[B] = { + + new MockPromise[B]( + { (innerResolve: js.Function1[B | Thenable[B], _], + innerReject: js.Function1[scala.Any, _]) => + + def doFulfilled(value: A): Unit = { + tryCatchAny[Unit] { + innerResolve(onFulfilled(value)) + } { e => + innerReject(e) + } + } + + def doRejected(reason: Any): Unit = { + tryCatchAny[Unit] { + onRejected.fold[Unit] { + innerReject(reason) + } { onRejectedFun => + innerResolve(onRejectedFun(reason)) + } + } { e => + innerReject(e) + } + } + + state match { + case Pending => + fulfillReactions += doFulfilled _ + rejectReactions += doRejected _ + + case Fulfilled(value) => + enqueue(() => doFulfilled(value)) + + case Rejected(reason) => + enqueue(() => doRejected(reason)) + } + } + ) + } + + def `then`[B >: A]( + onFulfilled: Unit, + onRejected: js.UndefOr[js.Function1[scala.Any, B | Thenable[B]]]): MockPromise[B] = { + `then`((x: A) => (x: B | Thenable[B]), onRejected) + } + + // 25.4.5.1 Promise.prototype.catch + def `catch`[B >: A]( + onRejected: js.UndefOr[js.Function1[scala.Any, B | Thenable[B]]]): MockPromise[B] = { + `then`((), onRejected) + } + } +}