
org.scalajs.nscplugin.PrepJSExports.scala Maven / Gradle / Ivy
/*
* Scala.js (https://www.scala-js.org/)
*
* Copyright EPFL.
*
* Licensed under Apache License 2.0
* (https://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/
package org.scalajs.nscplugin
import scala.collection.mutable
import scala.tools.nsc.Global
import org.scalajs.ir.Names.DefaultModuleID
import org.scalajs.ir.Trees.TopLevelExportDef.isValidTopLevelExportName
/**
* Prepare export generation
*
* Helpers for transformation of @JSExport annotations
*/
trait PrepJSExports[G <: Global with Singleton] { this: PrepJSInterop[G] =>
import global._
import jsAddons._
import definitions._
import jsDefinitions._
import scala.reflect.internal.Flags
private sealed abstract class ExportDestination
private object ExportDestination {
/** Export in the "normal" way: as an instance member, or at the top-level
* for naturally top-level things (classes and modules).
*/
case object Normal extends ExportDestination
/** Export at the top-level. */
case class TopLevel(moduleID: String) extends ExportDestination
/** Export as a static member of the companion class. */
case object Static extends ExportDestination
}
/* Not final because it causes the following compile warning:
* "The outer reference in this type test cannot be checked at run time."
*/
private case class ExportInfo(jsName: String,
destination: ExportDestination)(val pos: Position)
/** Generate the exporter for the given DefDef
* or ValDef (abstract val in class, val in trait or lazy val;
* these don't get DefDefs until the fields phase)
*
* If this DefDef is a constructor, it is registered to be exported by
* GenJSCode instead and no trees are returned.
*/
def genExportMember(baseSym: Symbol): List[Tree] = {
val clsSym = baseSym.owner
val exports = exportsOf(baseSym)
// Helper function for errors
def err(msg: String) = {
reporter.error(exports.head.pos, msg)
Nil
}
def memType = if (baseSym.isConstructor) "constructor" else "method"
if (exports.isEmpty) {
Nil
} else if (!hasLegalExportVisibility(baseSym)) {
err(s"You may only export public and protected ${memType}s")
} else if (baseSym.isMacro) {
err("You may not export a macro")
} else if (isJSAny(clsSym)) {
err(s"You may not export a $memType of a subclass of js.Any")
} else if (scalaPrimitives.isPrimitive(baseSym)) {
err("You may not export a primitive")
} else if (baseSym.isLocalToBlock) {
// We exclude case class apply (and unapply) to work around SI-8826
if (clsSym.isCaseApplyOrUnapply) {
Nil
} else {
err("You may not export a local definition")
}
} else if (hasIllegalRepeatedParam(baseSym)) {
err(s"In an exported $memType, a *-parameter must come last " +
"(through all parameter lists)")
} else if (hasIllegalDefaultParam(baseSym)) {
err(s"In an exported $memType, all parameters with defaults " +
"must be at the end")
} else if (baseSym.isConstructor) {
// we can generate constructors entirely in the backend, since they
// do not need inheritance and such. But we want to check their sanity
// here by previous tests and the following ones.
if (checkClassOrModuleExports(clsSym, exports.head.pos))
registerStaticAndTopLevelExports(baseSym, exports)
Nil
} else {
assert(!baseSym.isBridge,
s"genExportMember called for bridge symbol $baseSym")
// Reset interface flag: Any trait will contain non-empty methods
clsSym.resetFlag(Flags.INTERFACE)
val (normalExports, topLevelAndStaticExports) =
exports.partition(_.destination == ExportDestination.Normal)
/* We can handle top level exports and static exports entirely in the
* backend. So just register them here.
*/
registerStaticAndTopLevelExports(baseSym, topLevelAndStaticExports)
// Actually generate exporter methods
normalExports.flatMap(exp => genExportDefs(baseSym, exp.jsName, exp.pos))
}
}
/** Check and (potentially) register a class or module for export.
*
* Note that Scala classes are never registered for export, their
* constructors are.
*/
def registerClassOrModuleExports(sym: Symbol): Unit = {
val exports = exportsOf(sym)
def isScalaClass = !sym.isModuleClass && !isJSAny(sym)
if (exports.nonEmpty && checkClassOrModuleExports(sym, exports.head.pos) &&
!isScalaClass) {
registerStaticAndTopLevelExports(sym, exports)
}
}
private def registerStaticAndTopLevelExports(sym: Symbol,
exports: List[ExportInfo]): Unit = {
val topLevel = exports.collect {
case info @ ExportInfo(jsName, ExportDestination.TopLevel(moduleID)) =>
jsInterop.TopLevelExportInfo(moduleID, jsName)(info.pos)
}
if (topLevel.nonEmpty)
jsInterop.registerTopLevelExports(sym, topLevel)
val static = exports.collect {
case info @ ExportInfo(jsName, ExportDestination.Static) =>
jsInterop.StaticExportInfo(jsName)(info.pos)
}
if (static.nonEmpty)
jsInterop.registerStaticExports(sym, static)
}
/** Check a class or module for export.
*
* There are 2 ways that this method can be reached:
* - via `registerClassOrModuleExports`
* - via `genExportMember` (constructor of Scala class)
*/
private def checkClassOrModuleExports(sym: Symbol, errPos: Position): Boolean = {
val isMod = sym.isModuleClass
def err(msg: String) = {
reporter.error(errPos, msg)
false
}
def hasAnyNonPrivateCtor: Boolean =
sym.info.member(nme.CONSTRUCTOR).filter(!isPrivateMaybeWithin(_)).exists
def isJSNative = sym.hasAnnotation(JSNativeAnnotation)
if (sym.isTrait) {
err("You may not export a trait")
} else if (isJSNative) {
err("You may not export a native JS " + (if (isMod) "object" else "class"))
} else if (!hasLegalExportVisibility(sym)) {
err("You may only export public and protected " +
(if (isMod) "objects" else "classes"))
} else if (sym.isLocalToBlock) {
err("You may not export a local " +
(if (isMod) "object" else "class"))
} else if (!sym.isStatic) {
err("You may not export a nested " +
(if (isMod) "object" else s"class. $createFactoryInOuterClassHint"))
} else if (sym.isAbstractClass && !isJSAny(sym)) {
err("You may not export an abstract class")
} else if (!isMod && !hasAnyNonPrivateCtor) {
/* This test is only relevant for JS classes but doesn't hurt for Scala
* classes as we could not reach it if there were only private
* constructors.
*/
err("You may not export a class that has only private constructors")
} else {
true
}
}
private def createFactoryInOuterClassHint = {
"Create an exported factory method in the outer class to work " +
"around this limitation."
}
/** retrieves the names a sym should be exported to from its annotations
*
* Note that for accessor symbols, the annotations of the accessed symbol
* are used, rather than the annotations of the accessor itself.
*/
private def exportsOf(sym: Symbol): List[ExportInfo] = {
val trgSym = {
def isOwnerScalaClass = !sym.owner.isModuleClass && !isJSAny(sym.owner)
// For primary Scala class constructors, look on the class itself
if (sym.isPrimaryConstructor && isOwnerScalaClass) sym.owner
else sym
}
// Annotations that are directly on the member
val directAnnots = trgSym.annotations.filter(
annot => isDirectMemberAnnot(annot.symbol))
// Is this a member export (i.e. not a class or module export)?
val isMember = !sym.isClass && !sym.isConstructor
// Annotations for this member on the whole unit
val unitAnnots = {
if (isMember && sym.isPublic && !sym.isSynthetic)
sym.owner.annotations.filter(_.symbol == JSExportAllAnnotation)
else
Nil
}
val allExportInfos = for {
annot <- directAnnots ++ unitAnnots
} yield {
val isExportAll = annot.symbol == JSExportAllAnnotation
val isTopLevelExport = annot.symbol == JSExportTopLevelAnnotation
val isStaticExport = annot.symbol == JSExportStaticAnnotation
val hasExplicitName = annot.args.nonEmpty
assert(!isTopLevelExport || hasExplicitName,
"Found a top-level export without an explicit name at " + annot.pos)
val name = {
if (hasExplicitName) {
annot.stringArg(0).getOrElse {
reporter.error(annot.args(0).pos,
s"The argument to ${annot.symbol.name} must be a literal string")
"dummy"
}
} else if (sym.isConstructor) {
decodedFullName(sym.owner)
} else if (sym.isClass) {
decodedFullName(sym)
} else {
sym.unexpandedName.decoded.stripSuffix("_=")
}
}
val destination = {
if (isTopLevelExport) {
val moduleID = if (annot.args.size == 1) {
DefaultModuleID
} else {
annot.stringArg(1).getOrElse {
reporter.error(annot.args(1).pos,
"moduleID must be a literal string")
DefaultModuleID
}
}
ExportDestination.TopLevel(moduleID)
} else if (isStaticExport) {
ExportDestination.Static
} else {
ExportDestination.Normal
}
}
// Enforce proper setter signature
if (jsInterop.isJSSetter(sym))
checkSetterSignature(sym, annot.pos, exported = true)
// Enforce no __ in name
if (!isTopLevelExport && name.contains("__")) {
// Get position for error message
val pos = if (hasExplicitName) annot.args.head.pos else trgSym.pos
reporter.error(pos,
"An exported name may not contain a double underscore (`__`)")
}
/* Illegal function application exports, i.e., method named 'apply'
* without an explicit export name.
*/
if (isMember && !hasExplicitName && sym.name == nme.apply) {
destination match {
case ExportDestination.Normal =>
def shouldBeTolerated = {
isExportAll && directAnnots.exists { annot =>
annot.symbol == JSExportAnnotation &&
annot.args.nonEmpty &&
annot.stringArg(0) == Some("apply")
}
}
// Don't allow apply without explicit name
if (!shouldBeTolerated) {
// Get position for error message
val pos = if (isExportAll) trgSym.pos else annot.pos
reporter.error(pos, "A member cannot be exported to function " +
"application. Add @JSExport(\"apply\") to export under the " +
"name apply.")
}
case _: ExportDestination.TopLevel =>
throw new AssertionError(
"Found a top-level export without an explicit name at " +
annot.pos)
case ExportDestination.Static =>
reporter.error(annot.pos,
"A member cannot be exported to function application as " +
"static. Use @JSExportStatic(\"apply\") to export it under " +
"the name 'apply'.")
}
}
val symOwner =
if (sym.isConstructor) sym.owner.owner
else sym.owner
// Destination-specific restrictions
destination match {
case ExportDestination.Normal =>
// Make sure we do not override the default export of toString
def isIllegalToString = {
isMember && name == "toString" && sym.name != nme.toString_ &&
sym.tpe.params.isEmpty && !jsInterop.isJSGetter(sym)
}
if (isIllegalToString) {
reporter.error(annot.pos, "You may not export a zero-argument " +
"method named other than 'toString' under the name 'toString'")
}
// Disallow @JSExport on non-members.
if (!isMember && !sym.isTrait) {
reporter.error(annot.pos,
"@JSExport is forbidden on objects and classes. " +
"Use @JSExportTopLevel instead.")
}
case _: ExportDestination.TopLevel =>
if (sym.isLazy) {
reporter.error(annot.pos,
"You may not export a lazy val to the top level")
} else if (!sym.isAccessor && jsInterop.isJSProperty(sym)) {
reporter.error(annot.pos,
"You may not export a getter or a setter to the top level")
}
/* Disallow non-static methods.
* Note: Non-static classes have more specific error messages in
* checkClassOrModuleExports
*/
if (sym.isMethod && (!symOwner.isStatic || !symOwner.isModuleClass)) {
reporter.error(annot.pos,
"Only static objects may export their members to the top level")
}
// The top-level name must be a valid JS identifier
if (!isValidTopLevelExportName(name)) {
reporter.error(annot.pos,
"The top-level export name must be a valid JavaScript " +
"identifier name")
}
case ExportDestination.Static =>
def companionIsNonNativeJSClass: Boolean = {
val companion = symOwner.companionClass
companion != NoSymbol &&
!companion.isTrait &&
isJSAny(companion) &&
!companion.hasAnnotation(JSNativeAnnotation)
}
if (!symOwner.isStatic || !symOwner.isModuleClass ||
!companionIsNonNativeJSClass) {
reporter.error(annot.pos,
"Only a static object whose companion class is a " +
"non-native JS class may export its members as static.")
}
if (isMember) {
if (sym.isLazy) {
reporter.error(annot.pos,
"You may not export a lazy val as static")
}
} else {
if (sym.isTrait) {
reporter.error(annot.pos,
"You may not export a trait as static.")
} else {
reporter.error(annot.pos,
"Implementation restriction: cannot export a class or " +
"object as static")
}
}
}
ExportInfo(name, destination)(annot.pos)
}
allExportInfos.filter(_.destination == ExportDestination.Normal)
.groupBy(_.jsName)
.filter { case (jsName, group) =>
if (jsName == "apply" && group.size == 2)
// @JSExportAll and single @JSExport("apply") should not be warned.
!unitAnnots.exists(_.symbol == JSExportAllAnnotation)
else
group.size > 1
}
.foreach(_ => reporter.warning(sym.pos, s"Found duplicate @JSExport"))
/* Filter out static exports of accessors (as they are not actually
* exported, their fields are). The above is only used to uniformly perform
* checks.
*/
val filteredExports = if (!sym.isAccessor || sym.accessed == NoSymbol) {
allExportInfos
} else {
/* For accessors, we need to apply some special logic to static exports.
* When tested on accessors, they actually apply on *fields*, not on the
* accessors. We use the same code paths hereabove to uniformly perform
* relevant checks, but at the end of the day, we have to throw away the
* ExportInfo.
* However, we must make sure that no field is exported *twice* as static,
* nor both as static and as top-level (it is possible to export a field
* several times as top-level, though).
*/
val (topLevelAndStaticExportInfos, actualExportInfos) =
allExportInfos.partition(_.destination != ExportDestination.Normal)
if (sym.isGetter) {
topLevelAndStaticExportInfos.find {
_.destination == ExportDestination.Static
}.foreach { firstStatic =>
for {
duplicate <- topLevelAndStaticExportInfos
if duplicate ne firstStatic
} {
if (duplicate.destination == ExportDestination.Static) {
reporter.error(duplicate.pos,
"Fields (val or var) cannot be exported as static more " +
"than once")
} else {
reporter.error(duplicate.pos,
"Fields (val or var) cannot be exported both as static " +
"and at the top-level")
}
}
}
registerStaticAndTopLevelExports(sym.accessed, topLevelAndStaticExportInfos)
}
actualExportInfos
}
filteredExports.distinct
}
/** Just like sym.fullName, but does not encode components */
private def decodedFullName(sym: Symbol): String = {
if (sym.isRoot || sym.isRootPackage || sym == NoSymbol) sym.name.decoded
else if (sym.owner.isEffectiveRoot) sym.name.decoded
else decodedFullName(sym.effectiveOwner.enclClass) + '.' + sym.name.decoded
}
/** generate an exporter for a DefDef including default parameter methods */
private def genExportDefs(defSym: Symbol, jsName: String, pos: Position) = {
val clsSym = defSym.owner
val scalaName =
jsInterop.scalaExportName(jsName, jsInterop.isJSProperty(defSym))
// Create symbol for new method
val expSym = defSym.cloneSymbol
// Set position of symbol
expSym.pos = pos
// Alter type for new method (lift return type to Any)
// The return type is lifted, in order to avoid bridge
// construction and to detect methods whose signature only differs
// in the return type.
// Attention: This will cause boxes for primitive value types and value
// classes. However, since we have restricted the return types, we can
// always safely remove these boxes again in the back-end.
if (!defSym.isConstructor)
expSym.setInfo(retToAny(expSym.tpe))
// Change name for new method
expSym.name = scalaName
// Update flags
expSym.setFlag(Flags.SYNTHETIC)
expSym.resetFlag(
Flags.DEFERRED | // We always have a body
Flags.ACCESSOR | // We are never a "direct" accessor
Flags.CASEACCESSOR | // And a fortiori not a case accessor
Flags.LAZY | // We are not a lazy val (even if we export one)
Flags.OVERRIDE // Synthetic methods need not bother with this
)
// Remove export annotations
expSym.removeAnnotation(JSExportAnnotation)
// Add symbol to class
clsSym.info.decls.enter(expSym)
// Construct exporter DefDef tree
val exporter = genProxyDefDef(clsSym, defSym, expSym, pos)
// Construct exporters for default getters
val defaultGetters = for {
(param, i) <- expSym.paramss.flatten.zipWithIndex
if param.hasFlag(Flags.DEFAULTPARAM)
} yield genExportDefaultGetter(clsSym, defSym, expSym, i + 1, pos)
exporter :: defaultGetters
}
private def genExportDefaultGetter(clsSym: Symbol, trgMethod: Symbol,
exporter: Symbol, paramPos: Int, pos: Position) = {
// Get default getter method we'll copy
val trgGetter =
clsSym.tpe.member(nme.defaultGetterName(trgMethod.name, paramPos))
assert(trgGetter.exists,
s"Cannot find default getter for param $paramPos of $trgMethod")
// Although the following must be true in a correct program, we cannot
// assert, since a graceful failure message is only generated later
if (!trgGetter.isOverloaded) {
val expGetter = trgGetter.cloneSymbol
expGetter.name = nme.defaultGetterName(exporter.name, paramPos)
expGetter.pos = pos
clsSym.info.decls.enter(expGetter)
genProxyDefDef(clsSym, trgGetter, expGetter, pos)
} else EmptyTree
}
/** generate a DefDef tree (from [[proxySym]]) that calls [[trgSym]] */
private def genProxyDefDef(clsSym: Symbol, trgSym: Symbol,
proxySym: Symbol, pos: Position) = atPos(pos) {
// Helper to ascribe repeated argument lists when calling
def spliceParam(sym: Symbol) = {
if (isRepeated(sym))
Typed(Ident(sym), Ident(tpnme.WILDCARD_STAR))
else
Ident(sym)
}
// Construct proxied function call
val sel = Select(This(clsSym), trgSym)
val rhs = proxySym.paramss.foldLeft[Tree](sel) {
(fun,params) => Apply(fun, params map spliceParam)
}
typer.typedDefDef(DefDef(proxySym, rhs))
}
/** changes the return type of the method type tpe to Any. returns new type */
private def retToAny(tpe: Type): Type = tpe match {
case MethodType(params, result) => MethodType(params, retToAny(result))
case NullaryMethodType(result) => NullaryMethodType(AnyClass.tpe)
case PolyType(tparams, result) => PolyType(tparams, retToAny(result))
case _ => AnyClass.tpe
}
/** Whether the given symbol has a visibility that allows exporting */
private def hasLegalExportVisibility(sym: Symbol): Boolean =
sym.isPublic || sym.isProtected && !sym.isProtectedLocal
/** checks whether this type has a repeated parameter elsewhere than at the end
* of all the params
*/
private def hasIllegalRepeatedParam(sym: Symbol): Boolean = {
val params = sym.paramss.flatten
params.nonEmpty && params.init.exists(isRepeated _)
}
/** checks whether there are default parameters not at the end of
* the flattened parameter list
*/
private def hasIllegalDefaultParam(sym: Symbol): Boolean = {
val isDefParam = (_: Symbol).hasFlag(Flags.DEFAULTPARAM)
sym.paramss.flatten.reverse.dropWhile(isDefParam).exists(isDefParam)
}
/** Whether a symbol is an annotation that goes directly on a member */
private lazy val isDirectMemberAnnot = Set[Symbol](
JSExportAnnotation,
JSExportTopLevelAnnotation,
JSExportStaticAnnotation
)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy