diff --git a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala index c73821430098..a97b6ad2687e 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala @@ -95,6 +95,9 @@ final class JSDefinitions()(using Context) { def JSExportStaticAnnot(using Context) = JSExportStaticAnnotType.symbol.asClass @threadUnsafe lazy val JSExportAllAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSExportAll") def JSExportAllAnnot(using Context) = JSExportAllAnnotType.symbol.asClass + + def JSAnnotPackage(using Context) = JSGlobalAnnot.owner.asClass + @threadUnsafe lazy val JSTypeAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.internal.JSType") def JSTypeAnnot(using Context) = JSTypeAnnotType.symbol.asClass @threadUnsafe lazy val JSOptionalAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.internal.JSOptional") diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala index 8e9e11aef048..cf4edef45b08 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala @@ -123,6 +123,8 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP checkInternalAnnotations(sym) + stripJSAnnotsOnExported(sym) + /* Checks related to @js.native: * - if @js.native, verify that it is allowed in this context, and if * yes, compute and store the JS native load spec @@ -299,6 +301,14 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP super.transform(tree) + case _: Export => + if enclosingOwner is OwnerKind.JSNative then + report.error("Native JS traits, classes and objects cannot contain exported definitions.", tree) + else if enclosingOwner is OwnerKind.JSTrait then + report.error("Non-native JS traits cannot contain exported definitions.", tree) + + super.transform(tree) + case _ => super.transform(tree) } @@ -457,7 +467,8 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP val kind = { if (!isJSNative) { if (sym.is(ModuleClass)) OwnerKind.JSMod - else OwnerKind.JSClass + else if (sym.is(Trait)) OwnerKind.JSTrait + else OwnerKind.JSNonTraitClass } else { if (sym.is(ModuleClass)) OwnerKind.JSNativeMod else OwnerKind.JSNativeClass @@ -814,7 +825,29 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP super.transform(tree) } + /** Removes annotations from exported definitions (e.g. `export foo.bar`): + * - `js.native` + * - `js.annotation.*` + */ + private def stripJSAnnotsOnExported(sym: Symbol)(using Context): Unit = + if !sym.is(Exported) then + return // only remove annotations from exported definitions + + val JSNativeAnnot = jsdefn.JSNativeAnnot + val JSAnnotPackage = jsdefn.JSAnnotPackage + + extension (sym: Symbol) def isJSAnnot = + (sym eq JSNativeAnnot) || (sym.owner eq JSAnnotPackage) + + val newAnnots = sym.annotations.filterConserve(!_.symbol.isJSAnnot) + if newAnnots ne sym.annotations then + sym.annotations = newAnnots + end stripJSAnnotsOnExported + private def checkRHSCallsJSNative(tree: ValOrDefDef, longKindStr: String)(using Context): Unit = { + if tree.symbol.is(Exported) then + return // we already report an error that exports are not allowed here, this prevents extra errors. + // Check that the rhs is exactly `= js.native` tree.rhs match { case sel: Select if sel.symbol == jsdefn.JSPackage_native => @@ -992,10 +1025,12 @@ object PrepJSInterop { val JSNativeClass = new OwnerKind(0x04) /** A native JS object, which extends js.Any. */ val JSNativeMod = new OwnerKind(0x08) - /** A non-native JS class/trait. */ - val JSClass = new OwnerKind(0x10) + /** A non-native JS class (not a trait). */ + val JSNonTraitClass = new OwnerKind(0x10) + /** A non-native JS trait. */ + val JSTrait = new OwnerKind(0x20) /** A non-native JS object. */ - val JSMod = new OwnerKind(0x20) + val JSMod = new OwnerKind(0x40) // Compound kinds @@ -1005,12 +1040,12 @@ object PrepJSInterop { /** A native JS class/trait/object. */ val JSNative = JSNativeClass | JSNativeMod /** A non-native JS class/trait/object. */ - val JSNonNative = JSClass | JSMod + val JSNonNative = JSNonTraitClass | JSTrait | JSMod /** A JS type, i.e., something extending js.Any. */ val JSType = JSNative | JSNonNative /** Any kind of class/trait, i.e., a Scala or JS class/trait. */ - val AnyClass = ScalaClass | JSNativeClass | JSClass + val AnyClass = ScalaClass | JSNativeClass | JSNonTraitClass | JSTrait } /** Tests if the symbol extend `js.Any`. diff --git a/tests/neg-scalajs/js-native-exports.check b/tests/neg-scalajs/js-native-exports.check new file mode 100644 index 000000000000..1003580b64ca --- /dev/null +++ b/tests/neg-scalajs/js-native-exports.check @@ -0,0 +1,16 @@ +-- Error: tests/neg-scalajs/js-native-exports.scala:17:11 -------------------------------------------------------------- +17 | export bag.{str, int, bool, dbl} // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Native JS traits, classes and objects cannot contain exported definitions. +-- Error: tests/neg-scalajs/js-native-exports.scala:23:11 -------------------------------------------------------------- +23 | export bag.{str, int, bool, dbl} // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Native JS traits, classes and objects cannot contain exported definitions. +-- Error: tests/neg-scalajs/js-native-exports.scala:30:11 -------------------------------------------------------------- +30 | export bag.{str, int, bool, dbl} // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Native JS traits, classes and objects cannot contain exported definitions. +-- Error: tests/neg-scalajs/js-native-exports.scala:35:11 -------------------------------------------------------------- +35 | export bag.{str, int, bool, dbl} // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Non-native JS traits cannot contain exported definitions. diff --git a/tests/neg-scalajs/js-native-exports.scala b/tests/neg-scalajs/js-native-exports.scala new file mode 100644 index 000000000000..73c0ffe93573 --- /dev/null +++ b/tests/neg-scalajs/js-native-exports.scala @@ -0,0 +1,38 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation.* + +object A { + + @js.native + trait Bag extends js.Any { + val str: String + def int: Int + def bool(): Boolean + def dbl(dbl: Double): Double + } + + @js.native + @JSGlobal("BagHolder_GlobalClass") + final class BagHolder(val bag: Bag) extends js.Object { + export bag.{str, int, bool, dbl} // error + } + + @js.native + trait BagHolderTrait extends js.Any { + val bag: Bag + export bag.{str, int, bool, dbl} // error + } + + @js.native + @JSGlobal("BagHolderModule_GlobalVar") + object BagHolderModule extends js.Object { + val bag: Bag = js.native + export bag.{str, int, bool, dbl} // error + } + + trait NonNativeBagHolderTrait extends js.Any { + val bag: Bag + export bag.{str, int, bool, dbl} // error + } + +} diff --git a/tests/sjs-junit/test/org/scalajs/testsuite/jsinterop/ExportedJSNativeMembersScala3.scala b/tests/sjs-junit/test/org/scalajs/testsuite/jsinterop/ExportedJSNativeMembersScala3.scala new file mode 100644 index 000000000000..79d78d929578 --- /dev/null +++ b/tests/sjs-junit/test/org/scalajs/testsuite/jsinterop/ExportedJSNativeMembersScala3.scala @@ -0,0 +1,161 @@ +package org.scalajs.testsuite.jsinterop + +import org.junit.Assert.* +import org.junit.Test + +import scala.scalajs.js +import scala.scalajs.js.annotation.* + +object ExportedJSNativeMembersScala3: + + object A { + + @js.native + trait FooModule extends js.Any { self: Foo.type => + val foo: String + } + + @js.native + @JSGlobal("Foo_GlobalThatWillBeExported") + val Foo: FooModule = js.native + + @js.native + @JSGlobal("Bar_GlobalThatWillBeExported") + object Bar extends js.Any { + val bar: Int = js.native + } + + @js.native + @JSGlobal("Baz_GlobalThatWillBeExported") + final class Baz(var baz: String) extends js.Object + + @js.native + @JSGlobal("QuxHolder_GlobalThatWillBeExported") + final class QuxHolder(val qux: String) extends js.Object + + @js.native + @JSGlobal("QuxHolderHolder_GlobalThatWillBeExported") + final class QuxHolderHolder(val quxHolder: QuxHolder) extends js.Object { + val qux: quxHolder.qux.type = js.native + } + + @js.native // structurally equivalent to QuxHolderHolder, but a trait + trait QuxHolderHolderTrait(val quxHolder: QuxHolder) extends js.Any { + val qux: quxHolder.qux.type + } + + @js.native + @JSGlobal("quxxInstance_GlobalThatWillBeExported") + val quxxInstance: QuxHolderHolderTrait = js.native + + @js.native + @JSGlobal("addOne_GlobalThatWillBeExported") + def addOne(i: Int): Int = js.native + + } + + object B extends js.Object { + export A.FooModule // trait (native) + export A.Foo // val (native) + export A.Bar // object (native) + export A.Baz // class (native) + export A.QuxHolder // class (native) + export A.QuxHolderHolder // class (native) + export A.QuxHolderHolderTrait // trait (native) + export A.quxxInstance // val (native) + export A.addOne // def (native) + } + + final class C extends js.Object { + export A.FooModule // trait (native) + export A.Foo // val (native) + export A.Bar // object (native) + export A.Baz // class (native) + export A.QuxHolder // class (native) + export A.QuxHolderHolder // class (native) + export A.QuxHolderHolderTrait // trait (native) + export A.quxxInstance // val (native) + export A.addOne // def (native) + } + +class ExportedJSNativeMembersScala3: + import ExportedJSNativeMembersScala3.* + + @Test def forward_top_level_JS_var_with_export(): Unit = { + js.eval(""" + var Foo_GlobalThatWillBeExported = { + foo: "foo" + } + var Bar_GlobalThatWillBeExported = { + bar: 23 + } + function Baz_GlobalThatWillBeExported(baz) { + this.baz = baz + } + function QuxHolder_GlobalThatWillBeExported(qux) { + this.qux = qux + } + function QuxHolderHolder_GlobalThatWillBeExported(quxHolder) { + this.quxHolder = quxHolder; + this.qux = quxHolder.qux; + } + var quxxInstance_GlobalThatWillBeExported = ( + new QuxHolderHolder_GlobalThatWillBeExported( + new QuxHolder_GlobalThatWillBeExported("quxxInstance") + ) + ) + function addOne_GlobalThatWillBeExported(i) { + return i + 1; + } + """) + + val C = ExportedJSNativeMembersScala3.C() + + assertEquals("foo", A.Foo.foo) + assertEquals("foo", B.Foo.foo) + assertEquals("foo", C.Foo.foo) + + assertEquals(23, A.Bar.bar) + assertEquals(23, B.Bar.bar) + assertEquals(23, C.Bar.bar) + + val abaz = A.Baz("abaz1") + assertEquals("abaz1", abaz.baz) + abaz.baz = "abaz2" + assertEquals("abaz2", abaz.baz) + + val bbaz = B.Baz("bbaz1") + assertEquals("bbaz1", bbaz.baz) + bbaz.baz = "bbaz2" + assertEquals("bbaz2", bbaz.baz) + + val cbaz = C.Baz("cbaz1") + assertEquals("cbaz1", cbaz.baz) + cbaz.baz = "cbaz2" + assertEquals("cbaz2", cbaz.baz) + + val quxHolderHolderA = A.QuxHolderHolder(A.QuxHolder("quxHolderHolderA")) + assertEquals("quxHolderHolderA", quxHolderHolderA.qux) + assertEquals("quxHolderHolderA", quxHolderHolderA.quxHolder.qux) + + val quxHolderHolderB = B.QuxHolderHolder(B.QuxHolder("quxHolderHolderB")) + assertEquals("quxHolderHolderB", quxHolderHolderB.qux) + assertEquals("quxHolderHolderB", quxHolderHolderB.quxHolder.qux) + + val quxHolderHolderC = C.QuxHolderHolder(C.QuxHolder("quxHolderHolderC")) + assertEquals("quxHolderHolderC", quxHolderHolderC.qux) + assertEquals("quxHolderHolderC", quxHolderHolderC.quxHolder.qux) + + assertEquals("quxxInstance", A.quxxInstance.qux) + assertEquals("quxxInstance", A.quxxInstance.quxHolder.qux) + assertEquals("quxxInstance", B.quxxInstance.qux) + assertEquals("quxxInstance", B.quxxInstance.quxHolder.qux) + assertEquals("quxxInstance", C.quxxInstance.qux) + assertEquals("quxxInstance", C.quxxInstance.quxHolder.qux) + + assertEquals(2, A.addOne(1)) + assertEquals(3, B.addOne(2)) + assertEquals(4, C.addOne(3)) + } + +end ExportedJSNativeMembersScala3