Skip to content

Commit 7dc3d24

Browse files
committed
Fix scala-js/scala-js#4142: Support exporting nested classes / objects
Forward port of the upstream commit scala-js/scala-js@a8d428e
1 parent 4d7354b commit 7dc3d24

File tree

3 files changed

+95
-85
lines changed

3 files changed

+95
-85
lines changed

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

Lines changed: 89 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -54,23 +54,19 @@ object PrepJSExports {
5454
// Scala classes are never exported: Their constructors are.
5555
val isScalaClass = sym.isClass && !sym.isOneOf(Trait | Module) && !isJSAny(sym)
5656

57+
// Filter constructors of module classes: The module classes themselves will be exported.
58+
val isModuleClassCtor = sym.isConstructor && sym.owner.is(ModuleClass)
59+
5760
val exports =
58-
if (isScalaClass) Nil
61+
if (isScalaClass || isModuleClassCtor) Nil
5962
else exportsOf(sym)
6063

6164
assert(exports.isEmpty || !sym.is(Bridge),
6265
s"found exports for bridge symbol $sym. exports: $exports")
6366

64-
if (sym.isClass || sym.isConstructor) {
65-
/* we can generate constructors, classes and modules entirely in the backend,
66-
* since they do not need inheritance and such.
67-
*/
68-
Nil
69-
} else {
70-
// For normal exports, generate exporter methods.
71-
val normalExports = exports.filter(_.destination == ExportDestination.Normal)
72-
normalExports.flatMap(exp => genExportDefs(sym, exp.jsName, exp.pos.span))
73-
}
67+
// For normal exports, generate exporter methods.
68+
val normalExports = exports.filter(_.destination == ExportDestination.Normal)
69+
normalExports.flatMap(exp => genExportDefs(sym, exp.jsName, exp.pos.span))
7470
}
7571

7672
/** Computes the ExportInfos for sym from its annotations. */
@@ -83,6 +79,10 @@ object PrepJSExports {
8379
else sym
8480
}
8581

82+
val symOwner =
83+
if (sym.isConstructor) sym.owner.owner
84+
else sym.owner
85+
8686
val JSExportAnnot = jsdefn.JSExportAnnot
8787
val JSExportTopLevelAnnot = jsdefn.JSExportTopLevelAnnot
8888
val JSExportStaticAnnot = jsdefn.JSExportStaticAnnot
@@ -92,13 +92,22 @@ object PrepJSExports {
9292
val directMemberAnnots = Set[Symbol](JSExportAnnot, JSExportTopLevelAnnot, JSExportStaticAnnot)
9393
val directAnnots = trgSym.annotations.filter(annot => directMemberAnnots.contains(annot.symbol))
9494

95-
// Is this a member export (i.e. not a class or module export)?
96-
val isMember = !sym.isClass && !sym.isConstructor
97-
98-
// Annotations for this member on the whole unit
95+
/* Annotations for this member on the whole unit
96+
*
97+
* Note that for top-level classes / modules this is always empty, because
98+
* packages cannot have annotations.
99+
*/
99100
val unitAnnots = {
100-
if (isMember && sym.isPublic && !sym.is(Synthetic))
101-
sym.owner.annotations.filter(_.symbol == JSExportAllAnnot)
101+
val useExportAll = {
102+
sym.isPublic &&
103+
!sym.is(Synthetic) &&
104+
!sym.isConstructor &&
105+
!sym.is(Trait) &&
106+
(!sym.isClass || sym.is(ModuleClass))
107+
}
108+
109+
if (useExportAll)
110+
symOwner.annotations.filter(_.symbol == JSExportAllAnnot)
102111
else
103112
Nil
104113
}
@@ -139,7 +148,13 @@ object PrepJSExports {
139148
"dummy"
140149
}
141150
} else {
142-
sym.defaultJSName
151+
val name = (if (sym.isConstructor) sym.owner else sym).defaultJSName
152+
if (name.endsWith(str.SETTER_SUFFIX) && !sym.isJSSetter) {
153+
report.error(
154+
"You must set an explicit name when exporting a non-setter with a name ending in _=",
155+
exportPos)
156+
}
157+
name
143158
}
144159
}
145160

@@ -166,20 +181,12 @@ object PrepJSExports {
166181
if (!isTopLevelExport && name.contains("__"))
167182
report.error("An exported name may not contain a double underscore (`__`)", exportPos)
168183

169-
val symOwner =
170-
if (sym.isConstructor) sym.owner.owner
171-
else sym.owner
172-
173184
// Destination-specific restrictions
174185
destination match {
175186
case ExportDestination.Normal =>
176-
// Disallow @JSExport at the top-level, as well as on objects and classes
187+
// Disallow @JSExport on top-level definitions.
177188
if (symOwner.is(Package) || symOwner.isPackageObject) {
178189
report.error("@JSExport is forbidden on top-level definitions. Use @JSExportTopLevel instead.", exportPos)
179-
} else if (!isMember && !sym.is(Trait)) {
180-
report.error(
181-
"@JSExport is forbidden on objects and classes. Use @JSExport'ed factory methods instead.",
182-
exportPos)
183190
}
184191

185192
// Make sure we do not override the default export of toString
@@ -243,19 +250,19 @@ object PrepJSExports {
243250
exportPos)
244251
}
245252

246-
if (isMember) {
247-
if (sym.is(Lazy))
248-
report.error("You may not export a lazy val as static", exportPos)
253+
if (sym.is(Lazy))
254+
report.error("You may not export a lazy val as static", exportPos)
249255

250-
// Illegal function application export
251-
if (!hasExplicitName && sym.name == nme.apply) {
252-
report.error(
253-
"A member cannot be exported to function application as " +
254-
"static. Use @JSExportStatic(\"apply\") to export it under " +
255-
"the name 'apply'.",
256-
exportPos)
257-
}
258-
} else {
256+
// Illegal function application export
257+
if (!hasExplicitName && sym.name == nme.apply) {
258+
report.error(
259+
"A member cannot be exported to function application as " +
260+
"static. Use @JSExportStatic(\"apply\") to export it under " +
261+
"the name 'apply'.",
262+
exportPos)
263+
}
264+
265+
if (sym.isClass || sym.isConstructor) {
259266
report.error("Implementation restriction: cannot export a class or object as static", exportPos)
260267
}
261268
}
@@ -375,31 +382,41 @@ object PrepJSExports {
375382
}
376383

377384
/** Generates an exporter for a DefDef including default parameter methods. */
378-
private def genExportDefs(defSym: Symbol, jsName: String, span: Span)(using Context): List[Tree] = {
379-
val clsSym = defSym.owner.asClass
385+
private def genExportDefs(sym: Symbol, jsName: String, span: Span)(using Context): List[Tree] = {
386+
val siblingSym =
387+
if (sym.isConstructor) sym.owner
388+
else sym
389+
390+
val clsSym = siblingSym.owner.asClass
391+
392+
val isProperty = sym.is(ModuleClass) || isJSAny(sym) || sym.isJSProperty
393+
394+
val copiedFlags0 = (siblingSym.flags & (Protected | Final)).toTermFlags
395+
val copiedFlags =
396+
if (siblingSym.is(HasDefaultParams)) copiedFlags0 | HasDefaultParams // term flag only
397+
else copiedFlags0
380398

381399
// Create symbol for new method
382-
val name = makeExportName(jsName, !defSym.is(Method) || defSym.isJSProperty)
383-
val flags = (defSym.flags | Method | Synthetic)
384-
&~ (Deferred | Accessor | ParamAccessor | CaseAccessor | Mutable | Lazy | Override)
400+
val scalaName = makeExportName(jsName, !sym.is(Method) || sym.isJSProperty)
401+
val flags = Method | Synthetic | copiedFlags
385402
val info =
386-
if (defSym.isConstructor) defSym.info
387-
else if (defSym.is(Method)) finalResultTypeToAny(defSym.info)
403+
if (sym.isConstructor) sym.info
404+
else if (sym.is(Method)) finalResultTypeToAny(sym.info)
388405
else ExprType(defn.AnyType)
389-
val expSym = newSymbol(clsSym, name, flags, info, defSym.privateWithin, span).entered
406+
val expSym = newSymbol(clsSym, scalaName, flags, info, sym.privateWithin, span).entered
390407

391408
// Construct exporter DefDef tree
392-
val exporter = genProxyDefDef(clsSym, defSym, expSym, span)
409+
val exporter = genProxyDefDef(clsSym, sym, expSym, span)
393410

394411
// Construct exporters for default getters
395-
val defaultGetters = if (!defSym.hasDefaultParams) {
412+
val defaultGetters = if (!sym.hasDefaultParams) {
396413
Nil
397414
} else {
398415
for {
399-
(param, i) <- defSym.paramSymss.flatten.zipWithIndex
416+
(param, i) <- sym.paramSymss.flatten.zipWithIndex
400417
if param.is(HasDefault)
401418
} yield {
402-
genExportDefaultGetter(clsSym, defSym, expSym, i, span)
419+
genExportDefaultGetter(clsSym, sym, expSym, i, span)
403420
}
404421
}
405422

@@ -435,7 +452,27 @@ object PrepJSExports {
435452
proxySym: TermSymbol, span: Span)(using Context): Tree = {
436453

437454
DefDef(proxySym, { argss =>
438-
This(clsSym).select(trgSym).appliedToArgss(argss)
455+
if (trgSym.isConstructor) {
456+
val tycon = trgSym.owner.typeRef
457+
New(tycon).select(TermRef(tycon, trgSym)).appliedToArgss(argss)
458+
} else if (trgSym.is(ModuleClass)) {
459+
assert(argss.isEmpty,
460+
s"got a module export with non-empty paramss. target: $trgSym, proxy: $proxySym at $span")
461+
ref(trgSym.sourceModule)
462+
} else if (trgSym.isClass) {
463+
assert(isJSAny(trgSym), s"got a class export for a non-JS class ($trgSym) at $span")
464+
val tpe = argss match {
465+
case Nil =>
466+
trgSym.typeRef
467+
case (targs @ (first :: _)) :: Nil if first.isType =>
468+
trgSym.typeRef.appliedTo(targs.map(_.tpe))
469+
case _ =>
470+
throw AssertionError(s"got a class export with unexpected paramss. target: $trgSym, proxy: $proxySym at $span")
471+
}
472+
ref(jsdefn.JSPackage_constructorOf).appliedToType(tpe)
473+
} else {
474+
This(clsSym).select(trgSym).appliedToArgss(argss)
475+
}
439476
}).withSpan(span)
440477
}
441478

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP
161161
tree match {
162162
case tree: TypeDef if tree.isClassDef =>
163163
val exports = genExport(sym)
164-
assert(exports.isEmpty, s"got non-empty exports for $sym")
164+
if (exports.nonEmpty)
165+
exporters.getOrElseUpdate(sym.owner, mutable.ListBuffer.empty) ++= exports
165166

166167
if (isJSAny(sym))
167168
transformJSClassDef(tree)
@@ -174,8 +175,10 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP
174175
case tree: ValOrDefDef =>
175176
// Prepare exports
176177
val exports = genExport(sym)
177-
if (exports.nonEmpty)
178-
exporters.getOrElseUpdate(sym.owner, mutable.ListBuffer.empty) ++= exports
178+
if (exports.nonEmpty) {
179+
val target = if (sym.isConstructor) sym.owner.owner else sym.owner
180+
exporters.getOrElseUpdate(target, mutable.ListBuffer.empty) ++= exports
181+
}
179182

180183
if (sym.isLocalToBlock)
181184
super.transform(tree)

tests/neg-scalajs/jsexport-on-non-toplevel-class-object.scala

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)