summaryrefslogtreecommitdiff
path: root/examples/scala-js/compiler/src/main/scala/scala/scalajs/compiler/PrepJSInterop.scala
diff options
context:
space:
mode:
Diffstat (limited to 'examples/scala-js/compiler/src/main/scala/scala/scalajs/compiler/PrepJSInterop.scala')
-rw-r--r--examples/scala-js/compiler/src/main/scala/scala/scalajs/compiler/PrepJSInterop.scala621
1 files changed, 621 insertions, 0 deletions
diff --git a/examples/scala-js/compiler/src/main/scala/scala/scalajs/compiler/PrepJSInterop.scala b/examples/scala-js/compiler/src/main/scala/scala/scalajs/compiler/PrepJSInterop.scala
new file mode 100644
index 0000000..437576a
--- /dev/null
+++ b/examples/scala-js/compiler/src/main/scala/scala/scalajs/compiler/PrepJSInterop.scala
@@ -0,0 +1,621 @@
+/* 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
+ }
+
+}