/* Scala.js compiler
* Copyright 2013 LAMP/EPFL
* @author Tobias Schlatter
*/
package scala.scalajs.compiler
import scala.tools.nsc
import nsc._
import scala.collection.immutable.ListMap
import scala.collection.mutable
/** Prepares classes extending js.Any for JavaScript interop
*
* This phase does:
* - Sanity checks for js.Any hierarchy
* - Annotate subclasses of js.Any to be treated specially
* - Rewrite calls to scala.Enumeration.Value (include name string)
* - Create JSExport methods: Dummy methods that are propagated
* through the whole compiler chain to mark exports. This allows
* exports to have the same semantics than methods.
*
* @author Tobias Schlatter
*/
abstract class PrepJSInterop extends plugins.PluginComponent
with PrepJSExports
with transform.Transform
with Compat210Component {
val jsAddons: JSGlobalAddons {
val global: PrepJSInterop.this.global.type
}
val scalaJSOpts: ScalaJSOptions
import global._
import jsAddons._
import definitions._
import rootMirror._
import jsDefinitions._
val phaseName = "jsinterop"
override def newPhase(p: nsc.Phase) = new JSInteropPhase(p)
class JSInteropPhase(prev: nsc.Phase) extends Phase(prev) {
override def name = phaseName
override def description = "Prepare ASTs for JavaScript interop"
override def run(): Unit = {
jsPrimitives.initPrepJSPrimitives()
super.run()
}
}
override protected def newTransformer(unit: CompilationUnit) =
new JSInteropTransformer(unit)
private object jsnme {
val hasNext = newTermName("hasNext")
val next = newTermName("next")
val nextName = newTermName("nextName")
val x = newTermName("x")
val Value = newTermName("Value")
val Val = newTermName("Val")
}
private object jstpnme {
val scala_ = newTypeName("scala") // not defined in 2.10's tpnme
}
class JSInteropTransformer(unit: CompilationUnit) extends Transformer {
// Force evaluation of JSDynamicLiteral: Strangely, we are unable to find
// nested objects in the JSCode phase (probably after flatten).
// Therefore we force the symbol of js.Dynamic.literal here in order to
// have access to it in JSCode.
JSDynamicLiteral
var inJSAnyMod = false
var inJSAnyCls = false
var inScalaCls = false
/** are we inside a subclass of scala.Enumeration */
var inScalaEnum = false
/** are we inside the implementation of scala.Enumeration? */
var inEnumImpl = false
def jsAnyClassOnly = !inJSAnyCls && allowJSAny
def allowImplDef = !inJSAnyCls && !inJSAnyMod
def allowJSAny = !inScalaCls
def inJSAny = inJSAnyMod || inJSAnyCls
/** DefDefs in class templates that export methods to JavaScript */
val exporters = mutable.Map.empty[Symbol, mutable.ListBuffer[Tree]]
override def transform(tree: Tree): Tree = postTransform { tree match {
// Catch special case of ClassDef in ModuleDef
case cldef: ClassDef if jsAnyClassOnly && isJSAny(cldef) =>
transformJSAny(cldef)
// Catch forbidden implDefs
case idef: ImplDef if !allowImplDef =>
reporter.error(idef.pos, "Traits, classes and objects extending " +
"js.Any may not have inner traits, classes or objects")
super.transform(tree)
// Handle js.Anys
case idef: ImplDef if isJSAny(idef) =>
transformJSAny(idef)
// Catch the definition of scala.Enumeration itself
case cldef: ClassDef if cldef.symbol == ScalaEnumClass =>
enterEnumImpl { super.transform(cldef) }
// Catch Scala Enumerations to transform calls to scala.Enumeration.Value
case cldef: ClassDef if isScalaEnum(cldef) =>
enterScalaCls {
enterScalaEnum {
super.transform(cldef)
}
}
case idef: ImplDef if isScalaEnum(idef) =>
enterScalaEnum { super.transform(idef) }
// Catch (Scala) ClassDefs to forbid js.Anys
case cldef: ClassDef =>
enterScalaCls { super.transform(cldef) }
// Catch ValorDefDef in js.Any
case vddef: ValOrDefDef if inJSAny =>
transformValOrDefDefInRawJSType(vddef)
// Catch ValDefs in enumerations with simple calls to Value
case ValDef(mods, name, tpt, ScalaEnumValue.NoName(optPar)) if inScalaEnum =>
val nrhs = ScalaEnumValName(tree.symbol.owner, tree.symbol, optPar)
treeCopy.ValDef(tree, mods, name, transform(tpt), nrhs)
// Catch Select on Enumeration.Value we couldn't transform but need to
// we ignore the implementation of scala.Enumeration itself
case ScalaEnumValue.NoName(_) if !inEnumImpl =>
reporter.warning(tree.pos,
"""Couldn't transform call to Enumeration.Value.
|The resulting program is unlikely to function properly as this
|operation requires reflection.""".stripMargin)
super.transform(tree)
case ScalaEnumValue.NullName() if !inEnumImpl =>
reporter.warning(tree.pos,
"""Passing null as name to Enumeration.Value
|requires reflection at runtime. The resulting
|program is unlikely to function properly.""".stripMargin)
super.transform(tree)
case ScalaEnumVal.NoName(_) if !inEnumImpl =>
reporter.warning(tree.pos,
"""Calls to the non-string constructors of Enumeration.Val
|require reflection at runtime. The resulting
|program is unlikely to function properly.""".stripMargin)
super.transform(tree)
case ScalaEnumVal.NullName() if !inEnumImpl =>
reporter.warning(tree.pos,
"""Passing null as name to a constructor of Enumeration.Val
|requires reflection at runtime. The resulting
|program is unlikely to function properly.""".stripMargin)
super.transform(tree)
// Catch calls to Predef.classOf[T]. These should NEVER reach this phase
// but unfortunately do. In normal cases, the typer phase replaces these
// calls by a literal constant of the given type. However, when we compile
// the scala library itself and Predef.scala is in the sources, this does
// not happen.
//
// The trees reach this phase under the form:
//
// scala.this.Predef.classOf[T]
//
// If we encounter such a tree, depending on the plugin options, we fail
// here or silently fix those calls.
case TypeApply(
classOfTree @ Select(Select(This(jstpnme.scala_), nme.Predef), nme.classOf),
List(tpeArg)) =>
if (scalaJSOpts.fixClassOf) {
// Replace call by literal constant containing type
if (typer.checkClassType(tpeArg)) {
typer.typed { Literal(Constant(tpeArg.tpe.dealias.widen)) }
} else {
reporter.error(tpeArg.pos, s"Type ${tpeArg} is not a class type")
EmptyTree
}
} else {
reporter.error(classOfTree.pos,
"""This classOf resulted in an unresolved classOf in the jscode
|phase. This is most likely a bug in the Scala compiler. ScalaJS
|is probably able to work around this bug. Enable the workaround
|by passing the fixClassOf option to the plugin.""".stripMargin)
EmptyTree
}
// Exporter generation
case ddef: DefDef =>
// Generate exporters for this ddef if required
exporters.getOrElseUpdate(ddef.symbol.owner,
mutable.ListBuffer.empty) ++= genExportMember(ddef)
super.transform(tree)
// Module export sanity check (export generated in JSCode phase)
case modDef: ModuleDef =>
val sym = modDef.symbol
def condErr(msg: String) = {
for (exp <- jsInterop.exportsOf(sym)) {
reporter.error(exp.pos, msg)
}
}
if (!hasLegalExportVisibility(sym))
condErr("You may only export public and protected objects")
else if (sym.isLocalToBlock)
condErr("You may not export a local object")
else if (!sym.owner.hasPackageFlag)
condErr("You may not export a nested object")
super.transform(modDef)
// Fix for issue with calls to js.Dynamic.x()
// Rewrite (obj: js.Dynamic).x(...) to obj.applyDynamic("x")(...)
case Select(Select(trg, jsnme.x), nme.apply) if isJSDynamic(trg) =>
val newTree = atPos(tree.pos) {
Apply(
Select(super.transform(trg), newTermName("applyDynamic")),
List(Literal(Constant("x")))
)
}
typer.typed(newTree, Mode.FUNmode, tree.tpe)
// Fix for issue with calls to js.Dynamic.x()
// Rewrite (obj: js.Dynamic).x to obj.selectDynamic("x")
case Select(trg, jsnme.x) if isJSDynamic(trg) =>
val newTree = atPos(tree.pos) {
Apply(
Select(super.transform(trg), newTermName("selectDynamic")),
List(Literal(Constant("x")))
)
}
typer.typed(newTree, Mode.FUNmode, tree.tpe)
case _ => super.transform(tree)
} }
private def postTransform(tree: Tree) = tree match {
case Template(parents, self, body) =>
val clsSym = tree.symbol.owner
val exports = exporters.get(clsSym).toIterable.flatten
// Add exports to the template
treeCopy.Template(tree, parents, self, body ++ exports)
case memDef: MemberDef =>
val sym = memDef.symbol
if (sym.isLocalToBlock && !sym.owner.isCaseApplyOrUnapply) {
// We exclude case class apply (and unapply) to work around SI-8826
for (exp <- jsInterop.exportsOf(sym)) {
val msg = {
val base = "You may not export a local definition"
if (sym.owner.isPrimaryConstructor)
base + ". To export a (case) class field, use the " +
"meta-annotation scala.annotation.meta.field like this: " +
"@(JSExport @field)."
else
base
}
reporter.error(exp.pos, msg)
}
}
memDef
case _ => tree
}
/**
* Performs checks and rewrites specific to classes / objects extending
* js.Any
*/
private def transformJSAny(implDef: ImplDef) = {
val sym = implDef.symbol
lazy val badParent = sym.info.parents.find(t => !(t <:< JSAnyClass.tpe))
val inScalaJSJSPackage =
sym.enclosingPackage == ScalaJSJSPackage ||
sym.enclosingPackage == ScalaJSJSPrimPackage
implDef match {
// Check that we do not have a case modifier
case _ if implDef.mods.hasFlag(Flag.CASE) =>
reporter.error(implDef.pos, "Classes and objects extending " +
"js.Any may not have a case modifier")
// Check that we do not extends a trait that does not extends js.Any
case _ if !inScalaJSJSPackage && !badParent.isEmpty &&
!isJSLambda(sym) =>
val badName = badParent.get.typeSymbol.fullName
reporter.error(implDef.pos, s"${sym.nameString} extends ${badName} " +
"which does not extend js.Any.")
// Check that we are not an anonymous class
case cldef: ClassDef
if cldef.symbol.isAnonymousClass && !isJSLambda(sym) =>
reporter.error(implDef.pos, "Anonymous classes may not " +
"extend js.Any")
// Check if we may have a js.Any here
case cldef: ClassDef if !allowJSAny && !jsAnyClassOnly &&
!isJSLambda(sym) =>
reporter.error(implDef.pos, "Classes extending js.Any may not be " +
"defined inside a class or trait")
case _: ModuleDef if !allowJSAny =>
reporter.error(implDef.pos, "Objects extending js.Any may not be " +
"defined inside a class or trait")
case _ if sym.isLocalToBlock && !isJSLambda(sym) =>
reporter.error(implDef.pos, "Local classes and objects may not " +
"extend js.Any")
// Check that this is not a class extending js.GlobalScope
case _: ClassDef if isJSGlobalScope(implDef) &&
implDef.symbol != JSGlobalScopeClass =>
reporter.error(implDef.pos, "Only objects may extend js.GlobalScope")
case _ =>
// We cannot use sym directly, since the symbol
// of a module is not its type's symbol but the value it declares
val tSym = sym.tpe.typeSymbol
tSym.setAnnotations(rawJSAnnot :: sym.annotations)
}
if (implDef.isInstanceOf[ModuleDef])
enterJSAnyMod { super.transform(implDef) }
else
enterJSAnyCls { super.transform(implDef) }
}
/** Verify a ValOrDefDef inside a js.Any */
private def transformValOrDefDefInRawJSType(tree: ValOrDefDef) = {
val sym = tree.symbol
val exports = jsInterop.exportsOf(sym)
if (exports.nonEmpty) {
val memType = if (sym.isConstructor) "constructor" else "method"
reporter.error(exports.head.pos,
s"You may not export a $memType of a subclass of js.Any")
}
if (isNonJSScalaSetter(sym)) {
// Forbid setters with non-unit return type
reporter.error(tree.pos, "Setters that do not return Unit are " +
"not allowed in types extending js.Any")
}
if (sym.hasAnnotation(NativeAttr)) {
// Native methods are not allowed
reporter.error(tree.pos, "Methods in a js.Any may not be @native")
}
for {
annot <- sym.getAnnotation(JSNameAnnotation)
if annot.stringArg(0).isEmpty
} {
reporter.error(annot.pos,
"The argument to JSName must be a literal string")
}
if (sym.isPrimaryConstructor || sym.isValueParameter ||
sym.isParamWithDefault || sym.isAccessor && !sym.isDeferred ||
sym.isParamAccessor || sym.isSynthetic ||
AllJSFunctionClasses.contains(sym.owner)) {
/* Ignore (i.e. allow) primary ctor, parameters, default parameter
* getters, accessors, param accessors, synthetic methods (to avoid
* double errors with case classes, e.g. generated copy method) and
* js.Functions and js.ThisFunctions (they need abstract methods for SAM
* treatment.
*/
} else if (jsPrimitives.isJavaScriptPrimitive(sym)) {
// Force rhs of a primitive to be `sys.error("stub")` except for the
// js.native primitive which displays an elaborate error message
if (sym != JSPackage_native) {
tree.rhs match {
case Apply(trg, Literal(Constant("stub")) :: Nil)
if trg.symbol == definitions.Sys_error =>
case _ =>
reporter.error(tree.pos,
"The body of a primitive must be `sys.error(\"stub\")`.")
}
}
} else if (sym.isConstructor) {
// Force secondary ctor to have only a call to the primary ctor inside
tree.rhs match {
case Block(List(Apply(trg, _)), Literal(Constant(())))
if trg.symbol.isPrimaryConstructor &&
trg.symbol.owner == sym.owner =>
// everything is fine here
case _ =>
reporter.error(tree.pos, "A secondary constructor of a class " +
"extending js.Any may only call the primary constructor")
}
} else {
// Check that the tree's body is either empty or calls js.native
tree.rhs match {
case sel: Select if sel.symbol == JSPackage_native =>
case _ =>
val pos = if (tree.rhs != EmptyTree) tree.rhs.pos else tree.pos
reporter.warning(pos, "Members of traits, classes and objects " +
"extending js.Any may only contain members that call js.native. " +
"This will be enforced in 1.0.")
}
if (sym.tpe.resultType.typeSymbol == NothingClass &&
tree.tpt.asInstanceOf[TypeTree].original == null) {
// Warn if resultType is Nothing and not ascribed
val name = sym.name.decoded.trim
reporter.warning(tree.pos, s"The type of $name got inferred " +
"as Nothing. To suppress this warning, explicitly ascribe " +
"the type.")
}
}
super.transform(tree)
}
private def enterJSAnyCls[T](body: =>T) = {
val old = inJSAnyCls
inJSAnyCls = true
val res = body
inJSAnyCls = old
res
}
private def enterJSAnyMod[T](body: =>T) = {
val old = inJSAnyMod
inJSAnyMod = true
val res = body
inJSAnyMod = old
res
}
private def enterScalaCls[T](body: =>T) = {
val old = inScalaCls
inScalaCls = true
val res = body
inScalaCls = old
res
}
private def enterScalaEnum[T](body: =>T) = {
val old = inScalaEnum
inScalaEnum = true
val res = body
inScalaEnum = old
res
}
private def enterEnumImpl[T](body: =>T) = {
val old = inEnumImpl
inEnumImpl = true
val res = body
inEnumImpl = old
res
}
}
def isJSAny(sym: Symbol): Boolean =
sym.tpe.typeSymbol isSubClass JSAnyClass
private def isJSAny(implDef: ImplDef): Boolean = isJSAny(implDef.symbol)
private def isJSGlobalScope(implDef: ImplDef) =
implDef.symbol.tpe.typeSymbol isSubClass JSGlobalScopeClass
private def isJSLambda(sym: Symbol) = sym.isAnonymousClass &&
AllJSFunctionClasses.exists(sym.tpe.typeSymbol isSubClass _)
private def isScalaEnum(implDef: ImplDef) =
implDef.symbol.tpe.typeSymbol isSubClass ScalaEnumClass
private def isJSDynamic(tree: Tree) = tree.tpe.typeSymbol == JSDynamicClass
/**
* is this symbol a setter that has a non-unit return type
*
* these setters don't make sense in JS (in JS, assignment returns
* the assigned value) and are therefore not allowed in facade types
*/
private def isNonJSScalaSetter(sym: Symbol) = nme.isSetterName(sym.name) && {
sym.tpe.paramss match {
case List(List(arg)) =>
!isScalaRepeatedParamType(arg.tpe) &&
sym.tpe.resultType.typeSymbol != UnitClass
case _ => false
}
}
trait ScalaEnumFctExtractors {
protected val methSym: Symbol
protected def resolve(ptpes: Symbol*) = {
val res = methSym suchThat {
_.tpe.params.map(_.tpe.typeSymbol) == ptpes.toList
}
assert(res != NoSymbol)
res
}
protected val noArg = resolve()
protected val nameArg = resolve(StringClass)
protected val intArg = resolve(IntClass)
protected val fullMeth = resolve(IntClass, StringClass)
/**
* Extractor object for calls to the targeted symbol that do not have an
* explicit name in the parameters
*
* Extracts:
* - `sel: Select` where sel.symbol is targeted symbol (no arg)
* - Apply(meth, List(param)) where meth.symbol is targeted symbol (i: Int)
*/
object NoName {
def unapply(t: Tree) = t match {
case sel: Select if sel.symbol == noArg =>
Some(None)
case Apply(meth, List(param)) if meth.symbol == intArg =>
Some(Some(param))
case _ =>
None
}
}
object NullName {
def unapply(tree: Tree) = tree match {
case Apply(meth, List(Literal(Constant(null)))) =>
meth.symbol == nameArg
case Apply(meth, List(_, Literal(Constant(null)))) =>
meth.symbol == fullMeth
case _ => false
}
}
}
private object ScalaEnumValue extends {
protected val methSym = getMemberMethod(ScalaEnumClass, jsnme.Value)
} with ScalaEnumFctExtractors
private object ScalaEnumVal extends {
protected val methSym = {
val valSym = getMemberClass(ScalaEnumClass, jsnme.Val)
valSym.tpe.member(nme.CONSTRUCTOR)
}
} with ScalaEnumFctExtractors
/**
* Construct a call to Enumeration.Value
* @param thisSym ClassSymbol of enclosing class
* @param nameOrig Symbol of ValDef where this call will be placed
* (determines the string passed to Value)
* @param intParam Optional tree with Int passed to Value
* @return Typed tree with appropriate call to Value
*/
private def ScalaEnumValName(
thisSym: Symbol,
nameOrig: Symbol,
intParam: Option[Tree]) = {
val defaultName = nameOrig.asTerm.getterName.encoded
// Construct the following tree
//
// if (nextName != null && nextName.hasNext)
// nextName.next()
// else
// <defaultName>
//
val nextNameTree = Select(This(thisSym), jsnme.nextName)
val nullCompTree =
Apply(Select(nextNameTree, nme.NE), Literal(Constant(null)) :: Nil)
val hasNextTree = Select(nextNameTree, jsnme.hasNext)
val condTree = Apply(Select(nullCompTree, nme.ZAND), hasNextTree :: Nil)
val nameTree = If(condTree,
Apply(Select(nextNameTree, jsnme.next), Nil),
Literal(Constant(defaultName)))
val params = intParam.toList :+ nameTree
typer.typed {
Apply(Select(This(thisSym), jsnme.Value), params)
}
}
private def rawJSAnnot =
Annotation(RawJSTypeAnnot.tpe, List.empty, ListMap.empty)
private lazy val ScalaEnumClass = getRequiredClass("scala.Enumeration")
/** checks if the primary constructor of the ClassDef `cldef` does not
* take any arguments
*/
private def primCtorNoArg(cldef: ClassDef) =
getPrimCtor(cldef.symbol.tpe).map(_.paramss == List(List())).getOrElse(true)
/** return the MethodSymbol of the primary constructor of the given type
* if it exists
*/
private def getPrimCtor(tpe: Type) =
tpe.declaration(nme.CONSTRUCTOR).alternatives.collectFirst {
case ctor: MethodSymbol if ctor.isPrimaryConstructor => ctor
}
}