Skip to content

Commit c95cc72

Browse files
authored
Merge pull request #12611 from dotty-staging/fix-12111
handle export forwarders in Scala.js
2 parents 8d324c6 + d714080 commit c95cc72

File tree

5 files changed

+259
-6
lines changed

5 files changed

+259
-6
lines changed

compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ final class JSDefinitions()(using Context) {
9595
def JSExportStaticAnnot(using Context) = JSExportStaticAnnotType.symbol.asClass
9696
@threadUnsafe lazy val JSExportAllAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.JSExportAll")
9797
def JSExportAllAnnot(using Context) = JSExportAllAnnotType.symbol.asClass
98+
99+
def JSAnnotPackage(using Context) = JSGlobalAnnot.owner.asClass
100+
98101
@threadUnsafe lazy val JSTypeAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.internal.JSType")
99102
def JSTypeAnnot(using Context) = JSTypeAnnotType.symbol.asClass
100103
@threadUnsafe lazy val JSOptionalAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.internal.JSOptional")

compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP
123123

124124
checkInternalAnnotations(sym)
125125

126+
stripJSAnnotsOnExported(sym)
127+
126128
/* Checks related to @js.native:
127129
* - if @js.native, verify that it is allowed in this context, and if
128130
* yes, compute and store the JS native load spec
@@ -299,6 +301,14 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP
299301

300302
super.transform(tree)
301303

304+
case _: Export =>
305+
if enclosingOwner is OwnerKind.JSNative then
306+
report.error("Native JS traits, classes and objects cannot contain exported definitions.", tree)
307+
else if enclosingOwner is OwnerKind.JSTrait then
308+
report.error("Non-native JS traits cannot contain exported definitions.", tree)
309+
310+
super.transform(tree)
311+
302312
case _ =>
303313
super.transform(tree)
304314
}
@@ -457,7 +467,8 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP
457467
val kind = {
458468
if (!isJSNative) {
459469
if (sym.is(ModuleClass)) OwnerKind.JSMod
460-
else OwnerKind.JSClass
470+
else if (sym.is(Trait)) OwnerKind.JSTrait
471+
else OwnerKind.JSNonTraitClass
461472
} else {
462473
if (sym.is(ModuleClass)) OwnerKind.JSNativeMod
463474
else OwnerKind.JSNativeClass
@@ -814,7 +825,29 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP
814825
super.transform(tree)
815826
}
816827

828+
/** Removes annotations from exported definitions (e.g. `export foo.bar`):
829+
* - `js.native`
830+
* - `js.annotation.*`
831+
*/
832+
private def stripJSAnnotsOnExported(sym: Symbol)(using Context): Unit =
833+
if !sym.is(Exported) then
834+
return // only remove annotations from exported definitions
835+
836+
val JSNativeAnnot = jsdefn.JSNativeAnnot
837+
val JSAnnotPackage = jsdefn.JSAnnotPackage
838+
839+
extension (sym: Symbol) def isJSAnnot =
840+
(sym eq JSNativeAnnot) || (sym.owner eq JSAnnotPackage)
841+
842+
val newAnnots = sym.annotations.filterConserve(!_.symbol.isJSAnnot)
843+
if newAnnots ne sym.annotations then
844+
sym.annotations = newAnnots
845+
end stripJSAnnotsOnExported
846+
817847
private def checkRHSCallsJSNative(tree: ValOrDefDef, longKindStr: String)(using Context): Unit = {
848+
if tree.symbol.is(Exported) then
849+
return // we already report an error that exports are not allowed here, this prevents extra errors.
850+
818851
// Check that the rhs is exactly `= js.native`
819852
tree.rhs match {
820853
case sel: Select if sel.symbol == jsdefn.JSPackage_native =>
@@ -992,10 +1025,12 @@ object PrepJSInterop {
9921025
val JSNativeClass = new OwnerKind(0x04)
9931026
/** A native JS object, which extends js.Any. */
9941027
val JSNativeMod = new OwnerKind(0x08)
995-
/** A non-native JS class/trait. */
996-
val JSClass = new OwnerKind(0x10)
1028+
/** A non-native JS class (not a trait). */
1029+
val JSNonTraitClass = new OwnerKind(0x10)
1030+
/** A non-native JS trait. */
1031+
val JSTrait = new OwnerKind(0x20)
9971032
/** A non-native JS object. */
998-
val JSMod = new OwnerKind(0x20)
1033+
val JSMod = new OwnerKind(0x40)
9991034

10001035
// Compound kinds
10011036

@@ -1005,12 +1040,12 @@ object PrepJSInterop {
10051040
/** A native JS class/trait/object. */
10061041
val JSNative = JSNativeClass | JSNativeMod
10071042
/** A non-native JS class/trait/object. */
1008-
val JSNonNative = JSClass | JSMod
1043+
val JSNonNative = JSNonTraitClass | JSTrait | JSMod
10091044
/** A JS type, i.e., something extending js.Any. */
10101045
val JSType = JSNative | JSNonNative
10111046

10121047
/** Any kind of class/trait, i.e., a Scala or JS class/trait. */
1013-
val AnyClass = ScalaClass | JSNativeClass | JSClass
1048+
val AnyClass = ScalaClass | JSNativeClass | JSNonTraitClass | JSTrait
10141049
}
10151050

10161051
/** Tests if the symbol extend `js.Any`.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- Error: tests/neg-scalajs/js-native-exports.scala:17:11 --------------------------------------------------------------
2+
17 | export bag.{str, int, bool, dbl} // error
3+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4+
| Native JS traits, classes and objects cannot contain exported definitions.
5+
-- Error: tests/neg-scalajs/js-native-exports.scala:23:11 --------------------------------------------------------------
6+
23 | export bag.{str, int, bool, dbl} // error
7+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8+
| Native JS traits, classes and objects cannot contain exported definitions.
9+
-- Error: tests/neg-scalajs/js-native-exports.scala:30:11 --------------------------------------------------------------
10+
30 | export bag.{str, int, bool, dbl} // error
11+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12+
| Native JS traits, classes and objects cannot contain exported definitions.
13+
-- Error: tests/neg-scalajs/js-native-exports.scala:35:11 --------------------------------------------------------------
14+
35 | export bag.{str, int, bool, dbl} // error
15+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
16+
| Non-native JS traits cannot contain exported definitions.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import scala.scalajs.js
2+
import scala.scalajs.js.annotation.*
3+
4+
object A {
5+
6+
@js.native
7+
trait Bag extends js.Any {
8+
val str: String
9+
def int: Int
10+
def bool(): Boolean
11+
def dbl(dbl: Double): Double
12+
}
13+
14+
@js.native
15+
@JSGlobal("BagHolder_GlobalClass")
16+
final class BagHolder(val bag: Bag) extends js.Object {
17+
export bag.{str, int, bool, dbl} // error
18+
}
19+
20+
@js.native
21+
trait BagHolderTrait extends js.Any {
22+
val bag: Bag
23+
export bag.{str, int, bool, dbl} // error
24+
}
25+
26+
@js.native
27+
@JSGlobal("BagHolderModule_GlobalVar")
28+
object BagHolderModule extends js.Object {
29+
val bag: Bag = js.native
30+
export bag.{str, int, bool, dbl} // error
31+
}
32+
33+
trait NonNativeBagHolderTrait extends js.Any {
34+
val bag: Bag
35+
export bag.{str, int, bool, dbl} // error
36+
}
37+
38+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package org.scalajs.testsuite.jsinterop
2+
3+
import org.junit.Assert.*
4+
import org.junit.Test
5+
6+
import scala.scalajs.js
7+
import scala.scalajs.js.annotation.*
8+
9+
object ExportedJSNativeMembersScala3:
10+
11+
object A {
12+
13+
@js.native
14+
trait FooModule extends js.Any { self: Foo.type =>
15+
val foo: String
16+
}
17+
18+
@js.native
19+
@JSGlobal("Foo_GlobalThatWillBeExported")
20+
val Foo: FooModule = js.native
21+
22+
@js.native
23+
@JSGlobal("Bar_GlobalThatWillBeExported")
24+
object Bar extends js.Any {
25+
val bar: Int = js.native
26+
}
27+
28+
@js.native
29+
@JSGlobal("Baz_GlobalThatWillBeExported")
30+
final class Baz(var baz: String) extends js.Object
31+
32+
@js.native
33+
@JSGlobal("QuxHolder_GlobalThatWillBeExported")
34+
final class QuxHolder(val qux: String) extends js.Object
35+
36+
@js.native
37+
@JSGlobal("QuxHolderHolder_GlobalThatWillBeExported")
38+
final class QuxHolderHolder(val quxHolder: QuxHolder) extends js.Object {
39+
val qux: quxHolder.qux.type = js.native
40+
}
41+
42+
@js.native // structurally equivalent to QuxHolderHolder, but a trait
43+
trait QuxHolderHolderTrait(val quxHolder: QuxHolder) extends js.Any {
44+
val qux: quxHolder.qux.type
45+
}
46+
47+
@js.native
48+
@JSGlobal("quxxInstance_GlobalThatWillBeExported")
49+
val quxxInstance: QuxHolderHolderTrait = js.native
50+
51+
@js.native
52+
@JSGlobal("addOne_GlobalThatWillBeExported")
53+
def addOne(i: Int): Int = js.native
54+
55+
}
56+
57+
object B extends js.Object {
58+
export A.FooModule // trait (native)
59+
export A.Foo // val (native)
60+
export A.Bar // object (native)
61+
export A.Baz // class (native)
62+
export A.QuxHolder // class (native)
63+
export A.QuxHolderHolder // class (native)
64+
export A.QuxHolderHolderTrait // trait (native)
65+
export A.quxxInstance // val (native)
66+
export A.addOne // def (native)
67+
}
68+
69+
final class C extends js.Object {
70+
export A.FooModule // trait (native)
71+
export A.Foo // val (native)
72+
export A.Bar // object (native)
73+
export A.Baz // class (native)
74+
export A.QuxHolder // class (native)
75+
export A.QuxHolderHolder // class (native)
76+
export A.QuxHolderHolderTrait // trait (native)
77+
export A.quxxInstance // val (native)
78+
export A.addOne // def (native)
79+
}
80+
81+
class ExportedJSNativeMembersScala3:
82+
import ExportedJSNativeMembersScala3.*
83+
84+
@Test def forward_top_level_JS_var_with_export(): Unit = {
85+
js.eval("""
86+
var Foo_GlobalThatWillBeExported = {
87+
foo: "foo"
88+
}
89+
var Bar_GlobalThatWillBeExported = {
90+
bar: 23
91+
}
92+
function Baz_GlobalThatWillBeExported(baz) {
93+
this.baz = baz
94+
}
95+
function QuxHolder_GlobalThatWillBeExported(qux) {
96+
this.qux = qux
97+
}
98+
function QuxHolderHolder_GlobalThatWillBeExported(quxHolder) {
99+
this.quxHolder = quxHolder;
100+
this.qux = quxHolder.qux;
101+
}
102+
var quxxInstance_GlobalThatWillBeExported = (
103+
new QuxHolderHolder_GlobalThatWillBeExported(
104+
new QuxHolder_GlobalThatWillBeExported("quxxInstance")
105+
)
106+
)
107+
function addOne_GlobalThatWillBeExported(i) {
108+
return i + 1;
109+
}
110+
""")
111+
112+
val C = ExportedJSNativeMembersScala3.C()
113+
114+
assertEquals("foo", A.Foo.foo)
115+
assertEquals("foo", B.Foo.foo)
116+
assertEquals("foo", C.Foo.foo)
117+
118+
assertEquals(23, A.Bar.bar)
119+
assertEquals(23, B.Bar.bar)
120+
assertEquals(23, C.Bar.bar)
121+
122+
val abaz = A.Baz("abaz1")
123+
assertEquals("abaz1", abaz.baz)
124+
abaz.baz = "abaz2"
125+
assertEquals("abaz2", abaz.baz)
126+
127+
val bbaz = B.Baz("bbaz1")
128+
assertEquals("bbaz1", bbaz.baz)
129+
bbaz.baz = "bbaz2"
130+
assertEquals("bbaz2", bbaz.baz)
131+
132+
val cbaz = C.Baz("cbaz1")
133+
assertEquals("cbaz1", cbaz.baz)
134+
cbaz.baz = "cbaz2"
135+
assertEquals("cbaz2", cbaz.baz)
136+
137+
val quxHolderHolderA = A.QuxHolderHolder(A.QuxHolder("quxHolderHolderA"))
138+
assertEquals("quxHolderHolderA", quxHolderHolderA.qux)
139+
assertEquals("quxHolderHolderA", quxHolderHolderA.quxHolder.qux)
140+
141+
val quxHolderHolderB = B.QuxHolderHolder(B.QuxHolder("quxHolderHolderB"))
142+
assertEquals("quxHolderHolderB", quxHolderHolderB.qux)
143+
assertEquals("quxHolderHolderB", quxHolderHolderB.quxHolder.qux)
144+
145+
val quxHolderHolderC = C.QuxHolderHolder(C.QuxHolder("quxHolderHolderC"))
146+
assertEquals("quxHolderHolderC", quxHolderHolderC.qux)
147+
assertEquals("quxHolderHolderC", quxHolderHolderC.quxHolder.qux)
148+
149+
assertEquals("quxxInstance", A.quxxInstance.qux)
150+
assertEquals("quxxInstance", A.quxxInstance.quxHolder.qux)
151+
assertEquals("quxxInstance", B.quxxInstance.qux)
152+
assertEquals("quxxInstance", B.quxxInstance.quxHolder.qux)
153+
assertEquals("quxxInstance", C.quxxInstance.qux)
154+
assertEquals("quxxInstance", C.quxxInstance.quxHolder.qux)
155+
156+
assertEquals(2, A.addOne(1))
157+
assertEquals(3, B.addOne(2))
158+
assertEquals(4, C.addOne(3))
159+
}
160+
161+
end ExportedJSNativeMembersScala3

0 commit comments

Comments
 (0)