Skip to content

Commit ce351db

Browse files
committed
handle export forwarders in Scala.js
- prevent export forwarders in JS Native types - remove annotations js.native, js.annotation.* from export forwarders in PrepJSInterop
1 parent a956774 commit ce351db

File tree

5 files changed

+210
-12
lines changed

5 files changed

+210
-12
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,18 @@ 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")
101104
def JSOptionalAnnot(using Context) = JSOptionalAnnotType.symbol.asClass
102105
@threadUnsafe lazy val ExposedJSMemberAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.annotation.internal.ExposedJSMember")
103106
def ExposedJSMemberAnnot(using Context) = ExposedJSMemberAnnotType.symbol.asClass
104107

108+
def JSAnnotInternalPackage(using Context) = JSTypeAnnot.owner.asClass
109+
105110
@threadUnsafe lazy val JSImportNamespaceModuleRef = requiredModuleRef("scala.scalajs.js.annotation.JSImport.Namespace")
106111
def JSImportNamespaceModule(using Context) = JSImportNamespaceModuleRef.symbol
107112

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

Lines changed: 41 additions & 12 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,12 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP
299301

300302
super.transform(tree)
301303

304+
case _: Export =>
305+
if anyEnclosingOwner is OwnerKind.JSNative then
306+
report.error("Native JS traits, classes and objects cannot contain exported definitions.", tree)
307+
308+
super.transform(tree)
309+
302310
case _ =>
303311
super.transform(tree)
304312
}
@@ -814,20 +822,41 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP
814822
super.transform(tree)
815823
}
816824

825+
/** Removes annotations from exported definitions (e.g. `export foo.bar`):
826+
* - `js.native`
827+
* - `js.annotation.*`
828+
* - `js.annotation.internal.*`
829+
*/
830+
private def stripJSAnnotsOnExported(sym: Symbol)(using Context): Unit =
831+
if !sym.is(Exported) then return
832+
833+
val JSNativeAnnot = jsdefn.JSNativeAnnot
834+
val JSAnnotPackage = jsdefn.JSAnnotPackage
835+
val JSAnnotInternalPackage = jsdefn.JSAnnotInternalPackage
836+
837+
extension (sym: Symbol) def isJSAnnot =
838+
(sym eq JSNativeAnnot) || (sym.owner eq JSAnnotPackage) || (sym.owner eq JSAnnotInternalPackage)
839+
840+
val newAnnots = sym.annotations.filterConserve(!_.symbol.isJSAnnot)
841+
if newAnnots ne sym.annotations then
842+
sym.annotations = newAnnots
843+
end stripJSAnnotsOnExported
844+
817845
private def checkRHSCallsJSNative(tree: ValOrDefDef, longKindStr: String)(using Context): Unit = {
818-
// Check that the rhs is exactly `= js.native`
819-
tree.rhs match {
820-
case sel: Select if sel.symbol == jsdefn.JSPackage_native =>
821-
// ok
822-
case _ =>
823-
val pos = if (tree.rhs != EmptyTree) tree.rhs.srcPos else tree.srcPos
824-
report.error(s"$longKindStr may only call js.native.", pos)
825-
}
846+
if !tree.symbol.is(Exported) then
847+
// Check that the rhs is exactly `= js.native`
848+
tree.rhs match {
849+
case sel: Select if sel.symbol == jsdefn.JSPackage_native =>
850+
// ok
851+
case _ =>
852+
val pos = if (tree.rhs != EmptyTree) tree.rhs.srcPos else tree.srcPos
853+
report.error(s"$longKindStr may only call js.native.", pos)
854+
}
826855

827-
// Check that the resul type was explicitly specified
828-
// (This is stronger than Scala 2, which only warns, and only if it was inferred as Nothing.)
829-
if (tree.tpt.span.isSynthetic)
830-
report.error(i"The type of ${tree.name} must be explicitly specified because it is JS native.", tree)
856+
// Check that the resul type was explicitly specified
857+
// (This is stronger than Scala 2, which only warns, and only if it was inferred as Nothing.)
858+
if (tree.tpt.span.isSynthetic)
859+
report.error(i"The type of ${tree.name} must be explicitly specified because it is JS native.", tree)
831860
}
832861

833862
private def checkJSNativeSpecificAnnotsOnNonJSNative(memberDef: MemberDef)(using Context): Unit = {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-- Error: tests/neg-scalajs/js-native-exports.scala:15:11 --------------------------------------------------------------
2+
15 | export bag.{str, int} // error
3+
| ^^^^^^^^^^^^^^^^^^^^^
4+
| Native JS traits, classes and objects cannot contain exported definitions.
5+
-- Error: tests/neg-scalajs/js-native-exports.scala:21:11 --------------------------------------------------------------
6+
21 | export bag.{str, int} // error
7+
| ^^^^^^^^^^^^^^^^^^^^^
8+
| Native JS traits, classes and objects cannot contain exported definitions.
9+
-- Error: tests/neg-scalajs/js-native-exports.scala:28:11 --------------------------------------------------------------
10+
28 | export bag.{str, int} // error
11+
| ^^^^^^^^^^^^^^^^^^^^^
12+
| Native JS traits, classes and objects cannot contain exported definitions.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
val int: Int
10+
}
11+
12+
@js.native
13+
@JSGlobal("BagHolder_GlobalClass")
14+
final class BagHolder(val bag: Bag) extends js.Object {
15+
export bag.{str, int} // error
16+
}
17+
18+
@js.native
19+
trait BagHolderTrait extends js.Any {
20+
val bag: Bag
21+
export bag.{str, int} // error
22+
}
23+
24+
@js.native
25+
@JSGlobal("BagHolderModule_GlobalVar")
26+
object BagHolderModule extends js.Object {
27+
val bag: Bag = js.native
28+
export bag.{str, int} // error
29+
}
30+
31+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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+
}
52+
53+
object B {
54+
export A.FooModule // trait (native)
55+
export A.Foo // val (native)
56+
export A.Bar // object (native)
57+
export A.Baz // class (native)
58+
export A.QuxHolder // class (native)
59+
export A.QuxHolderHolder // class (native)
60+
export A.QuxHolderHolderTrait // trait (native)
61+
export A.quxxInstance // val (native)
62+
}
63+
64+
class ExportedJSNativeMembersScala3:
65+
import ExportedJSNativeMembersScala3.*
66+
67+
@Test def forward_top_level_JS_var_with_export(): Unit = {
68+
js.eval("""
69+
var Foo_GlobalThatWillBeExported = {
70+
foo: "foo"
71+
}
72+
var Bar_GlobalThatWillBeExported = {
73+
bar: 23
74+
}
75+
function Baz_GlobalThatWillBeExported(baz) {
76+
this.baz = baz
77+
}
78+
function QuxHolder_GlobalThatWillBeExported(qux) {
79+
this.qux = qux
80+
}
81+
function QuxHolderHolder_GlobalThatWillBeExported(quxHolder) {
82+
this.quxHolder = quxHolder;
83+
this.qux = quxHolder.qux;
84+
}
85+
var quxxInstance_GlobalThatWillBeExported = (
86+
new QuxHolderHolder_GlobalThatWillBeExported(
87+
new QuxHolder_GlobalThatWillBeExported("quxxInstance")
88+
)
89+
)
90+
""")
91+
assertEquals("foo", A.Foo.foo)
92+
assertEquals("foo", B.Foo.foo)
93+
94+
assertEquals(23, A.Bar.bar)
95+
assertEquals(23, B.Bar.bar)
96+
97+
val abaz = A.Baz("abaz1")
98+
assertEquals("abaz1", abaz.baz)
99+
abaz.baz = "abaz2"
100+
assertEquals("abaz2", abaz.baz)
101+
102+
val bbaz = B.Baz("bbaz1")
103+
assertEquals("bbaz1", bbaz.baz)
104+
bbaz.baz = "bbaz2"
105+
assertEquals("bbaz2", bbaz.baz)
106+
107+
val quxHolderHolderA = A.QuxHolderHolder(A.QuxHolder("quxHolderHolderA"))
108+
assertEquals("quxHolderHolderA", quxHolderHolderA.qux)
109+
assertEquals("quxHolderHolderA", quxHolderHolderA.quxHolder.qux)
110+
111+
val quxHolderHolderB = B.QuxHolderHolder(B.QuxHolder("quxHolderHolderB"))
112+
assertEquals("quxHolderHolderB", quxHolderHolderB.qux)
113+
assertEquals("quxHolderHolderB", quxHolderHolderB.quxHolder.qux)
114+
115+
assertEquals("quxxInstance", A.quxxInstance.qux)
116+
assertEquals("quxxInstance", A.quxxInstance.quxHolder.qux)
117+
assertEquals("quxxInstance", B.quxxInstance.qux)
118+
assertEquals("quxxInstance", B.quxxInstance.quxHolder.qux)
119+
}
120+
121+
end ExportedJSNativeMembersScala3

0 commit comments

Comments
 (0)