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