summaryrefslogblamecommitdiff
path: root/compiler/src/main/scala/scala/scalajs/compiler/JSGlobalAddons.scala
blob: 362105078fb4357683c702bdb7bd15b47c931e17 (plain) (tree)



















































































































































































































































                                                                                                           
/* Scala.js compiler
 * Copyright 2013 LAMP/EPFL
 * @author  Sébastien Doeraene
 */

package scala.scalajs.compiler

import scala.tools.nsc._

import scala.collection.mutable

/** Additions to Global meaningful for the JavaScript backend
 *
 *  @author Sébastien Doeraene
 */
trait JSGlobalAddons extends JSDefinitions
                        with Compat210Component {
  val global: Global

  import global._
  import jsDefinitions._
  import definitions._

  /** JavaScript primitives, used in jscode */
  object jsPrimitives extends JSPrimitives {
    val global: JSGlobalAddons.this.global.type = JSGlobalAddons.this.global
    val jsAddons: ThisJSGlobalAddons =
      JSGlobalAddons.this.asInstanceOf[ThisJSGlobalAddons]
  }

  /** global javascript interop related helpers */
  object jsInterop {
    import scala.reflect.NameTransformer
    import scala.reflect.internal.Flags

    private val exportPrefix = "$js$exported$"
    private val methodExportPrefix = exportPrefix + "meth$"
    private val propExportPrefix = exportPrefix + "prop$"

    case class ExportInfo(jsName: String, pos: Position, isNamed: Boolean)

    /** 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.
     */
    def exportsOf(sym: Symbol): List[ExportInfo] = {
      val exports = directExportsOf(sym) ++ inheritedExportsOf(sym)

      // Calculate the distinct exports for this symbol (eliminate double
      // occurrences of (name, isNamed) pairs).
      val buf = new mutable.ListBuffer[ExportInfo]
      val seen = new mutable.HashSet[(String, Boolean)]
      for (exp <- exports) {
        if (!seen.contains((exp.jsName, exp.isNamed))) {
          buf += exp
          seen += ((exp.jsName, exp.isNamed))
        }
      }

      buf.toList
    }

    private def directExportsOf(sym: Symbol): List[ExportInfo] = {
      val trgSym = {
        // For accessors, look on the val/var def
        if (sym.isAccessor) sym.accessed
        // For primary class constructors, look on the class itself
        else if (sym.isPrimaryConstructor && !sym.owner.isModuleClass) sym.owner
        else sym
      }

      // Annotations that are directly on the member
      val directAnnots = for {
        annot <- trgSym.annotations
        if annot.symbol == JSExportAnnotation ||
           annot.symbol == JSExportNamedAnnotation
      } yield annot

      // Annotations for this member on the whole unit
      val unitAnnots = {
        if (sym.isMethod && sym.isPublic &&
            !sym.isConstructor && !sym.isSynthetic)
          sym.owner.annotations.filter(_.symbol == JSExportAllAnnotation)
        else
          Nil
      }

      for {
        annot <- directAnnots ++ unitAnnots
      } yield {
        // Is this a named export or a normal one?
        val named = annot.symbol == JSExportNamedAnnotation

        def explicitName = annot.stringArg(0).getOrElse {
          reporter.error(annot.pos,
            s"The argument to ${annot.symbol.name} must be a literal string")
          "dummy"
        }

        val name =
          if (annot.args.nonEmpty) explicitName
          else if (sym.isConstructor) decodedFullName(sym.owner)
          else if (sym.isModuleClass) decodedFullName(sym)
          else sym.unexpandedName.decoded.stripSuffix("_=")

        // Enforce that methods ending with _= are exported as setters
        if (sym.isMethod && !sym.isConstructor &&
          sym.name.decoded.endsWith("_=") && !isJSSetter(sym)) {
          reporter.error(annot.pos, "A method ending in _= will be exported " +
              s"as setter. But ${sym.name.decoded} does not have the right " +
              "signature to do so (single argument, unit return type).")
        }

        // Enforce no __ in name
        if (name.contains("__")) {
          // Get position for error message
          val pos = if (annot.stringArg(0).isDefined)
            annot.args.head.pos
          else trgSym.pos

          reporter.error(pos,
              "An exported name may not contain a double underscore (`__`)")
        }

        // Make sure we do not override the default export of toString
        if (!sym.isConstructor && name == "toString" && !named &&
            sym.name != nme.toString_ && sym.tpe.params.isEmpty &&
            !isJSGetter(sym)) {
          reporter.error(annot.pos, "You may not export a zero-argument " +
              "method named other than 'toString' under the name 'toString'")
        }

        if (named && isJSProperty(sym)) {
          reporter.error(annot.pos,
              "You may not export a getter or a setter as a named export")
        }

        ExportInfo(name, annot.pos, named)
      }
    }

    private def inheritedExportsOf(sym: Symbol): List[ExportInfo] = {
      // The symbol from which we (potentially) inherit exports. It also
      // gives the exports their name
      val trgSym = {
        if (sym.isModuleClass)
          sym
        else if (sym.isConstructor && sym.isPublic &&
            sym.owner.isConcreteClass && !sym.owner.isModuleClass)
          sym.owner
        else NoSymbol
      }

      if (trgSym == NoSymbol) {
        Nil
      } else {
        val trgAnnot =
          if (sym.isModuleClass) JSExportDescendentObjectsAnnotation
          else JSExportDescendentClassesAnnotation

        val forcingSym =
          trgSym.ancestors.find(_.annotations.exists(_.symbol == trgAnnot))

        val name = decodedFullName(trgSym)

        forcingSym.map { fs =>
          // Enfore no __ in name
          if (name.contains("__")) {
            // Get all annotation positions for error message
            reporter.error(sym.pos,
                s"""${trgSym.name} may not have a double underscore (`__`) in its fully qualified
                   |name, since it is forced to be exported by a @${trgAnnot.name} on ${fs}""".stripMargin)
          }

          ExportInfo(name, sym.pos, false)
        }.toList
      }
    }

    /** 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
    }

    /** creates a name for an export specification */
    def scalaExportName(jsName: String, isProp: Boolean): TermName = {
      val pref = if (isProp) propExportPrefix else methodExportPrefix
      val encname = NameTransformer.encode(jsName)
      newTermName(pref + encname)
    }

    /** checks if the given symbol is a JSExport */
    def isExport(sym: Symbol): Boolean =
      sym.unexpandedName.startsWith(exportPrefix) &&
      !sym.hasFlag(Flags.DEFAULTPARAM)

    /** retrieves the originally assigned jsName of this export and whether it
     *  is a property
     */
    def jsExportInfo(name: Name): (String, Boolean) = {
      def dropPrefix(prefix: String) ={
        if (name.startsWith(prefix)) {
          // We can't decode right away due to $ separators
          val enc = name.encoded.substring(prefix.length)
          Some(NameTransformer.decode(enc))
        } else None
      }

      dropPrefix(methodExportPrefix).map((_,false)) orElse
      dropPrefix(propExportPrefix).map((_,true)) getOrElse
      sys.error("non-exported name passed to jsInfoSpec")
    }

    def isJSProperty(sym: Symbol): Boolean = isJSGetter(sym) || isJSSetter(sym)

    /** has this symbol to be translated into a JS getter (both directions)? */
    def isJSGetter(sym: Symbol): Boolean = {
      sym.tpe.params.isEmpty && enteringPhase(currentRun.uncurryPhase) {
        sym.tpe.isInstanceOf[NullaryMethodType]
      }
    }

    /** has this symbol to be translated into a JS setter (both directions)? */
    def isJSSetter(sym: Symbol) = {
      sym.unexpandedName.decoded.endsWith("_=") &&
      sym.tpe.resultType.typeSymbol == UnitClass &&
      enteringPhase(currentRun.uncurryPhase) {
        sym.tpe.paramss match {
          case List(List(arg)) => !isScalaRepeatedParamType(arg.tpe)
          case _ => false
        }
      }
    }

    /** has this symbol to be translated into a JS bracket access (JS to Scala) */
    def isJSBracketAccess(sym: Symbol) =
      sym.hasAnnotation(JSBracketAccessAnnotation)

  }

}