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

org.scalajs.linker.analyzer.Analyzer.scala Maven / Gradle / Ivy

There is a newer version: 1.17.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.linker.analyzer

import scala.annotation.tailrec

import scala.collection.mutable
import scala.concurrent._

import scala.util.{Try, Success, Failure}

import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic._

import org.scalajs.ir
import org.scalajs.ir.ClassKind
import org.scalajs.ir.Names._
import org.scalajs.ir.Trees.{MemberNamespace, JSNativeLoadSpec}
import org.scalajs.ir.Types.ClassRef

import org.scalajs.linker._
import org.scalajs.linker.frontend.IRLoader
import org.scalajs.linker.interface._
import org.scalajs.linker.interface.unstable.ModuleInitializerImpl
import org.scalajs.linker.standard._
import org.scalajs.linker.standard.ModuleSet.ModuleID

import org.scalajs.logging._

import Platform._

import Analysis._
import Infos.{NamespacedMethodName, ReachabilityInfo, ReachabilityInfoInClass}

final class Analyzer(config: CommonPhaseConfig, initial: Boolean,
    checkIR: Boolean, failOnError: Boolean, irLoader: IRLoader) {

  private val infoLoader: InfoLoader = {
    new InfoLoader(irLoader,
        if (!checkIR) InfoLoader.NoIRCheck
        else if (initial) InfoLoader.InitialIRCheck
        else InfoLoader.InternalIRCheck
    )
  }

  def computeReachability(moduleInitializers: Seq[ModuleInitializer],
      symbolRequirements: SymbolRequirement, logger: Logger)(implicit ec: ExecutionContext): Future[Analysis] = {

    infoLoader.update(logger)

    val run = new AnalyzerRun(config, initial, infoLoader)(
        adjustExecutionContextForParallelism(ec, config.parallel))

    run
      .computeReachability(moduleInitializers, symbolRequirements)
      .map { _ =>
        if (failOnError && run.errors.nonEmpty)
          reportErrors(run.errors, logger)

        run
      }
      .andThen { case _ => infoLoader.cleanAfterRun() }
  }

  private def reportErrors(errors: List[Error], logger: Logger): Unit = {
    require(errors.nonEmpty)

    val maxDisplayErrors = {
      val propName = "org.scalajs.linker.maxlinkingerrors"
      Try(System.getProperty(propName, "20").toInt).getOrElse(20).max(1)
    }

    errors
      .take(maxDisplayErrors)
      .foreach(logError(_, logger, Level.Error))

    val skipped = errors.size - maxDisplayErrors
    if (skipped > 0)
      logger.log(Level.Error, s"Not showing $skipped more linking errors")

    if (initial) {
      throw new LinkingException("There were linking errors")
    } else {
      throw new AssertionError(
          "There were linking errors after the optimizer has run. " +
          "This is a bug, please report it. " +
          "You can work around the bug by disabling the optimizer. " +
          "In the sbt plugin, this can be done with " +
          "`scalaJSLinkerConfig ~= { _.withOptimizer(false) }`.")
    }
  }
}

private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean,
    infoLoader: InfoLoader)(implicit ec: ExecutionContext) extends Analysis {
  import AnalyzerRun._

  private val allowAddingSyntheticMethods = initial
  private val checkAbstractReachability = initial

  private val isNoModule = config.coreSpec.moduleKind == ModuleKind.NoModule

  private val workTracker: WorkTracker = new WorkTracker
  private[this] val classLoader: ClassLoader = new ClassLoader

  private var objectClassInfo: ClassInfo = _
  private var _classInfos: scala.collection.Map[ClassName, ClassInfo] = _

  def classInfos: scala.collection.Map[ClassName, Analysis.ClassInfo] = _classInfos

  private[this] val _errors = new GrowingList[Error]

  override def errors: List[Error] = _errors.get()

  private val fromAnalyzer = FromCore("analyzer")

  private[this] val _topLevelExportInfos: mutable.Map[(ModuleID, String), TopLevelExportInfo] = emptyThreadSafeMap
  def topLevelExportInfos: scala.collection.Map[(ModuleID, String), Analysis.TopLevelExportInfo] = _topLevelExportInfos

  def computeReachability(moduleInitializers: Seq[ModuleInitializer],
      symbolRequirements: SymbolRequirement): Future[Unit] = {
    loadObjectClass(() => loadEverything(moduleInitializers, symbolRequirements))

    workTracker
      .allowComplete()
      .map(_ => postLoad(moduleInitializers))
  }

  private def loadObjectClass(onSuccess: () => Unit): Unit = {
    implicit val from = fromAnalyzer

    /* Load the java.lang.Object class, and validate it
     * If it is missing or invalid, we're in deep trouble, and cannot continue.
     */
    lookupClass(ObjectClass) { clazz =>
      if (!clazz.nonExistent) {
        objectClassInfo = clazz
        onSuccess()
      }
    }
  }

  private def loadEverything(moduleInitializers: Seq[ModuleInitializer],
      symbolRequirements: SymbolRequirement): Unit = {
    assert(objectClassInfo != null)

    implicit val from = fromAnalyzer

    /* java.lang.Object is always instantiated, because it is the
     * representative class for JS objects.
     */
    objectClassInfo.instantiated()

    /* Hijacked classes are always instantiated, because values of primitive
     * types are their instances.
     *
     * Also, they are part of the core infrastructure. As such, j.l.Object
     * depends on them.
     */
    for (hijacked <- HijackedClasses) {
      lookupClass(hijacked) { clazz =>
        objectClassInfo.addStaticDependency(clazz.className)
        clazz.instantiated()
      }
    }

    // External symbol requirements.
    reachSymbolRequirement(symbolRequirements)

    // Reach entry points
    for (className <- infoLoader.classesWithEntryPoints())
      lookupClass(className)(_.reachEntryPoints())

    // Reach module initializers.
    reachInitializers(moduleInitializers)
  }

  private def postLoad(moduleInitializers: Seq[ModuleInitializer]): Unit = {
    _classInfos = classLoader.loadedInfos()

    if (isNoModule) {
      // Check there is only a single module.
      val publicModuleIDs = (
         _topLevelExportInfos.keys.map(_._1).toList ++
         moduleInitializers.map(i => ModuleID(i.moduleID))
      ).distinct

      if (publicModuleIDs.size > 1)
        _errors ::= MultiplePublicModulesWithoutModuleSupport(publicModuleIDs)
    }

    // Reach additional data, based on reflection methods used
    reachDataThroughReflection()
  }

  private def reachSymbolRequirement(requirement: SymbolRequirement): Unit = {

    /* We use j.l.Object as representation of the core infrastructure.
     * As such, everything depends on j.l.Object and j.l.Object depends on all
     * symbol requirements.
     */

    import SymbolRequirement.Nodes._

    requirement match {
      case AccessModule(origin, moduleName) =>
        implicit val from = FromCore(origin)
        lookupClass(moduleName) { clazz =>
          objectClassInfo.addStaticDependency(clazz.className)
          clazz.accessModule()
        }

      case InstantiateClass(origin, className, constructor) =>
        implicit val from = FromCore(origin)
        lookupClass(className) { clazz =>
          objectClassInfo.addStaticDependency(clazz.className)
          clazz.instantiated()
          clazz.callMethodStatically(MemberNamespace.Constructor, constructor)
        }

      case InstanceTests(origin, className) =>
        implicit val from = FromCore(origin)
        lookupClass(className){ clazz =>
          objectClassInfo.addStaticDependency(clazz.className)
          clazz.useInstanceTests()
        }

      case ClassData(origin, className) =>
        implicit val from = FromCore(origin)
        lookupClass(className) { clazz =>
          objectClassInfo.addStaticDependency(clazz.className)
          clazz.accessData()
        }

      case CallMethod(origin, className, methodName, statically) =>
        implicit val from = FromCore(origin)
        lookupClass(className) { clazz =>
          if (statically) {
            objectClassInfo.addStaticDependency(clazz.className)
            clazz.callMethodStatically(MemberNamespace.Public, methodName)
          } else {
            clazz.callMethod(methodName)
          }
        }

      case CallStaticMethod(origin, className, methodName) =>
        implicit val from = FromCore(origin)
        lookupClass(className) { clazz =>
          objectClassInfo.addStaticDependency(clazz.className)
          clazz.callMethodStatically(MemberNamespace.PublicStatic, methodName)
        }

      case Multiple(requirements) =>
        for (requirement <- requirements)
          reachSymbolRequirement(requirement)

      case NoRequirement => // skip
    }
  }

  private def reachInitializers(
      moduleInitializers: Seq[ModuleInitializer]): Unit = {
    implicit val from = FromCore("module initializers")

    for (moduleInitializer <- moduleInitializers) {
      import ModuleInitializerImpl._

      fromInitializer(moduleInitializer.initializer) match {
        case VoidMainMethod(className, mainMethodName) =>
          lookupClass(className) { classInfo =>
            classInfo.callMethodStatically(MemberNamespace.PublicStatic, mainMethodName)
          }

        case MainMethodWithArgs(className, mainMethodName, _) =>
          lookupClass(className) { classInfo =>
            classInfo.callMethodStatically(MemberNamespace.PublicStatic, mainMethodName)
          }

          // For new Array[String]
          lookupClass(BoxedStringClass)(_.accessData())
      }
    }
  }

  /** Reach additional class data based on reflection methods being used. */
  private def reachDataThroughReflection(): Unit = {

    val classClassInfo = _classInfos.get(ClassClass)

    /* If Class.getSuperclass() is reachable, we can reach the data of all
     * superclasses of classes whose data we can already reach.
     */
    for {
      getSuperclassMethodInfo <-
        classClassInfo.flatMap(_.publicMethodInfos.get(getSuperclassMethodName))
      if getSuperclassMethodInfo.isReachable
    } {
      // calledFrom should always be nonEmpty if isReachable, but let's be robust
      implicit val from =
        getSuperclassMethodInfo.calledFrom.headOption.getOrElse(fromAnalyzer)
      for (classInfo <- _classInfos.values.filter(_.isDataAccessed).toList) {
        @tailrec
        def loop(classInfo: ClassInfo): Unit = {
          classInfo.accessData()
          classInfo.superClass match {
            case Some(superClass) =>
              classInfo.addStaticDependency(superClass.className)
              loop(superClass)

            case None =>
          }
        }
        loop(classInfo)
      }
    }
  }

  private def lookupClass(className: ClassName)(
      onSuccess: ClassInfo => Unit)(implicit from: From): Unit = {
    workTracker.track {
      classLoader.lookupClass(className).map {
        case info: ClassInfo =>
          info.link()
          onSuccess(info)

        case CycleInfo(cycle, root) =>
          assert(root == null, s"unresolved root: $root")
          _errors ::= CycleInInheritanceChain(cycle, fromAnalyzer)
      }
    }
  }

  private final class ClassLoader(implicit ec: ExecutionContext) {
    private[this] val _classInfos = emptyThreadSafeMap[ClassName, ClassLoadingState]

    def lookupClass(className: ClassName): Future[LoadingResult] = {
      ensureLoading(className) match {
        case loading: LoadingClass => loading.result
        case info: ClassInfo       => Future.successful(info)
      }
    }

    def loadedInfos(): scala.collection.Map[ClassName, ClassInfo] = {
      // Assemble loaded infos.
      val infos = _classInfos.collect { case (k, i: ClassInfo) => (k, i) }

      assert(_errors.get().nonEmpty || infos.size == _classInfos.size,
        "unloaded classes in post load phase")

      infos
    }

    private def lookupClassForLinking(className: ClassName,
        origin: LoadingClass): Future[LoadingResult] = {
      ensureLoading(className) match {
        case loading: LoadingClass => loading.requestLink(origin)
        case info: ClassInfo       => Future.successful(info)
      }
    }

    private def ensureLoading(className: ClassName): ClassLoadingState = {
      var loading: LoadingClass = null
      val state = _classInfos.getOrElseUpdate(className, {
        loading = new LoadingClass(className)
        loading
      })

      if (state eq loading) {
        // We just added `loading`, actually load.
        val maybeInfo = infoLoader.loadInfo(className)
        val info = maybeInfo.getOrElse {
          Future.successful(createMissingClassInfo(className))
        }

        val result = info.flatMap { data =>
          doLoad(data, loading, nonExistent = maybeInfo.isEmpty)
        }

        loading.completeWith(result)
      }

      state
    }

    private def doLoad(data: Infos.ClassInfo, origin: LoadingClass,
        nonExistent: Boolean): Future[LoadingResult] = {
      val className = data.className

      for {
        maybeAncestors <- Future.traverse(data.superClass.toList ++ data.interfaces)(
            lookupClassForLinking(_, origin))
      } yield {
        val maybeCycle = maybeAncestors.collectFirst {
          case cycle @ CycleInfo(_, null) => cycle

          case CycleInfo(c, root) if root == className =>
            CycleInfo(className :: c, null)

          case CycleInfo(c, root) =>
            CycleInfo(className :: c, root)
        }

        maybeCycle.getOrElse {
          val ancestors = maybeAncestors.asInstanceOf[List[ClassInfo]]

          val (superClass, interfaces) =
            if (data.superClass.isEmpty) (None, ancestors)
            else (Some(ancestors.head), ancestors.tail)

          val info = new ClassInfo(data, superClass, interfaces, nonExistent)

          _classInfos.put(className, info)

          implicit val from = FromClass(info)
          ancestors.foreach(_.link())

          info
        }
      }
    }
  }

  private sealed trait LoadingResult
  private sealed trait ClassLoadingState

  // sealed instead of final because of spurious unchecked warnings
  private sealed case class CycleInfo(cycle: List[ClassName], root: ClassName)
      extends LoadingResult

  private final class LoadingClass(className: ClassName)
      extends ClassLoadingState {

    private val promise = Promise[LoadingResult]()
    private val knownDescendants = emptyThreadSafeMap[LoadingClass, Unit]

    knownDescendants.update(this, ())

    def requestLink(origin: LoadingClass): Future[LoadingResult] = {
      if (origin.knownDescendants.contains(this)) {
        Future.successful(CycleInfo(Nil, className))
      } else {
        this.knownDescendants ++= origin.knownDescendants
        promise.future
      }
    }

    def result: Future[LoadingResult] = promise.future

    def completeWith(result: Future[LoadingResult]): Unit =
      promise.completeWith(result)
  }

  private sealed trait ModuleUnit {
    def addStaticDependency(clazz: ClassName): Unit
    def addExternalDependency(module: String): Unit
    def addDynamicDependency(clazz: ClassName): Unit
  }

  private class ClassInfo(
      val data: Infos.ClassInfo,
      unvalidatedSuperClass: Option[ClassInfo],
      unvalidatedInterfaces: List[ClassInfo],
      val nonExistent: Boolean)
      extends Analysis.ClassInfo with ClassLoadingState with LoadingResult with ModuleUnit {

    private[this] val _linkedFrom = new GrowingList[From]
    def linkedFrom: List[From] = _linkedFrom.get()

    val className = data.className
    val kind = data.kind
    val isAnyModuleClass =
      data.kind.hasModuleAccessor || data.kind == ClassKind.NativeJSModuleClass
    val isInterface = data.kind == ClassKind.Interface
    val isScalaClass = data.kind.isClass || data.kind == ClassKind.HijackedClass
    val isJSClass = data.kind.isJSClass
    val isJSType = data.kind.isJSType
    val isAnyClass = isScalaClass || isJSClass
    val isNativeJSClass =
      kind == ClassKind.NativeJSClass || kind == ClassKind.NativeJSModuleClass

    val superClass: Option[ClassInfo] =
      validateSuperClass(unvalidatedSuperClass)

    val interfaces: List[ClassInfo] =
      validateInterfaces(unvalidatedInterfaces)

    /** Ancestors of this class or interface.
     *
     *  This always includes this class and `java.lang.Object`.
     */
    val ancestors: List[ClassInfo] = {
      if (className == ObjectClass) {
        this :: Nil
      } else {
        val parents = superClass.getOrElse(objectClassInfo) :: interfaces
        this +: parents.flatMap(_.ancestors).distinct
      }
    }

    def link()(implicit from: From): Unit = {
      if (nonExistent)
        _errors ::= MissingClass(this, from)

      _linkedFrom ::= from
    }

    private[this] def validateSuperClass(superClass: Option[ClassInfo]): Option[ClassInfo] = {
      def from = FromClass(this)

      kind match {
        case _ if className == ObjectClass =>
          assert(superClass.isEmpty)

          None

        case ClassKind.Class | ClassKind.ModuleClass | ClassKind.HijackedClass =>
          val superCl = superClass.get // checked by ClassDef checker.
          if (superCl.kind != ClassKind.Class) {
            _errors ::= InvalidSuperClass(superCl, this, from)
            Some(objectClassInfo)
          } else {
            superClass
          }

        case ClassKind.Interface =>
          assert(superClass.isEmpty)

          None

        case ClassKind.JSClass | ClassKind.JSModuleClass =>
          /* There is no correct fallback in case of error, here. The logical
           * thing to do would be to pick `js.Object`, but we cannot be sure
           * that `js.Object` and its inheritance chain are valid themselves.
           * So we just say superClass = None in invalid cases, and make sure
           * this does not blow up the rest of the analysis.
           */
          val superCl = superClass.get // checked by ClassDef checker.
          superCl.kind match {
            case ClassKind.JSClass | ClassKind.NativeJSClass =>
              superClass // ok
            case _ =>
              _errors ::= InvalidSuperClass(superCl, this, from)
              None
          }

        case ClassKind.NativeJSClass | ClassKind.NativeJSModuleClass =>
          val superCl = superClass.get // checked by ClassDef checker.
          superCl.kind match {
            case ClassKind.JSClass | ClassKind.NativeJSClass =>
              superClass // ok
            case _ if superCl eq objectClassInfo =>
              superClass // ok
            case _ =>
              _errors ::= InvalidSuperClass(superCl, this, from)
              Some(objectClassInfo)
          }

        case ClassKind.AbstractJSType =>
          superClass.flatMap { superCl =>
            superCl.kind match {
              case ClassKind.JSClass | ClassKind.NativeJSClass =>
                superClass // ok
              case _ if superCl eq objectClassInfo =>
                superClass // ok
              case _ =>
                _errors ::= InvalidSuperClass(superCl, this, from)
                None
            }
          }
      }
    }

    private[this] def validateInterfaces(interfaces: List[ClassInfo]): List[ClassInfo] = {
      def from = FromClass(this)

      val validSuperIntfKind = kind match {
        case ClassKind.Class | ClassKind.ModuleClass |
            ClassKind.HijackedClass | ClassKind.Interface =>
          ClassKind.Interface
        case ClassKind.JSClass | ClassKind.JSModuleClass |
            ClassKind.NativeJSClass | ClassKind.NativeJSModuleClass |
            ClassKind.AbstractJSType =>
          ClassKind.AbstractJSType
      }

      interfaces.filter { superIntf =>
        if (superIntf.nonExistent) {
          // Remove it but do not report an additional error message
          false
        } else if (superIntf.kind != validSuperIntfKind) {
          _errors ::= InvalidImplementedInterface(superIntf, this, from)
          false
        } else {
          true
        }
      }
    }

    private[this] val _isInstantiated = new AtomicBoolean(false)
    def isInstantiated: Boolean = _isInstantiated.get()

    private[this] val _isAnySubclassInstantiated = new AtomicBoolean(false)
    def isAnySubclassInstantiated: Boolean = _isAnySubclassInstantiated.get()

    private[this] val isModuleAccessed = new AtomicBoolean(false)

    private[this] val _areInstanceTestsUsed = new AtomicBoolean(false)
    def areInstanceTestsUsed: Boolean = _areInstanceTestsUsed.get()

    private[this] val _isDataAccessed = new AtomicBoolean(false)
    def isDataAccessed: Boolean = _isDataAccessed.get()

    private[this] val _fieldsRead: mutable.Map[FieldName, Unit] = emptyThreadSafeMap
    private[this] val _fieldsWritten: mutable.Map[FieldName, Unit] = emptyThreadSafeMap
    val _staticFieldsRead: mutable.Map[FieldName, Unit] = emptyThreadSafeMap
    val _staticFieldsWritten: mutable.Map[FieldName, Unit] = emptyThreadSafeMap

    def fieldsRead: scala.collection.Set[FieldName] = _fieldsRead.keySet
    def fieldsWritten: scala.collection.Set[FieldName] = _fieldsWritten.keySet
    def staticFieldsRead: scala.collection.Set[FieldName] = _staticFieldsRead.keySet
    def staticFieldsWritten: scala.collection.Set[FieldName] = _staticFieldsWritten.keySet

    private[this] val _jsNativeMembersUsed: mutable.Map[MethodName, Unit] = emptyThreadSafeMap
    def jsNativeMembersUsed: scala.collection.Set[MethodName] = _jsNativeMembersUsed.keySet

    val jsNativeLoadSpec: Option[JSNativeLoadSpec] = data.jsNativeLoadSpec

    private[this] val _staticDependencies: mutable.Map[ClassName, Unit] = emptyThreadSafeMap
    private[this] val _externalDependencies: mutable.Map[String, Unit] = emptyThreadSafeMap
    private[this] val _dynamicDependencies: mutable.Map[ClassName, Unit] = emptyThreadSafeMap

    def addStaticDependency(clazz: ClassName): Unit = _staticDependencies.update(clazz, ())
    def addExternalDependency(module: String): Unit = _externalDependencies.update(module, ())
    def addDynamicDependency(clazz: ClassName): Unit = _dynamicDependencies.update(clazz, ())

    def staticDependencies: scala.collection.Set[ClassName] = _staticDependencies.keySet
    def externalDependencies: scala.collection.Set[String] = _externalDependencies.keySet
    def dynamicDependencies: scala.collection.Set[ClassName] = _dynamicDependencies.keySet

    /* j.l.Object represents the core infrastructure. As such, everything
     * depends on it unconditionally.
     */
    if (className != ObjectClass)
      addStaticDependency(ObjectClass)

    private[this] val _instantiatedFrom = new GrowingList[From]
    def instantiatedFrom: List[From] = _instantiatedFrom.get()

    private[this] val _dispatchCalledFrom: mutable.Map[MethodName, GrowingList[From]] = emptyThreadSafeMap
    def dispatchCalledFrom(methodName: MethodName): Option[List[From]] =
      _dispatchCalledFrom.get(methodName).map(_.get())

    /** Methods that have been called on this interface.
     *
     *  Note that we maintain the invariant
     *
     *  methodsCalledLog.toSet == dispatchCalledFrom.keySet.
     *
     *  This is because we need to be able to snapshot methodsCalledLog in
     *  subclassInstantiated. TrieMap would support snapshotting, but a plain
     *  mutable.Map doesn't (so it wouldn't cross compile to JS).
     */
    private val methodsCalledLog = new GrowingList[MethodName]

    /** List of all instantiated (Scala) subclasses of this Scala class/trait.
     *  For JS types, this always remains empty.
     */
    private val _instantiatedSubclasses = new GrowingList[ClassInfo]

    private val nsMethodInfos = {
      val nsMethodInfos = Array.fill(MemberNamespace.Count) {
        emptyThreadSafeMap[MethodName, MethodInfo]
      }
      for (methodData <- data.methods) {
        // TODO It would be good to report duplicates as errors at this point
        val relevantMap = nsMethodInfos(methodData.namespace.ordinal)
        relevantMap(methodData.methodName) = new MethodInfo(this, methodData)
      }
      nsMethodInfos
    }

    def methodInfos(
        namespace: MemberNamespace): mutable.Map[MethodName, MethodInfo] = {
      nsMethodInfos(namespace.ordinal)
    }

    val publicMethodInfos: mutable.Map[MethodName, MethodInfo] =
      methodInfos(MemberNamespace.Public)

    def lookupAbstractMethod(methodName: MethodName): MethodInfo = {
      val candidatesIterator = for {
        ancestor <- ancestors.iterator
        m <- ancestor.publicMethodInfos.get(methodName)
        if !m.isDefaultBridge && (!m.nonExistent || ancestor == this)
      } yield {
        m
      }

      if (candidatesIterator.isEmpty)
        createNonExistentPublicMethod(methodName)
      else
        candidatesIterator.next()
    }

    def lookupMethod(methodName: MethodName): MethodInfo = {
      tryLookupMethod(methodName).getOrElse {
        createNonExistentPublicMethod(methodName)
      }
    }

    private def createNonExistentPublicMethod(methodName: MethodName): MethodInfo = {
      /* Use getOrElseUpdate to avoid overriding an abstract method:
       * When being called from lookupMethod, it is possible that an abstract
       * method exists.
       */
      publicMethodInfos.getOrElseUpdate(methodName, {
        val syntheticData = makeSyntheticMethodInfo(methodName, MemberNamespace.Public)
        new MethodInfo(this, syntheticData, nonExistent = true)
      })
    }

    def tryLookupMethod(methodName: MethodName): Option[MethodInfo] = {
      assert(isScalaClass || isInterface,
          s"Cannot call lookupMethod($methodName) on non Scala class $this")

      publicMethodInfos.get(methodName) match {
        case Some(m) if !m.isAbstract => Some(m)

        case _ =>
          val candidate = superClass
            .flatMap(_.tryLookupMethod(methodName))
            .filterNot(_.nonExistent)

          if (allowAddingSyntheticMethods) {
            def maybeDefaultTarget = getDefaultTarget(methodName)

            def needsDefaultOverride(method: MethodInfo): Boolean = {
              /* The .get is OK, since we only get here if:
               * - This class doesn't implement the method directly.
               * - The superClass has found a default target.
               * In this case, we always find at least one target.
               */
              method.isDefaultBridge && method.defaultBridgeTarget != maybeDefaultTarget.get.owner.className
            }

            candidate
              .filterNot(needsDefaultOverride(_))
              .orElse(maybeDefaultTarget.map(createDefaultBridge(_)))
          } else {
            candidate
          }
      }
    }

    private val defaultTargets = emptyThreadSafeMap[MethodName, Option[MethodInfo]]

    private def getDefaultTarget(methodName: MethodName): Option[MethodInfo] =
      defaultTargets.getOrElseUpdate(methodName, findDefaultTarget(methodName))

    /** Resolves an inherited default method.
     *
     *  This lookup is specified by the JVM resolution rules for default
     *  methods. See the `invokespecial` opcode in the JVM Specification
     *  version 8, Section 6.5:
     *  https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.invokespecial
     */
    private def findDefaultTarget(methodName: MethodName): Option[MethodInfo] = {
      val candidates = for {
        intf <- ancestors if intf.isInterface
        m <- intf.publicMethodInfos.get(methodName)
        if !m.isAbstract && !m.isDefaultBridge && !m.nonExistent
      } yield m

      val notShadowed = candidates filterNot { m =>
        candidates exists { n =>
          (n ne m) && n.owner.ancestors.contains(m.owner)
        }
      }

      if (notShadowed.size > 1) {
        /* Deviation from the spec: if there are several targets, the spec
         * chooses one arbitrarily. However, unless the classpath is
         * manipulated and/or corrupted, this should not happen. The Java
         * *language* and compiler do not let this happen on their own.
         * Besides, the current implementation of the JVM throws an
         * IncompatibleClassChangeError when trying to resolve such ambiguous
         * references.
         * So we emit an error too, so that we can more easily discover bugs.
         * We use fromAnalyzer because we don't have any From here (we
         * shouldn't, since lookup methods are not supposed to produce errors).
         */
        _errors ::= ConflictingDefaultMethods(notShadowed, fromAnalyzer)
      }

      notShadowed.headOption
    }

    private def createDefaultBridge(target: MethodInfo): MethodInfo = {
      val methodName = target.methodName

      publicMethodInfos.getOrElseUpdate(methodName, {
        val targetOwner = target.owner

        val syntheticInfo = makeSyntheticMethodInfo(
            methodName = methodName,
            namespace = MemberNamespace.Public,
            methodsCalledStatically = List(
                targetOwner.className -> NamespacedMethodName(MemberNamespace.Public, methodName)))
        new MethodInfo(this, syntheticInfo,
            syntheticKind = MethodSyntheticKind.DefaultBridge(targetOwner.className))
      })
    }

    private def maybeReachReflProxyMethod(proxyName: MethodName)(implicit from: From): Unit = {
      if (!allowAddingSyntheticMethods) {
        tryLookupMethod(proxyName).foreach(_.reach(this))
      } else {
        publicMethodInfos
          .get(proxyName)
          .fold(findAndReachReflectiveTarget(proxyName))(_.reach(this))
      }
    }

    private def findAndReachReflectiveTarget(
        proxyName: MethodName)(implicit from: From): Unit = {
      /* The lookup for a target method in this code implements the
       * algorithm defining `java.lang.Class.getMethod`. This mimics how
       * reflective calls are implemented on the JVM, at link time.
       *
       * We add a bit of guess-work for default methods, as the documentation
       * is very vague about them. Basically, we just take the first match in
       * `ancestors`, as it's easy, and we're in a gray area anyway. At least,
       * this will work when there is no overload.
       *
       * Caveat: protected methods are not ignored. This can only make an
       * otherwise invalid reflective call suddenly able to call a protected
       * method. It never breaks valid reflective calls. This could be fixed
       * if the IR retained the information that a method is protected.
       */

      @tailrec
      def findFirstNonEmptyCandidates(ancestors: List[ClassInfo]): List[MethodInfo] = {
        ancestors match {
          case ancestor :: nextAncestors =>
            val candidates = ancestor.findProxyCandidates(proxyName)
            if (candidates.isEmpty)
              findFirstNonEmptyCandidates(nextAncestors)
            else
              candidates
          case Nil =>
            Nil
        }
      }

      val candidates = findFirstNonEmptyCandidates(ancestorsInReflectiveTargetOrder)

      candidates match {
        case Nil =>
          ()

        case onlyCandidate :: Nil =>
          // Fast path that does not require workTracker.track
          createReflProxy(proxyName, onlyCandidate.methodName).reach(this)

        case _ =>
          val future = for {
            reflectiveTarget <- computeMostSpecificProxyMatch(candidates)
          } yield {
            createReflProxy(proxyName, reflectiveTarget.methodName).reach(this)
          }

          workTracker.track(future)
      }
    }

    private lazy val ancestorsInReflectiveTargetOrder: List[ClassInfo] = {
      val b = new mutable.ListBuffer[ClassInfo]

      @tailrec
      def addSuperClasses(superClass: ClassInfo): Unit = {
        b += superClass
        superClass.superClass match {
          case Some(next) => addSuperClasses(next)
          case None       => ()
        }
      }
      addSuperClasses(this)

      b.prependToList(ancestors.filter(_.isInterface))
    }

    private def findProxyCandidates(proxyName: MethodName): List[MethodInfo] =
      proxyCandidates.getOrElse(proxyName, Nil)

    private lazy val proxyCandidates = {
      val result = mutable.Map.empty[MethodName, List[MethodInfo]]
      val iter = publicMethodInfos.valuesIterator
      while (iter.hasNext) {
        val m = iter.next()
        val include = {
          // TODO In theory we should filter out protected methods
          !m.isReflectiveProxy && !m.isDefaultBridge && !m.isAbstract && !m.nonExistent
        }
        if (include) {
          val proxyName = MethodName.reflectiveProxy(m.methodName.simpleName, m.methodName.paramTypeRefs)
          val prev = result.getOrElse(proxyName, Nil)
          result.update(proxyName, m :: prev)
        }
      }
      result
    }

    private def computeMostSpecificProxyMatch(candidates: List[MethodInfo])(
        implicit from: From): Future[MethodInfo] = {

      /* From the JavaDoc of java.lang.Class.getMethod:
       *
       *   If more than one [candidate] method is found in C, and one of these
       *   methods has a return type that is more specific than any of the
       *   others, that method is reflected; otherwise one of the methods is
       *   chosen arbitrarily.
       */

      def ifMostSpecific(candidate: MethodInfo): Future[Option[MethodInfo]] = {
        val specificityChecks = for {
          otherCandidate <- candidates
          if candidate != otherCandidate
        } yield {
          isMoreSpecific(otherCandidate.methodName.resultTypeRef,
              candidate.methodName.resultTypeRef)
        }

        for {
          moreSpecific <- Future.find(specificityChecks)(identity)
        } yield {
          if (moreSpecific.isEmpty) Some(candidate)
          else None
        }
      }

      val specificCandidates = candidates.map(ifMostSpecific)

      /* This last step (chosen arbitrarily) causes some soundness issues of
       * the implementation of reflective calls. This is bug-compatible with
       * Scala/JVM.
       */
      for {
        candidate <- Future.find(specificCandidates)(_.nonEmpty)
      } yield {
        /* First get: There must be a most specific candidate.
         * Second get: That's our find condition from above.
         */
        candidate.get.get
      }
    }

    private def reflProxyMatches(methodName: MethodName,
        proxyName: MethodName): Boolean = {
      methodName.simpleName == proxyName.simpleName &&
      methodName.paramTypeRefs == proxyName.paramTypeRefs
    }

    private def isMoreSpecific(left: ir.Types.TypeRef, right: ir.Types.TypeRef)(
        implicit from: From): Future[Boolean] = {
      import ir.Types._

      def classIsMoreSpecific(leftCls: ClassName, rightCls: ClassName): Future[Boolean] = {
        if (leftCls == rightCls) {
          Future.successful(false)
        } else {
          val promise = Promise[Boolean]()

          lookupClass(leftCls) { leftInfo =>
            lookupClass(rightCls) { rightInfo =>
              promise.success(leftInfo.ancestors.contains(rightInfo))
            }
          }

          promise.future
        }
      }

      (left, right) match {
        case (ClassRef(leftCls), ClassRef(rightCls)) =>
          classIsMoreSpecific(leftCls, rightCls)
        case (ArrayTypeRef(ClassRef(leftBaseCls), leftDepth),
            ArrayTypeRef(ClassRef(rightBaseCls), rightDepth)) =>
          if (leftDepth != rightDepth) Future.successful(false)
          else classIsMoreSpecific(leftBaseCls, rightBaseCls)
        case (ArrayTypeRef(_, _), ClassRef(ObjectClass)) =>
          Future.successful(true)
        case _ =>
          Future.successful(false)
      }
    }

    private def createReflProxy(proxyName: MethodName,
        targetName: MethodName): MethodInfo = {
      assert(this.isScalaClass,
          s"Cannot create reflective proxy in non-Scala class $this")

      publicMethodInfos.getOrElseUpdate(proxyName, {
        val syntheticInfo = makeSyntheticMethodInfo(
            methodName = proxyName,
            namespace = MemberNamespace.Public,
            methodsCalled = List(this.className -> targetName))
        new MethodInfo(this, syntheticInfo,
            syntheticKind = MethodSyntheticKind.ReflectiveProxy(targetName))
      })
    }

    def lookupStaticLikeMethod(namespace: MemberNamespace,
        methodName: MethodName): MethodInfo = {
      assert(namespace != MemberNamespace.Public)

      methodInfos(namespace).getOrElseUpdate(methodName, {
        val syntheticData = makeSyntheticMethodInfo(methodName, namespace)
        new MethodInfo(this, syntheticData, nonExistent = true)
      })
    }

    def tryLookupStaticLikeMethod(namespace: MemberNamespace,
        methodName: MethodName): Option[MethodInfo] = {
      assert(namespace != MemberNamespace.Public)
      methodInfos(namespace).get(methodName)
    }

    override def toString(): String = className.nameString

    def reachEntryPoints(): Unit = {
      implicit val from = FromExports

      // Static initializer
      tryLookupStaticLikeMethod(MemberNamespace.StaticConstructor,
          StaticInitializerName).foreach {
        _.reachStatic()(fromAnalyzer)
      }

      // Top Level Exports
      for (tle <- data.topLevelExports) {
        val key = (tle.moduleID, tle.exportName)
        val info = new TopLevelExportInfo(className, tle)
        info.reach()

        _topLevelExportInfos.put(key, info).foreach { other =>
          _errors ::= ConflictingTopLevelExport(tle.moduleID, tle.exportName, List(info, other))
        }
      }
    }

    def accessModule()(implicit from: From): Unit = {
      if (!isAnyModuleClass) {
        _errors ::= NotAModule(this, from)
      } else if (!isModuleAccessed.getAndSet(true)) {
        instantiated() // TODO: Shouldn't we always add the from?
        if (isScalaClass)
          callMethodStatically(MemberNamespace.Constructor, NoArgConstructorName)
      }
    }

    def instantiated()(implicit from: From): Unit = {
      _instantiatedFrom ::= from

      if (!(isScalaClass || isJSClass || isNativeJSClass)) {
        /* Ignore.
         * TODO? Shouldn't this be a linking error
         * instead?
         */
      } else if (!_isInstantiated.getAndSet(true)) {

        // TODO: Why is this not in subclassInstantiated()?
        referenceFieldClasses(fieldsRead ++ fieldsWritten)

        if (isScalaClass) {
          accessData()

          /* First mark the ancestors as subclassInstantiated() then fetch the
           * methodsCalledLog, for all ancestors. This order is important to
           * ensure that concurrently analyzed method calls work correctly.
           *
           * Further, we only actually perform the resolved calls once we have
           * fetched all the logs. This is to minimize duplicate work:
           * during the resolved calls, new methods could be called and added
           * to the log; they will already see the new subclasses so we should
           * *not* see them in the logs, lest we perform that work twice.
           */

          val allMethodsCalledLogs = for (ancestor <- ancestors) yield {
            ancestor.subclassInstantiated()
            ancestor._instantiatedSubclasses ::= this
            ancestor -> ancestor.methodsCalledLog.get()
          }

          for {
            (ancestor, ancestorLog) <- allMethodsCalledLogs
            methodName <- ancestorLog
          } {
            implicit val from = FromDispatch(ancestor, methodName)
            callMethodResolved(methodName)
          }
        } else {
          assert(isJSClass || isNativeJSClass)

          subclassInstantiated()

          if (isJSClass) {
            superClass.foreach(_.instantiated())
            tryLookupStaticLikeMethod(MemberNamespace.StaticConstructor,
                ClassInitializerName).foreach {
              staticInit => staticInit.reachStatic()
            }
          } else {
            for (jsNativeLoadSpec <- data.jsNativeLoadSpec)
              validateLoadSpec(jsNativeLoadSpec, jsNativeMember = None)
          }

          for (reachabilityInfo <- data.jsMethodProps)
            followReachabilityInfo(reachabilityInfo, this)(FromExports)
        }
      }
    }

    private def subclassInstantiated()(implicit from: From): Unit = {
      _instantiatedFrom ::= from

      if (!(isScalaClass || isJSType)) {
        // Ignore
      } else if (!_isAnySubclassInstantiated.getAndSet(true)) {

        if (!isNativeJSClass) {
          for (clazz <- superClass) {
            if (clazz.isNativeJSClass)
              clazz.jsNativeLoadSpec.foreach(addLoadSpec(this, _))
            else
              addStaticDependency(clazz.className)
          }
        }

        // Reach exported members
        if (!isJSClass) {
          for (reachabilityInfo <- data.jsMethodProps)
            followReachabilityInfo(reachabilityInfo, this)(FromExports)
        }
      }
    }

    def useInstanceTests()(implicit from: From): Unit = {
      _areInstanceTestsUsed.set(true)
    }

    def accessData()(implicit from: From): Unit = {
      if (!_isDataAccessed.getAndSet(true)) {
        // #4548 The `isInstance` function will refer to the class value
        if (kind == ClassKind.NativeJSClass)
          jsNativeLoadSpec.foreach(addLoadSpec(this, _))
      }
    }

    def callMethod(methodName: MethodName)(implicit from: From): Unit = {
      /* First add the call to the log, then fetch the instantiated subclasses,
       * then perform the resolved call. This order is important because,
       * during the resolved calls, new instantiated subclasses could be
       * detected, and those need to see the updated log, since the loop in
       * this method won't see them.
       */

      val froms = _dispatchCalledFrom.getOrElseUpdate(methodName, new GrowingList)

      if (froms.addIfNil(from)) {
        // New call.
        val fromDispatch = FromDispatch(this, methodName)

        methodsCalledLog ::= methodName
        val subclasses = _instantiatedSubclasses.get()
        for (subclass <- subclasses)
          subclass.callMethodResolved(methodName)(fromDispatch)

        if (checkAbstractReachability) {
          /* Also lookup the method as abstract from this class, to make sure it
           * is *declared* on this type. We do this after the concrete lookup to
           * avoid work, since a concretely reachable method is already marked as
           * abstractly reachable.
           */
          if (!methodName.isReflectiveProxy)
            lookupAbstractMethod(methodName).reachAbstract()(fromDispatch)
        }
      } else {
        // Already called before; add the new from
        froms ::= from
      }
    }

    private def callMethodResolved(methodName: MethodName)(
        implicit from: From): Unit = {
      if (methodName.isReflectiveProxy) {
        maybeReachReflProxyMethod(methodName)
      } else {
        lookupMethod(methodName).reach(this)
      }
    }

    def callMethodStatically(namespacedMethodName: NamespacedMethodName)(
        implicit from: From): Unit = {
      callMethodStatically(namespacedMethodName.namespace,
          namespacedMethodName.methodName)
    }

    def callMethodStatically(namespace: MemberNamespace,
        methodName: MethodName)(
        implicit from: From): Unit = {
      assert(!methodName.isReflectiveProxy,
          s"Trying to call statically refl proxy $this.$methodName")
      if (namespace != MemberNamespace.Public)
        lookupStaticLikeMethod(namespace, methodName).reachStatic()
      else
        lookupMethod(methodName).reachStatic()
    }

    def readFields(names: List[FieldName])(implicit from: From): Unit = {
      names.foreach(_fieldsRead.update(_, ()))
      if (isInstantiated)
        referenceFieldClasses(names)
    }

    def writeFields(names: List[FieldName])(implicit from: From): Unit = {
      names.foreach(_fieldsWritten.update(_, ()))
      if (isInstantiated)
        referenceFieldClasses(names)
    }

    def useJSNativeMember(name: MethodName)(
        implicit from: From): Option[JSNativeLoadSpec] = {
      val maybeJSNativeLoadSpec = data.jsNativeMembers.get(name)
      if (_jsNativeMembersUsed.put(name, ()).isEmpty) {
        maybeJSNativeLoadSpec match {
          case None =>
            _errors ::= MissingJSNativeMember(this, name, from)
          case Some(jsNativeLoadSpec) =>
            validateLoadSpec(jsNativeLoadSpec, Some(name))
        }
      }
      maybeJSNativeLoadSpec
    }

    private def referenceFieldClasses(fieldNames: Iterable[FieldName])(
        implicit from: From): Unit = {
      assert(isInstantiated)

      /* Reach referenced classes of non-static fields
       *
       * We do not need to add this to staticDependencies: The definition
       * site will not reference the classes in the final JS code.
       */
      for {
        fieldName <- fieldNames
        className <- data.referencedFieldClasses.get(fieldName)
      } {
        lookupClass(className)(_ => ())
      }
    }

    private def validateLoadSpec(jsNativeLoadSpec: JSNativeLoadSpec,
        jsNativeMember: Option[MethodName])(implicit from: From): Unit = {
      if (isNoModule) {
        jsNativeLoadSpec match {
          case JSNativeLoadSpec.Import(module, _) =>
            _errors ::= ImportWithoutModuleSupport(module, this, jsNativeMember, from)
          case _ =>
        }
      }
    }
  }

  private class MethodInfo(
    val owner: ClassInfo,
    data: Infos.MethodInfo,
    val nonExistent: Boolean = false,
    val syntheticKind: MethodSyntheticKind = MethodSyntheticKind.None
  ) extends Analysis.MethodInfo {

    val methodName = data.methodName
    val namespace = data.namespace
    val isAbstract = data.isAbstract

    private[this] val _isAbstractReachable = new AtomicBoolean(false)
    def isAbstractReachable: Boolean = _isAbstractReachable.get()

    private[this] val _isReachable = new AtomicBoolean(false)
    def isReachable: Boolean = _isReachable.get()

    private[this] val _calledFrom = new GrowingList[From]
    def calledFrom: List[From] = _calledFrom.get()

    private[this] val _instantiatedSubclasses = new GrowingList[ClassInfo]
    def instantiatedSubclasses: List[ClassInfo] = _instantiatedSubclasses.get()

    def isReflectiveProxy: Boolean =
      methodName.isReflectiveProxy

    def isDefaultBridge: Boolean =
      syntheticKind.isInstanceOf[MethodSyntheticKind.DefaultBridge]

    /** Throws MatchError if `!isDefaultBridge`. */
    def defaultBridgeTarget: ClassName = (syntheticKind: @unchecked) match {
      case MethodSyntheticKind.DefaultBridge(target) => target
    }

    override def toString(): String =
      s"$owner.${methodName.simpleName.nameString}"

    def reachStatic()(implicit from: From): Unit = {
      checkConcrete()

      _calledFrom ::= from
      if (!_isReachable.getAndSet(true)) {
        _isAbstractReachable.set(true)
        doReach()
      }
    }

    def reachAbstract()(implicit from: From): Unit = {
      assert(namespace == MemberNamespace.Public)

      if (!_isAbstractReachable.getAndSet(true)) {
        checkExistent()
        _calledFrom ::= from
      }
    }

    def reach(inClass: ClassInfo)(implicit from: From): Unit = {
      assert(!namespace.isStatic,
          s"Trying to dynamically reach the static method $this")
      assert(owner.isAnyClass,
          s"Trying to dynamically reach the non-class method $this")
      assert(!namespace.isConstructor,
          s"Trying to dynamically reach the constructor $this")

      checkConcrete()

      _calledFrom ::= from
      _instantiatedSubclasses ::= inClass

      if (!_isReachable.getAndSet(true)) {
        _isAbstractReachable.set(true)
        doReach()
      }
    }

    private def checkExistent()(implicit from: From) = {
      if (nonExistent)
        _errors ::= MissingMethod(this, from)
    }

    private def checkConcrete()(implicit from: From) = {
      if (nonExistent || isAbstract)
        _errors ::= MissingMethod(this, from)
    }

    private[this] def doReach(): Unit =
      followReachabilityInfo(data.reachabilityInfo, owner)(FromMethod(this))
  }

  private class TopLevelExportInfo(val owningClass: ClassName, data: Infos.TopLevelExportInfo)
      extends Analysis.TopLevelExportInfo with ModuleUnit {
    val moduleID: ModuleID = data.moduleID
    val exportName: String = data.exportName

    if (isNoModule && !ir.Trees.JSGlobalRef.isValidJSGlobalRefName(exportName)) {
      _errors ::= InvalidTopLevelExportInScript(this)
    }

    private[this] val _staticDependencies: mutable.Map[ClassName, Unit] = emptyThreadSafeMap
    private[this] val _externalDependencies: mutable.Map[String, Unit] = emptyThreadSafeMap

    def addStaticDependency(clazz: ClassName): Unit = _staticDependencies.update(clazz, ())
    def addExternalDependency(module: String): Unit = _externalDependencies.update(module, ())
    def addDynamicDependency(clazz: ClassName): Unit = {
      throw new AssertionError("dynamic dependency for top level export " +
          s"$moduleID.$exportName (owned by $owningClass) on $clazz")
    }

    def staticDependencies: scala.collection.Set[ClassName] = _staticDependencies.keySet
    def externalDependencies: scala.collection.Set[String] = _externalDependencies.keySet

    def reach(): Unit = followReachabilityInfo(data.reachability, this)(FromExports)
  }

  private def followReachabilityInfo(data: ReachabilityInfo, moduleUnit: ModuleUnit)(
      implicit from: From): Unit = {

    def addInstanceDependency(info: ClassInfo) = {
      info.jsNativeLoadSpec.foreach(addLoadSpec(moduleUnit, _))
      if (info.kind.isAnyNonNativeClass)
        moduleUnit.addStaticDependency(info.className)
    }

    for (dataInClass <- data.byClass) {
      lookupClass(dataInClass.className) { clazz =>
        val className = dataInClass.className

        val flags = dataInClass.flags
        if (flags != 0) {
          if ((flags & ReachabilityInfoInClass.FlagModuleAccessed) != 0) {
            clazz.accessModule()
            addInstanceDependency(clazz)
          }

          if ((flags & ReachabilityInfoInClass.FlagInstantiated) != 0) {
            clazz.instantiated()
            addInstanceDependency(clazz)
          }

          if ((flags & ReachabilityInfoInClass.FlagInstanceTestsUsed) != 0) {
            moduleUnit.addStaticDependency(className)
            clazz.useInstanceTests()
          }

          if ((flags & ReachabilityInfoInClass.FlagClassDataAccessed) != 0) {
            moduleUnit.addStaticDependency(className)
            clazz.accessData()
          }

          if ((flags & ReachabilityInfoInClass.FlagStaticallyReferenced) != 0) {
            moduleUnit.addStaticDependency(className)
          }
        }

        /* Since many of the lists below are likely to be empty, we always
         * test `!list.isEmpty` before calling `foreach` or any other
         * processing, avoiding closure allocations.
         */

        if (!dataInClass.fieldsRead.isEmpty) {
          clazz.readFields(dataInClass.fieldsRead)
        }

        if (!dataInClass.fieldsWritten.isEmpty) {
          clazz.writeFields(dataInClass.fieldsWritten)
        }

        if (!dataInClass.staticFieldsRead.isEmpty) {
          moduleUnit.addStaticDependency(className)
          dataInClass.staticFieldsRead.foreach(
              clazz._staticFieldsRead.update(_, ()))
        }

        if (!dataInClass.staticFieldsWritten.isEmpty) {
          moduleUnit.addStaticDependency(className)
          dataInClass.staticFieldsWritten.foreach(
              clazz._staticFieldsWritten.update(_, ()))
        }

        if (!dataInClass.methodsCalled.isEmpty) {
          // Do not add to staticDependencies: We call these on the object.
          for (methodName <- dataInClass.methodsCalled)
            clazz.callMethod(methodName)
        }

        if (!dataInClass.methodsCalledStatically.isEmpty) {
          moduleUnit.addStaticDependency(className)
          for (methodName <- dataInClass.methodsCalledStatically)
            clazz.callMethodStatically(methodName)
        }

        if (!dataInClass.methodsCalledDynamicImport.isEmpty) {
          if (isNoModule) {
            _errors ::= DynamicImportWithoutModuleSupport(from)
          } else {
            moduleUnit.addDynamicDependency(className)
            // In terms of reachability, a dynamic import call is just a static call.
            for (methodName <- dataInClass.methodsCalledDynamicImport)
              clazz.callMethodStatically(methodName)
          }
        }

        if (!dataInClass.jsNativeMembersUsed.isEmpty) {
          for (member <- dataInClass.jsNativeMembersUsed)
            clazz.useJSNativeMember(member)
              .foreach(addLoadSpec(moduleUnit, _))
        }
      }
    }

    val globalFlags = data.globalFlags

    if (globalFlags != 0) {
      if ((globalFlags & ReachabilityInfo.FlagAccessedClassClass) != 0) {
        /* java.lang.Class is only ever instantiated in the CoreJSLib.
         * Therefore, make java.lang.Object depend on it instead of the caller itself.
         */
        objectClassInfo.addStaticDependency(ClassClass)
        lookupClass(ClassClass) { clazz =>
          clazz.instantiated()
          clazz.callMethodStatically(MemberNamespace.Constructor, ObjectArgConstructorName)
        }
      }

      if ((globalFlags & ReachabilityInfo.FlagAccessedNewTarget) != 0 &&
          config.coreSpec.esFeatures.esVersion < ESVersion.ES2015) {
        _errors ::= NewTargetWithoutES2015Support(from)
      }

      if ((globalFlags & ReachabilityInfo.FlagAccessedImportMeta) != 0 &&
          config.coreSpec.moduleKind != ModuleKind.ESModule) {
        _errors ::= ImportMetaWithoutESModule(from)
      }

      if ((globalFlags & ReachabilityInfo.FlagUsedExponentOperator) != 0 &&
          config.coreSpec.esFeatures.esVersion < ESVersion.ES2016) {
        _errors ::= ExponentOperatorWithoutES2016Support(from)
      }
    }
  }

  @tailrec
  private def addLoadSpec(moduleUnit: ModuleUnit,
      jsNativeLoadSpec: JSNativeLoadSpec): Unit = {
    jsNativeLoadSpec match {
      case _: JSNativeLoadSpec.Global =>

      case JSNativeLoadSpec.Import(module, _) =>
        moduleUnit.addExternalDependency(module)

      case JSNativeLoadSpec.ImportWithGlobalFallback(importSpec, _) =>
        if (!isNoModule)
          addLoadSpec(moduleUnit, importSpec)
    }
  }

  private def createMissingClassInfo(className: ClassName): Infos.ClassInfo = {
    val superClass =
      if (className == ObjectClass) None
      else Some(ObjectClass)

    new Infos.ClassInfoBuilder(className, ClassKind.Class,
        superClass = superClass, interfaces = Nil, jsNativeLoadSpec = None)
      .addMethod(makeSyntheticMethodInfo(NoArgConstructorName, MemberNamespace.Constructor))
      .result()
  }

  private def makeSyntheticMethodInfo(
      methodName: MethodName,
      namespace: MemberNamespace,
      methodsCalled: List[(ClassName, MethodName)] = Nil,
      methodsCalledStatically: List[(ClassName, NamespacedMethodName)] = Nil,
      instantiatedClasses: List[ClassName] = Nil
  ): Infos.MethodInfo = {
    val reachabilityInfoBuilder = new Infos.ReachabilityInfoBuilder()
    for ((className, methodName) <- methodsCalled)
      reachabilityInfoBuilder.addMethodCalled(className, methodName)
    for ((className, methodName) <- methodsCalledStatically)
      reachabilityInfoBuilder.addMethodCalledStatically(className, methodName)
    Infos.MethodInfo(methodName, namespace, isAbstract = false,
        reachabilityInfoBuilder.result())
  }

}

private object AnalyzerRun {
  private val getSuperclassMethodName =
    MethodName("getSuperclass", Nil, ClassRef(ClassClass))

  private class WorkTracker(implicit ec: ExecutionContext) {
    /** The number of tasks that have started but not completed, `+ 1` until
     *  `allowComplete()` gets called.
     */
    private val pending = new AtomicInteger(1)
    private val failures = new AtomicReference[List[Throwable]](Nil)
    private val promise = Promise[Unit]()

    def track(fut: Future[Unit]): Unit = {
      pending.incrementAndGet()

      fut.onComplete { result =>
        result match {
          case Success(_) => ()
          case Failure(t) => addFailure(t)
        }
        decrementPending()
      }
    }

    @tailrec
    private def addFailure(t: Throwable): Unit = {
      val prev = failures.get()
      if (!failures.compareAndSet(prev, t :: prev))
        addFailure(t)
    }

    private def decrementPending(): Unit = {
      /* When `pending` reaches 0, we are sure that all started tasks have
       * completed, and that `allowComplete()` was called. Therefore, no
       * further task can be concurrently added, and we are done.
       */
      if (pending.decrementAndGet() == 0)
        complete()
    }

    private def complete(): Unit = {
      failures.get() match {
        case Nil =>
          promise.success(())
        case firstFailure :: moreFailures =>
          for (t <- moreFailures)
            firstFailure.addSuppressed(t)
          promise.failure(firstFailure)
      }
    }

    /** Signals that no new top-level tasks will be started, and that it is
     *  therefore OK to complete the tracker once all ongoing tasks have finished.
     *
     *  `allowComplete()` must not be called more than once.
     */
    def allowComplete(): Future[Unit] = {
      decrementPending()
      promise.future
    }
  }

  private final class GrowingList[A] {
    private val list = new AtomicReference[List[A]](Nil)
    def ::=(item: A): Unit = list.updateAndGet(item :: _)
    def get(): List[A] = list.get()
    def addIfNil(item: A): Boolean = list.compareAndSet(Nil, item :: Nil)
    def clear(): Unit = list.set(Nil)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy