All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.scalajs.nscplugin.PrepJSExports.scala Maven / Gradle / Ivy

There is a newer version: 1.16.0
Show newest version
/*
 * 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 exports for the given Symbol.
   *
   *  * Registers top-level and static exports.
   *  * Returns (non-static) exporters for this symbol.
   */
  def genExport(sym: Symbol): List[Tree] = {
    // Scala classes are never exported: Their constructors are.
    val isScalaClass = sym.isClass && !sym.isTrait && !sym.isModuleClass && !isJSAny(sym)

    /* Filter case class apply (and unapply) to work around
     * https://github.com/scala/bug/issues/8826
     */
    val isCaseApplyOrUnapplyParam = sym.isLocalToBlock && sym.owner.isCaseApplyOrUnapply

    /* Filter constructors of module classes: The module classes themselves will
     * be exported.
     */
    val isModuleClassCtor = sym.isConstructor && sym.owner.isModuleClass

    val exports =
      if (isScalaClass || isCaseApplyOrUnapplyParam || isModuleClassCtor) Nil
      else exportsOf(sym)

    assert(exports.isEmpty || !sym.isBridge,
        s"found exports for bridge symbol $sym. exports: $exports")

    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.
     *
     * For accessors, we need to apply some special logic to static and
     * top-level exports: They actually apply to the *fields*, not to the
     * accessors.
     */
    if (sym.isAccessor && sym.accessed != NoSymbol) {
      /* Only forward registration from the getter (not the setter) to avoid
       * duplicate registration.
       */
      if (sym.isGetter)
        registerStaticAndTopLevelExports(sym.accessed, topLevelAndStaticExports)
    } else {
      registerStaticAndTopLevelExports(sym, topLevelAndStaticExports)
    }

    // For normal exports, generate exporter methods.
    normalExports.flatMap(exp => genExportDefs(sym, exp.jsName, exp.pos))
  }

  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)
  }

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

    val symOwner =
      if (sym.isConstructor) sym.owner.owner
      else sym.owner

    // Annotations that are directly on the member
    val directAnnots = trgSym.annotations.filter(
        annot => isDirectMemberAnnot(annot.symbol))

    /* Annotations for this member on the whole unit
     *
     * Note that for top-level classes / modules this is always empty, because
     * packages cannot have annotations.
     */
    val unitAnnots = {
      val useExportAll = {
        sym.isPublic &&
        !sym.isSynthetic &&
        !sym.isConstructor &&
        !sym.isTrait &&
        (!sym.isClass || sym.isModuleClass)
      }

      if (useExportAll)
        symOwner.annotations.filter(_.symbol == JSExportAllAnnotation)
      else
        Nil
    }

    val allAnnots = {
      val allAnnots0 = directAnnots ++ unitAnnots

      if (allAnnots0.nonEmpty) {
        if (checkExportTarget(sym, allAnnots0.head.pos)) allAnnots0
        else Nil // prevent code generation from running to avoid crashes.
      } else {
        Nil
      }
    }

    val allExportInfos = for {
      annot <- allAnnots
    } 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 {
          val nameBase =
            (if (sym.isConstructor) sym.owner else sym).unexpandedName

          if (nme.isSetterName(nameBase) && !jsInterop.isJSSetter(sym)) {
            reporter.error(annot.pos, "You must set an explicit name when " +
                "exporting a non-setter with a name ending in _=")
          }

          nameBase.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 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 (`__`)")
      }

      // Destination-specific restrictions
      destination match {
        case ExportDestination.Normal =>
          if (symOwner.hasPackageFlag) {
            // Disallow @JSExport on top-level definitions.
            reporter.error(annot.pos,
                "@JSExport is forbidden on top-level objects and classes. " +
                "Use @JSExportTopLevel instead.")
          }

          // Make sure we do not override the default export of toString
          def isIllegalToString = {
            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'")
          }

          /* Illegal function application exports, i.e., method named 'apply'
           * without an explicit export name.
           */
          if (!hasExplicitName && sym.name == nme.apply) {
            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 =>
          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 definitions.
          if (!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 (sym.isLazy) {
            reporter.error(annot.pos,
                "You may not export a lazy val as static")
          }

          // Illegal function application export
          if (!hasExplicitName && sym.name == nme.apply) {
            reporter.error(annot.pos,
                "A member cannot be exported to function application as " +
                "static. Use @JSExportStatic(\"apply\") to export it under " +
                "the name 'apply'.")
          }

          if (sym.isClass || sym.isConstructor) {
            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"))

    /* Check 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).
     */
    if (sym.isGetter) {
      for {
        firstStatic <- allExportInfos.find(_.destination == ExportDestination.Static).toList
        duplicate <- allExportInfos
        if duplicate ne firstStatic
      } {
        duplicate.destination match {
          case ExportDestination.Normal => // OK

          case ExportDestination.Static =>
            reporter.error(duplicate.pos,
                "Fields (val or var) cannot be exported as static more " +
                "than once")

          case _: ExportDestination.TopLevel =>
            reporter.error(duplicate.pos,
                "Fields (val or var) cannot be exported both as static " +
                "and at the top-level")
        }
      }
    }

    allExportInfos.distinct
  }

  /** Checks whether the given target is suitable for export and exporting
   *  should be performed.
   *
   *  Reports any errors for unsuitable targets.
   *  @returns a boolean indicating whether exporting should be performed. Note:
   *      a result of true is not a guarantee that no error was emitted. But it is
   *      a guarantee that the target is not "too broken" to run the rest of
   *      the generation. This approximation is done to avoid having to complicate
   *      shared code verifying conditions.
   */
  private def checkExportTarget(sym: Symbol, errPos: Position): Boolean = {
    def err(msg: String) = {
      reporter.error(errPos, msg)
      false
    }

    def hasLegalExportVisibility(sym: Symbol) =
      sym.isPublic || sym.isProtected && !sym.isProtectedLocal

    lazy val params = sym.paramss.flatten

    def hasIllegalDefaultParam = {
      val isDefParam = (_: Symbol).hasFlag(Flags.DEFAULTPARAM)
      params.reverse.dropWhile(isDefParam).exists(isDefParam)
    }

    def hasAnyNonPrivateCtor: Boolean =
      sym.info.member(nme.CONSTRUCTOR).filter(!isPrivateMaybeWithin(_)).exists

    if (sym.isTrait) {
      err("You may not export a trait")
    } else if (sym.hasAnnotation(JSNativeAnnotation)) {
      err("You may not export a native JS definition")
    } else if (!hasLegalExportVisibility(sym)) {
      err("You may only export public and protected definitions")
    } else if (sym.isConstructor && !hasLegalExportVisibility(sym.owner)) {
      err("You may only export constructors of public and protected classes")
    } else if (sym.isMacro) {
      err("You may not export a macro")
    } else if (isJSAny(sym.owner)) {
      err("You may not export a member of a subclass of js.Any")
    } else if (scalaPrimitives.isPrimitive(sym)) {
      err("You may not export a primitive")
    } else if (sym.isLocalToBlock) {
      err("You may not export a local definition")
    } else if (sym.isConstructor && sym.owner.isLocalToBlock) {
      err("You may not export constructors of local classes")
    } else if (params.nonEmpty && params.init.exists(isRepeated _)) {
      err("In an exported method or constructor, a *-parameter must come last " +
          "(through all parameter lists)")
    } else if (hasIllegalDefaultParam) {
      err("In an exported method or constructor, all parameters with " +
          "defaults must be at the end")
    } else if (sym.isConstructor && sym.owner.isAbstractClass && !isJSAny(sym)) {
      err("You may not export an abstract class")
    } else if (sym.isClass && !sym.isModuleClass && isJSAny(sym) && !hasAnyNonPrivateCtor) {
      /* This test is only relevant for JS classes: We'll complain on the
       * individual exported constructors in case of a Scala class.
       */
      err("You may not export a class that has only private constructors")
    } else {
      if (jsInterop.isJSSetter(sym))
        checkSetterSignature(sym, errPos, exported = true)

      true // ok even if a setter has the wrong signature.
    }
  }

  /** generate an exporter for a target including default parameter methods */
  private def genExportDefs(sym: Symbol, jsName: String, pos: Position) = {
    val siblingSym =
      if (sym.isConstructor) sym.owner
      else sym

    val clsSym = siblingSym.owner

    val isProperty = sym.isModuleClass || isJSAny(sym) || jsInterop.isJSProperty(sym)
    val scalaName = jsInterop.scalaExportName(jsName, isProperty)

    val copiedFlags = siblingSym.flags & (Flags.PROTECTED | Flags.FINAL)

    // Create symbol for new method
    val expSym = clsSym.newMethod(scalaName, pos, Flags.SYNTHETIC | copiedFlags)
    expSym.privateWithin = siblingSym.privateWithin

    val expSymTpe = {
      /* 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.
       */
      if (sym.isClass) NullaryMethodType(AnyClass.tpe)
      else retToAny(sym.tpe.cloneInfo(expSym))
    }

    expSym.setInfoAndEnter(expSymTpe)

    // Construct exporter DefDef tree
    val exporter = genProxyDefDef(sym, expSym, pos)

    // Construct exporters for default getters
    val defaultGetters = for {
      (param, i) <- expSym.paramss.flatten.zipWithIndex
      if param.hasFlag(Flags.DEFAULTPARAM)
    } yield genExportDefaultGetter(clsSym, sym, 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(trgGetter, expGetter, pos)

    } else EmptyTree
  }

  /** generate a DefDef tree (from [[proxySym]]) that calls [[trgSym]] */
  private def genProxyDefDef(trgSym: Symbol, proxySym: Symbol, pos: Position) = atPos(pos) {
    val tpeParams = proxySym.typeParams.map(gen.mkAttributedIdent(_))

    // Construct proxied function call
    val nonPolyFun = {
      if (trgSym.isConstructor) {
        val clsTpe = trgSym.owner.tpe
        val tpe = gen.mkTypeApply(TypeTree(clsTpe), tpeParams)
        Select(New(tpe), trgSym)
      } else if (trgSym.isModuleClass) {
        assert(proxySym.paramss.isEmpty,
            s"got a module export with non-empty paramss. target: $trgSym, proxy: $proxySym at $pos")
        gen.mkAttributedRef(trgSym.sourceModule)
      } else if (trgSym.isClass) {
        assert(isJSAny(trgSym), s"got a class export for a non-JS class ($trgSym) at $pos")
        assert(proxySym.paramss.isEmpty,
            s"got a class export with non-empty paramss. target: $trgSym, proxy: $proxySym at $pos")
        val tpe = gen.mkTypeApply(TypeTree(trgSym.tpe), tpeParams)
        gen.mkTypeApply(gen.mkAttributedRef(JSPackage_constructorOf), List(tpe))
      } else {
        val fun = gen.mkAttributedRef(trgSym)
        gen.mkTypeApply(fun, tpeParams)
      }
    }

    val rhs = proxySym.paramss.foldLeft(nonPolyFun) { (fun, params) =>
      val args = params.map { param =>
        val ident = gen.mkAttributedIdent(param)

        if (isRepeated(param)) Typed(ident, Ident(tpnme.WILDCARD_STAR))
        else ident
      }

      Apply(fun, args)
    }

    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 a symbol is an annotation that goes directly on a member */
  private lazy val isDirectMemberAnnot = Set[Symbol](
      JSExportAnnotation,
      JSExportTopLevelAnnotation,
      JSExportStaticAnnotation
  )

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy