Skip to content

Commit 4d7354b

Browse files
committed
Unify export target validation.
Instead of having separate checks for members and classes/modules, we unify them. This gives simpler code and will make it easier to allow nested objects. Additionally: We report a proper error when attempting to export a JS native member (instead of crashing the compiler). Forward port of the upstream commit scala-js/scala-js@14de17d
1 parent b3f0aca commit 4d7354b

File tree

2 files changed

+155
-162
lines changed

2 files changed

+155
-162
lines changed

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

Lines changed: 148 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -45,100 +45,31 @@ object PrepJSExports {
4545

4646
private final case class ExportInfo(jsName: String, destination: ExportDestination)(val pos: SrcPos)
4747

48-
/** Checks a class or module class for export.
48+
/** Generate exports for the given Symbol.
4949
*
50-
* Note that non-module Scala classes are never actually exported; their constructors are.
51-
* However, the checks are performed on the class when the class is annotated.
50+
* - Registers top-level and static exports.
51+
* - Returns (non-static) exporters for this symbol.
5252
*/
53-
def checkClassOrModuleExports(sym: Symbol)(using Context): Unit = {
54-
val exports = exportsOf(sym)
55-
if (exports.nonEmpty)
56-
checkClassOrModuleExports(sym, exports.head.pos)
57-
}
53+
def genExport(sym: Symbol)(using Context): List[Tree] = {
54+
// Scala classes are never exported: Their constructors are.
55+
val isScalaClass = sym.isClass && !sym.isOneOf(Trait | Module) && !isJSAny(sym)
5856

59-
/** Generate the exporter for the given DefDef or ValDef.
60-
*
61-
* If this DefDef is a constructor, it is registered to be exported by
62-
* GenJSCode instead and no trees are returned.
63-
*/
64-
def genExportMember(baseSym: Symbol)(using Context): List[Tree] = {
65-
val clsSym = baseSym.owner
57+
val exports =
58+
if (isScalaClass) Nil
59+
else exportsOf(sym)
6660

67-
val exports = exportsOf(baseSym)
61+
assert(exports.isEmpty || !sym.is(Bridge),
62+
s"found exports for bridge symbol $sym. exports: $exports")
6863

69-
// Helper function for errors
70-
def err(msg: String): List[Tree] = {
71-
report.error(msg, exports.head.pos)
72-
Nil
73-
}
74-
75-
def memType = if (baseSym.isConstructor) "constructor" else "method"
76-
77-
if (exports.isEmpty) {
78-
Nil
79-
} else if (!hasLegalExportVisibility(baseSym)) {
80-
err(s"You may only export public and protected ${memType}s")
81-
} else if (baseSym.is(Inline)) {
82-
err("You may not export an inline method")
83-
} else if (isJSAny(clsSym)) {
84-
err(s"You may not export a $memType of a subclass of js.Any")
85-
} else if (baseSym.isLocalToBlock) {
86-
err("You may not export a local definition")
87-
} else if (hasIllegalRepeatedParam(baseSym)) {
88-
err(s"In an exported $memType, a *-parameter must come last (through all parameter lists)")
89-
} else if (hasIllegalDefaultParam(baseSym)) {
90-
err(s"In an exported $memType, all parameters with defaults must be at the end")
91-
} else if (baseSym.isConstructor) {
92-
// Constructors do not need an exporter method. We only perform the checks at this phase.
93-
checkClassOrModuleExports(clsSym, exports.head.pos)
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+
*/
9468
Nil
9569
} else {
96-
assert(!baseSym.is(Bridge), s"genExportMember called for bridge symbol $baseSym")
70+
// For normal exports, generate exporter methods.
9771
val normalExports = exports.filter(_.destination == ExportDestination.Normal)
98-
normalExports.flatMap(exp => genExportDefs(baseSym, exp.jsName, exp.pos.span))
99-
}
100-
}
101-
102-
/** Check a class or module for export.
103-
*
104-
* There are 2 ways that this method can be reached:
105-
* - via `registerClassExports`
106-
* - via `genExportMember` (constructor of Scala class)
107-
*/
108-
private def checkClassOrModuleExports(sym: Symbol, errPos: SrcPos)(using Context): Unit = {
109-
val isMod = sym.is(ModuleClass)
110-
111-
def err(msg: String): Unit =
112-
report.error(msg, errPos)
113-
114-
def hasAnyNonPrivateCtor: Boolean =
115-
sym.info.decl(nme.CONSTRUCTOR).hasAltWith(denot => !isPrivateMaybeWithin(denot.symbol))
116-
117-
if (sym.is(Trait)) {
118-
err("You may not export a trait")
119-
} else if (sym.hasAnnotation(jsdefn.JSNativeAnnot)) {
120-
err("You may not export a native JS " + (if (isMod) "object" else "class"))
121-
} else if (!hasLegalExportVisibility(sym)) {
122-
err("You may only export public and protected " + (if (isMod) "objects" else "classes"))
123-
} else if (isJSAny(sym.owner)) {
124-
err("You may not export a " + (if (isMod) "object" else "class") + " in a subclass of js.Any")
125-
} else if (sym.isLocalToBlock) {
126-
err("You may not export a local " + (if (isMod) "object" else "class"))
127-
} else if (!sym.isStatic) {
128-
if (isMod)
129-
err("You may not export a nested object")
130-
else
131-
err("You may not export a nested class. Create an exported factory method in the outer class to work around this limitation.")
132-
} else if (sym.is(Abstract, butNot = Trait) && !isJSAny(sym)) {
133-
err("You may not export an abstract class")
134-
} else if (!isMod && !hasAnyNonPrivateCtor) {
135-
/* This test is only relevant for JS classes but doesn't hurt for Scala
136-
* classes as we could not reach it if there were only private
137-
* constructors.
138-
*/
139-
err("You may not export a class that has only private constructors")
140-
} else {
141-
// OK
72+
normalExports.flatMap(exp => genExportDefs(sym, exp.jsName, exp.pos.span))
14273
}
14374
}
14475

@@ -172,8 +103,22 @@ object PrepJSExports {
172103
Nil
173104
}
174105

106+
val allAnnots = {
107+
val allAnnots0 = directAnnots ++ unitAnnots
108+
109+
if (allAnnots0.nonEmpty) {
110+
val errorPos: SrcPos =
111+
if (allAnnots0.head.symbol == JSExportAllAnnot) sym
112+
else allAnnots0.head.tree
113+
if (checkExportTarget(sym, errorPos)) allAnnots0
114+
else Nil // prevent code generation from running to avoid crashes.
115+
} else {
116+
Nil
117+
}
118+
}
119+
175120
val allExportInfos = for {
176-
annot <- directAnnots ++ unitAnnots
121+
annot <- allAnnots
177122
} yield {
178123
val isExportAll = annot.symbol == JSExportAllAnnot
179124
val isTopLevelExport = annot.symbol == JSExportTopLevelAnnot
@@ -217,58 +162,29 @@ object PrepJSExports {
217162
}
218163
}
219164

220-
// Enforce proper setter signature
221-
if (sym.isJSSetter)
222-
checkSetterSignature(sym, exportPos, exported = true)
223-
224165
// Enforce no __ in name
225166
if (!isTopLevelExport && name.contains("__"))
226167
report.error("An exported name may not contain a double underscore (`__`)", exportPos)
227168

228-
/* Illegal function application exports, i.e., method named 'apply'
229-
* without an explicit export name.
230-
*/
231-
if (isMember && !hasExplicitName && sym.name == nme.apply) {
232-
destination match {
233-
case ExportDestination.Normal =>
234-
def shouldBeTolerated = {
235-
isExportAll && directAnnots.exists { annot =>
236-
annot.symbol == JSExportAnnot &&
237-
annot.arguments.nonEmpty &&
238-
annot.argumentConstantString(0).contains("apply")
239-
}
240-
}
241-
242-
// Don't allow apply without explicit name
243-
if (!shouldBeTolerated) {
244-
report.error(
245-
"A member cannot be exported to function application. " +
246-
"Add @JSExport(\"apply\") to export under the name apply.",
247-
exportPos)
248-
}
249-
250-
case _: ExportDestination.TopLevel =>
251-
throw new AssertionError(
252-
em"Found a top-level export without an explicit name at ${exportPos.sourcePos}")
253-
254-
case ExportDestination.Static =>
255-
report.error(
256-
"A member cannot be exported to function application as static. " +
257-
"Use @JSExportStatic(\"apply\") to export it under the name 'apply'.",
258-
exportPos)
259-
}
260-
}
261-
262169
val symOwner =
263170
if (sym.isConstructor) sym.owner.owner
264171
else sym.owner
265172

266173
// Destination-specific restrictions
267174
destination match {
268175
case ExportDestination.Normal =>
176+
// Disallow @JSExport at the top-level, as well as on objects and classes
177+
if (symOwner.is(Package) || symOwner.isPackageObject) {
178+
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)
183+
}
184+
269185
// Make sure we do not override the default export of toString
270186
def isIllegalToString = {
271-
isMember && name == "toString" && sym.name != nme.toString_ &&
187+
name == "toString" && sym.name != nme.toString_ &&
272188
sym.info.paramInfoss.forall(_.isEmpty) && !sym.isJSGetter
273189
}
274190
if (isIllegalToString) {
@@ -277,13 +193,25 @@ object PrepJSExports {
277193
exportPos)
278194
}
279195

280-
// Disallow @JSExport at the top-level, as well as on objects and classes
281-
if (symOwner.is(Package) || symOwner.isPackageObject) {
282-
report.error("@JSExport is forbidden on top-level definitions. Use @JSExportTopLevel instead.", exportPos)
283-
} else if (!isMember && !sym.is(Trait)) {
284-
report.error(
285-
"@JSExport is forbidden on objects and classes. Use @JSExport'ed factory methods instead.",
286-
exportPos)
196+
/* Illegal function application exports, i.e., method named 'apply'
197+
* without an explicit export name.
198+
*/
199+
if (!hasExplicitName && sym.name == nme.apply) {
200+
def shouldBeTolerated = {
201+
isExportAll && directAnnots.exists { annot =>
202+
annot.symbol == JSExportAnnot &&
203+
annot.arguments.nonEmpty &&
204+
annot.argumentConstantString(0).contains("apply")
205+
}
206+
}
207+
208+
// Don't allow apply without explicit name
209+
if (!shouldBeTolerated) {
210+
report.error(
211+
"A member cannot be exported to function application. " +
212+
"Add @JSExport(\"apply\") to export under the name apply.",
213+
exportPos)
214+
}
287215
}
288216

289217
case _: ExportDestination.TopLevel =>
@@ -292,10 +220,8 @@ object PrepJSExports {
292220
else if (sym.is(Method, butNot = Accessor) && sym.isJSProperty)
293221
report.error("You may not export a getter or a setter to the top level", exportPos)
294222

295-
/* Disallow non-static methods.
296-
* Note: Non-static classes have more specific error messages in checkClassOrModuleExports.
297-
*/
298-
if (sym.isTerm && (!symOwner.isStatic || !symOwner.is(ModuleClass)))
223+
// Disallow non-static definitions.
224+
if (!symOwner.isStatic || !symOwner.is(ModuleClass))
299225
report.error("Only static objects may export their members to the top level", exportPos)
300226

301227
// The top-level name must be a valid JS identifier
@@ -320,11 +246,17 @@ object PrepJSExports {
320246
if (isMember) {
321247
if (sym.is(Lazy))
322248
report.error("You may not export a lazy val as static", exportPos)
249+
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+
}
323258
} else {
324-
if (sym.is(Trait))
325-
report.error("You may not export a trait as static.", exportPos)
326-
else
327-
report.error("Implementation restriction: cannot export a class or object as static", exportPos)
259+
report.error("Implementation restriction: cannot export a class or object as static", exportPos)
328260
}
329261
}
330262

@@ -342,9 +274,9 @@ object PrepJSExports {
342274
}
343275
.foreach(_ => report.warning("Found duplicate @JSExport", sym))
344276

345-
/* Make sure that no field is exported *twice* as static, nor both as
346-
* static and as top-level (it is possible to export a field several times
347-
* as top-level, though).
277+
/* Check that no field is exported *twice* as static, nor both as static
278+
* and as top-level (it is possible to export a field several times as
279+
* top-level, though).
348280
*/
349281
if (!sym.is(Method)) {
350282
for (firstStatic <- allExportInfos.find(_.destination == ExportDestination.Static)) {
@@ -370,6 +302,78 @@ object PrepJSExports {
370302
allExportInfos.distinct
371303
}
372304

305+
/** Checks whether the given target is suitable for export and exporting
306+
* should be performed.
307+
*
308+
* Reports any errors for unsuitable targets.
309+
* @returns a boolean indicating whether exporting should be performed. Note:
310+
* a result of true is not a guarantee that no error was emitted. But it is
311+
* a guarantee that the target is not "too broken" to run the rest of
312+
* the generation. This approximation is done to avoid having to complicate
313+
* shared code verifying conditions.
314+
*/
315+
private def checkExportTarget(sym: Symbol, errPos: SrcPos)(using Context): Boolean = {
316+
def err(msg: String): Boolean = {
317+
report.error(msg, errPos)
318+
false
319+
}
320+
321+
def hasLegalExportVisibility(sym: Symbol): Boolean =
322+
sym.isPublic || sym.is(Protected, butNot = Local)
323+
324+
def isMemberOfJSAny: Boolean =
325+
isJSAny(sym.owner) || (sym.isConstructor && isJSAny(sym.owner.owner))
326+
327+
def hasIllegalRepeatedParam: Boolean = {
328+
val paramInfos = sym.info.paramInfoss.flatten
329+
paramInfos.nonEmpty && paramInfos.init.exists(_.isRepeatedParam)
330+
}
331+
332+
def hasIllegalDefaultParam: Boolean = {
333+
sym.hasDefaultParams
334+
&& sym.paramSymss.flatten.reverse.dropWhile(_.is(HasDefault)).exists(_.is(HasDefault))
335+
}
336+
337+
def hasAnyNonPrivateCtor: Boolean =
338+
sym.info.member(nme.CONSTRUCTOR).hasAltWith(d => !isPrivateMaybeWithin(d.symbol))
339+
340+
if (sym.is(Trait)) {
341+
err("You may not export a trait")
342+
} else if (sym.hasAnnotation(jsdefn.JSNativeAnnot)) {
343+
err("You may not export a native JS definition")
344+
} else if (!hasLegalExportVisibility(sym)) {
345+
err("You may only export public and protected definitions")
346+
} else if (sym.isConstructor && !hasLegalExportVisibility(sym.owner)) {
347+
err("You may only export constructors of public and protected classes")
348+
} else if (sym.is(Macro)) {
349+
err("You may not export a macro")
350+
} else if (isMemberOfJSAny) {
351+
err("You may not export a member of a subclass of js.Any")
352+
} else if (sym.isLocalToBlock) {
353+
err("You may not export a local definition")
354+
} else if (sym.isConstructor && sym.owner.isLocalToBlock) {
355+
err("You may not export constructors of local classes")
356+
} else if (hasIllegalRepeatedParam) {
357+
err("In an exported method or constructor, a *-parameter must come last " +
358+
"(through all parameter lists)")
359+
} else if (hasIllegalDefaultParam) {
360+
err("In an exported method or constructor, all parameters with " +
361+
"defaults must be at the end")
362+
} else if (sym.isConstructor && sym.owner.is(Abstract, butNot = Trait) && !isJSAny(sym)) {
363+
err("You may not export an abstract class")
364+
} else if (sym.isClass && !sym.is(ModuleClass) && isJSAny(sym) && !hasAnyNonPrivateCtor) {
365+
/* This test is only relevant for JS classes: We'll complain on the
366+
* individual exported constructors in case of a Scala class.
367+
*/
368+
err("You may not export a class that has only private constructors")
369+
} else {
370+
if (sym.isJSSetter)
371+
checkSetterSignature(sym, errPos, exported = true)
372+
373+
true // ok even if a setter has the wrong signature.
374+
}
375+
}
376+
373377
/** Generates an exporter for a DefDef including default parameter methods. */
374378
private def genExportDefs(defSym: Symbol, jsName: String, span: Span)(using Context): List[Tree] = {
375379
val clsSym = defSym.owner.asClass
@@ -448,20 +452,4 @@ object PrepJSExports {
448452
case _ =>
449453
defn.AnyType
450454
}
451-
452-
/** Whether the given symbol has a visibility that allows exporting */
453-
private def hasLegalExportVisibility(sym: Symbol)(using Context): Boolean =
454-
sym.isPublic || sym.is(Protected, butNot = Local)
455-
456-
/** Checks whether this type has a repeated parameter elsewhere than at the end of all the params. */
457-
private def hasIllegalRepeatedParam(sym: Symbol)(using Context): Boolean = {
458-
val paramInfos = sym.info.paramInfoss.flatten
459-
paramInfos.nonEmpty && paramInfos.init.exists(_.isRepeatedParam)
460-
}
461-
462-
/** Checks whether there are default parameters not at the end of the flattened parameter list. */
463-
private def hasIllegalDefaultParam(sym: Symbol)(using Context): Boolean = {
464-
sym.hasDefaultParams
465-
&& sym.paramSymss.flatten.reverse.dropWhile(_.is(HasDefault)).exists(_.is(HasDefault))
466-
}
467455
}

0 commit comments

Comments
 (0)