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

org.scalajs.nscplugin.ExplicitInnerJS.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.reflect.internal.Flags

import scala.tools.nsc
import nsc._
import nsc.transform.{InfoTransform, TypingTransformers}

import scala.collection.immutable.ListMap
import scala.collection.mutable

/** Makes the references to inner JS class values explicit.
 *
 *  Roughly, for every inner JS class of the form:
 *  {{{
 *  class Outer {
 *    class Inner extends ParentJSClass
 *  }
 *  }}}
 *  this phase creates a field `Inner\$jsclass` in `Outer` to hold the JS class
 *  value for `Inner`. The rhs of that field is a call to a magic method, used
 *  to retain information that the back-end will need.
 *  {{{
 *  class Outer {
 *     val Inner\$jsclass: AnyRef =
 *      createJSClass(classOf[Inner], js.constructorOf[ParentJSClass])
 *
 *    class Inner extends ParentJSClass
 *  }
 *  }}}
 *
 *  These fields will be read by code generated in `ExplicitLocalJS`.
 *
 *  A `\$jsclass` field is also generated for classes declared inside *static
 *  JS objects*. Indeed, even though those classes have a unique, globally
 *  accessible class value, that class value needs to be *exposed* as a field
 *  of the enclosing object. In those cases, the rhs of the field is a direct
 *  call to `runtime.constructorOf[classOf[Inner]]`.
 *
 *  Finally, for *modules* declared inside static JS objects, we generate an
 *  explicit exposed getter as well. For non-static objects, scalac already
 *  generates a getter with the `@ExposedJSMember` annotation, so we do not
 *  need to do anything. But for static objects, it doesn't, so we have to do
 *  it ourselves here.
 *
 *  To illustrate the two above paragraphs, for the following input:
 *  {{{
 *  object Outer extends js.Object {
 *    class InnerClass extends ParentJSClass
 *    object InnerObject extends SomeOtherJSClass
 *  }
 *  }}}
 *  this phase will generate
 *  {{{
 *  object Outer extends js.Object {
 *    ...
 *
 *    @ExposedJSMember @JSName("InnerClass")
 *    val InnerClass\$jsclass: AnyRef = runtime.constructorOf(classOf[InnerClass])
 *
 *    @ExposedJSMember @JSName("InnerObject")
 *    def InnerObject\$jsobject: AnyRef = InnerObject
 *  }
 *  }}}
 *
 *  Note that this field must also be added to outer classes and traits coming
 *  from separate compilation, therefore this phase is an `InfoTransform`.
 *  Since the `transformInfo` also applies to classes defined in the current
 *  compilation unit, the tree traversal must not create the field symbols a
 *  second time when synthesizing the `ValDef`. Instead, it must reuse the same
 *  symbols that `transformInfo` will create.
 *
 *  It seems the easiest way to do that is to run the entire `transform` "in
 *  the future", with `exitingPhase(ExplicitInnerJS)`. This design is similar
 *  to how `explicitouter` works.
 */
abstract class ExplicitInnerJS[G <: Global with Singleton](val global: G)
    extends plugins.PluginComponent with InfoTransform with TypingTransformers
    with CompatComponent {

  val jsAddons: JSGlobalAddons {
    val global: ExplicitInnerJS.this.global.type
  }

  import global._
  import jsAddons._
  import jsInterop.jsclassAccessorFor
  import definitions._
  import rootMirror._
  import jsDefinitions._

  /* The missing 'e' is intentional so that the name of the phase is not longer
   * than the longest standard phase (packageobjects/superaccessors). This
   * avoids destroying the nice formatting of `-Xshow-phases`.
   */
  val phaseName: String = "xplicitinnerjs"

  override def description: String =
    "make references to inner JS classes explicit"

  /** This class does not change linearization. */
  override protected def changesBaseClasses: Boolean = false

  protected def newTransformer(unit: CompilationUnit): Transformer =
    new ExplicitInnerJSTransformer(unit)

  /** Is the given symbol an owner for which this transformation applies?
   *
   *  This applies if either or both of the following are true:
   *  - It is not a static owner, or
   *  - It is a non-native JS object.
   *
   *  The latter is necessary for #4086.
   */
  private def isApplicableOwner(sym: Symbol): Boolean = {
    !sym.isStaticOwner || (
        sym.isModuleClass &&
        sym.hasAnnotation(JSTypeAnnot) &&
        !sym.hasAnnotation(JSNativeAnnotation)
    )
  }

  /** Is the given symbol a JS class (that is not a trait nor an object)? */
  private def isJSClass(sym: Symbol): Boolean = {
    sym.isClass &&
    !sym.hasFlag(Flags.TRAIT | Flags.MODULE) &&
    sym.hasAnnotation(JSTypeAnnot)
  }

  /** Is the given symbol a Module that should be exposed? */
  private def isExposedModule(sym: Symbol): Boolean =
    sym.isModule && sym.hasAnnotation(ExposedJSMemberAnnot)

  private def jsobjectGetterNameFor(moduleSym: Symbol): TermName =
    moduleSym.name.append("$jsobject").toTermName

  /** Transforms the info of types to add the `Inner\$jsclass` fields.
   *
   *  This method was inspired by `ExplicitOuter.transformInfo`.
   */
  def transformInfo(sym: Symbol, tp: Type): Type = tp match {
    case ClassInfoType(parents, decls, clazz) if !clazz.isJava && isApplicableOwner(clazz) =>
      val innerJSClasses = decls.filter(isJSClass)

      val innerObjectsForAdHocExposed =
        if (!clazz.isStaticOwner) Nil // those receive a module accessor from scalac
        else decls.filter(isExposedModule).toList

      if (innerJSClasses.isEmpty && innerObjectsForAdHocExposed.isEmpty) {
        tp
      } else {
        def addAnnots(sym: Symbol, symForName: Symbol): Unit = {
          symForName.getAnnotation(JSNameAnnotation).fold {
            sym.addAnnotation(JSNameAnnotation,
                Literal(Constant(jsInterop.defaultJSNameOf(symForName))))
          } { annot =>
            sym.addAnnotation(annot)
          }
          sym.addAnnotation(ExposedJSMemberAnnot)
        }

        val clazzIsJSClass = clazz.hasAnnotation(JSTypeAnnot)

        val decls1 = decls.cloneScope

        for (innerJSClass <- innerJSClasses) {
          def addAnnotsIfInJSClass(sym: Symbol): Unit = {
            if (clazzIsJSClass)
              addAnnots(sym, innerJSClass)
          }

          val accessorName: TermName =
            innerJSClass.name.append("$jsclass").toTermName
          val accessorFlags =
            Flags.SYNTHETIC | Flags.ARTIFACT | Flags.STABLE | Flags.ACCESSOR
          val accessor =
            clazz.newMethod(accessorName, innerJSClass.pos, accessorFlags)
          accessor.setInfo(NullaryMethodType(AnyRefTpe))
          addAnnotsIfInJSClass(accessor)
          decls1.enter(accessor)

          if (!clazz.isTrait) {
            val fieldName = accessorName.append(nme.LOCAL_SUFFIX_STRING)
            val fieldFlags =
              Flags.SYNTHETIC | Flags.ARTIFACT | Flags.PrivateLocal
            val field = clazz
              .newValue(fieldName, innerJSClass.pos, fieldFlags)
              .setInfo(AnyRefTpe)
            addAnnotsIfInJSClass(field)
            decls1.enter(field)
          }
        }

        // #4086 Create exposed getters for exposed objects in static JS objects
        for (innerObject <- innerObjectsForAdHocExposed) {
          assert(clazzIsJSClass && clazz.isModuleClass && clazz.isStatic,
              s"trying to ad-hoc expose objects in non-JS static object")

          val getterName = jsobjectGetterNameFor(innerObject)
          val getterFlags = Flags.SYNTHETIC | Flags.ARTIFACT | Flags.STABLE
          val getter = clazz.newMethod(getterName, innerObject.pos, getterFlags)
          getter.setInfo(NullaryMethodType(AnyRefTpe))
          addAnnots(getter, innerObject)
          decls1.enter(getter)
        }

        ClassInfoType(parents, decls1, clazz)
      }

    case PolyType(tparams, restp) =>
      val restp1 = transformInfo(sym, restp)
      if (restp eq restp1) tp else PolyType(tparams, restp1)

    case _ =>
      tp
  }

  class ExplicitInnerJSTransformer(unit: CompilationUnit)
      extends TypingTransformer(unit) {

    /** Execute the whole transformation in the future, exiting this phase. */
    override def transformUnit(unit: CompilationUnit): Unit = {
      global.exitingPhase(currentRun.phaseNamed(phaseName)) {
        super.transformUnit(unit)
      }
    }

    /** The main transformation method. */
    override def transform(tree: Tree): Tree = {
      tree match {
        // Add the ValDefs for inner JS class values
        case Template(parents, self, decls) if isApplicableOwner(currentOwner) =>
          val newDecls = mutable.ListBuffer.empty[Tree]
          atOwner(tree, currentOwner) {
            for (decl <- decls) {
              val declSym = decl.symbol
              if (declSym eq null) {
                // not a member def, do nothing
              } else if (isJSClass(declSym)) {
                val jsclassAccessor = jsclassAccessorFor(declSym)

                val rhs = if (currentOwner.hasAnnotation(JSNativeAnnotation)) {
                  gen.mkAttributedRef(JSPackage_native)
                } else {
                  val clazzValue = gen.mkClassOf(declSym.tpe_*)
                  if (currentOwner.isStaticOwner) {
                    // #4086
                    gen.mkMethodCall(Runtime_constructorOf, List(clazzValue))
                  } else {
                    val parentTpe =
                      extractSuperTpeFromImpl(decl.asInstanceOf[ClassDef].impl)
                    val superClassCtor = gen.mkNullaryCall(
                        JSPackage_constructorOf, List(parentTpe))
                    gen.mkMethodCall(Runtime_createInnerJSClass,
                        List(clazzValue, superClassCtor))
                  }
                }

                if (!currentOwner.isTrait) {
                  val jsclassField = jsclassAccessor.accessed
                  assert(jsclassField != NoSymbol, jsclassAccessor.fullName)
                  newDecls += localTyper.typedValDef(ValDef(jsclassField, rhs))
                  newDecls += localTyper.typed {
                    val rhs = Select(This(currentClass), jsclassField)
                    DefDef(jsclassAccessor, rhs)
                  }
                } else {
                  newDecls += localTyper.typedValDef(ValDef(jsclassAccessor, rhs))
                }
              } else if (currentOwner.isStaticOwner) {
                // #4086
                if (isExposedModule(declSym)) {
                  val getter =
                    currentOwner.info.member(jsobjectGetterNameFor(declSym))
                  newDecls += localTyper.typedDefDef {
                    DefDef(getter, gen.mkAttributedRef(declSym))
                  }
                }
              }

              newDecls += decl
            }
          }

          val newTemplate =
            treeCopy.Template(tree, parents, self, newDecls.result())
          super.transform(newTemplate)

        case _ =>
          // Taken from ExplicitOuter
          val x = super.transform(tree)
          if (x.tpe eq null) x
          else x.setType(transformInfo(currentOwner, x.tpe))
      }
    }

  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy