Skip to content

handle export forwarders in Scala.js #12611

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
47 changes: 41 additions & 6 deletions compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 =>
Expand Down Expand Up @@ -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

Expand All @@ -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`.
Expand Down
16 changes: 16 additions & 0 deletions tests/neg-scalajs/js-native-exports.check
Original file line number Diff line number Diff line change
@@ -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.
38 changes: 38 additions & 0 deletions tests/neg-scalajs/js-native-exports.scala
Original file line number Diff line number Diff line change
@@ -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
}

}
Original file line number Diff line number Diff line change
@@ -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