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

dotty.tools.dotc.sbt.ExtractAPI.scala Maven / Gradle / Ivy

There is a newer version: 3.6.4-RC1-bin-20241220-0bfa1af-NIGHTLY
Show newest version
package dotty.tools.dotc
package sbt

import scala.language.unsafeNulls

import ExtractDependencies.internalError
import ast.{Positioned, Trees, tpd}
import core.*
import core.Decorators.*
import Annotations.*
import Contexts.*
import Flags.*
import Phases.*
import Trees.*
import Types.*
import Symbols.*
import Names.*
import StdNames.str
import NameOps.*
import inlines.Inlines
import transform.ValueClasses
import transform.Pickler
import dotty.tools.io.{File, FileExtension, JarArchive}
import util.{Property, SourceFile}
import java.io.PrintWriter

import ExtractAPI.NonLocalClassSymbolsInCurrentUnits

import scala.collection.mutable
import scala.util.hashing.MurmurHash3
import scala.util.chaining.*

/** This phase sends a representation of the API of classes to sbt via callbacks.
 *
 *  This is used by sbt for incremental recompilation.
 *
 *  See the documentation of `ExtractAPICollector`, `ExtractDependencies`,
 *  `ExtractDependenciesCollector` and
 *  http://www.scala-sbt.org/1.x/docs/Understanding-Recompilation.html for more
 *  information on incremental recompilation.
 *
 *  The following flags affect this phase:
 *   -Yforce-sbt-phases
 *   -Ydump-sbt-inc
 *
 *  @see ExtractDependencies
 */
class ExtractAPI extends Phase {

  override def phaseName: String = ExtractAPI.name

  override def description: String = ExtractAPI.description

  override def isRunnable(using Context): Boolean = {
    super.isRunnable && (ctx.runZincPhases || ctx.settings.XjavaTasty.value)
  }

  // Check no needed. Does not transform trees
  override def isCheckable: Boolean = false

  // when `-Xjava-tasty` is set we actually want to run this phase on Java sources
  override def skipIfJava(using Context): Boolean = false

  // SuperAccessors need to be part of the API (see the scripted test
  // `trait-super` for an example where this matters), this is only the case
  // after `PostTyper` (unlike `ExtractDependencies`, the simplication to trees
  // done by `PostTyper` do not affect this phase because it only cares about
  // definitions, and `PostTyper` does not change definitions).
  override def runsAfter: Set[String] = Set(transform.Pickler.name)

  override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] =
    val doZincCallback = ctx.runZincPhases
    val nonLocalClassSymbols = new mutable.HashSet[Symbol]
    val units0 =
      if doZincCallback then
        val ctx0 = ctx.withProperty(NonLocalClassSymbolsInCurrentUnits, Some(nonLocalClassSymbols))
        super.runOn(units)(using ctx0)
      else
        units // still run the phase for the side effects (writing TASTy files to -Yearly-tasty-output)
    if doZincCallback then
      ctx.withIncCallback(recordNonLocalClasses(nonLocalClassSymbols, _))
    if ctx.settings.XjavaTasty.value then
      units0.filterNot(_.typedAsJava) // remove java sources, this is the terminal phase when `-Xjava-tasty` is set
    else
      units0
  end runOn

  private def recordNonLocalClasses(nonLocalClassSymbols: mutable.HashSet[Symbol], cb: interfaces.IncrementalCallback)(using Context): Unit =
    for cls <- nonLocalClassSymbols do
      val sourceFile = cls.source
      if sourceFile.exists && cls.isDefinedInCurrentRun then
        recordNonLocalClass(cls, sourceFile, cb)
    ctx.run.nn.asyncTasty.foreach(_.signalAPIComplete())

  private def recordNonLocalClass(cls: Symbol, sourceFile: SourceFile, cb: interfaces.IncrementalCallback)(using Context): Unit =
    def registerProductNames(fullClassName: String, binaryClassName: String) =
      val pathToClassFile = s"${binaryClassName.replace('.', java.io.File.separatorChar)}.class"

      val classFile = {
        ctx.settings.outputDir.value match {
          case jar: JarArchive =>
            // important detail here, even on Windows, Zinc expects the separator within the jar
            // to be the system default, (even if in the actual jar file the entry always uses '/').
            // see https://github.com/sbt/zinc/blob/dcddc1f9cfe542d738582c43f4840e17c053ce81/internal/compiler-bridge/src/main/scala/xsbt/JarUtils.scala#L47
            new java.io.File(s"$jar!$pathToClassFile")
          case outputDir =>
            new java.io.File(outputDir.file, pathToClassFile)
        }
      }

      cb.generatedNonLocalClass(sourceFile, classFile.toPath(), binaryClassName, fullClassName)
    end registerProductNames

    val fullClassName = atPhase(sbtExtractDependenciesPhase) {
      ExtractDependencies.classNameAsString(cls)
    }
    val binaryClassName = cls.binaryClassName
    registerProductNames(fullClassName, binaryClassName)

    // Register the names of top-level module symbols that emit two class files
    val isTopLevelUniqueModule =
      cls.owner.is(PackageClass) && cls.is(ModuleClass) && cls.companionClass == NoSymbol
    if isTopLevelUniqueModule then
      registerProductNames(fullClassName, binaryClassName.stripSuffix(str.MODULE_SUFFIX))
  end recordNonLocalClass

  override def run(using Context): Unit = {
    val unit = ctx.compilationUnit
    val sourceFile = unit.source
    ctx.withIncCallback: cb =>
      cb.startSource(sourceFile)

    val nonLocalClassSymbols = ctx.property(NonLocalClassSymbolsInCurrentUnits).get
    val apiTraverser = ExtractAPICollector(nonLocalClassSymbols)
    val classes = apiTraverser.apiSource(unit.tpdTree)
    val mainClasses = apiTraverser.mainClasses

    if (ctx.settings.YdumpSbtInc.value) {
      // Append to existing file that should have been created by ExtractDependencies
      val pw = new PrintWriter(File(sourceFile.file.jpath).changeExtension(FileExtension.Inc).toFile
        .bufferedWriter(append = true), true)
      try {
        classes.foreach(source => pw.println(DefaultShowAPI(source)))
      } finally pw.close()
    }

    ctx.withIncCallback: cb =>
      if !ctx.compilationUnit.suspendedAtInliningPhase then // already registered before this unit was suspended
        classes.foreach(cb.api(sourceFile, _))
        mainClasses.foreach(cb.mainClass(sourceFile, _))
  }
}

object ExtractAPI:
  val name: String = "sbt-api"
  val description: String = "sends a representation of the API of classes to sbt"

  private val NonLocalClassSymbolsInCurrentUnits: Property.Key[mutable.HashSet[Symbol]] = Property.Key()

/** Extracts full (including private members) API representation out of Symbols and Types.
 *
 *  The exact representation used for each type is not important: the only thing
 *  that matters is that a binary-incompatible or source-incompatible change to
 *  the API (for example, changing the signature of a method, or adding a parent
 *  to a class) should result in a change to the API representation so that sbt
 *  can recompile files that depend on this API.
 *
 *  Note that we only records types as they are defined and never "as seen from"
 *  some other prefix because `Types#asSeenFrom` is a complex operation and
 *  doing it for every inherited member would be slow, and because the number
 *  of prefixes can be enormous in some cases:
 *
 *    class Outer {
 *      type T <: S
 *      type S
 *      class A extends Outer { /*...*/ }
 *      class B extends Outer { /*...*/ }
 *      class C extends Outer { /*...*/ }
 *      class D extends Outer { /*...*/ }
 *      class E extends Outer { /*...*/ }
 *    }
 *
 *  `S` might be refined in an arbitrary way inside `A` for example, this
 *  affects the type of `T` as seen from `Outer#A`, so we could record that, but
 *  the class `A` also contains itself as a member, so `Outer#A#A#A#...` is a
 *  valid prefix for `T`. Even if we avoid loops, we still have a combinatorial
 *  explosion of possible prefixes, like `Outer#A#B#C#D#E`.
 *
 *  It is much simpler to record `T` once where it is defined, but that means
 *  that the API representation of `T` may not change even though `T` as seen
 *  from some prefix has changed. This is why in `ExtractDependencies` we need
 *  to traverse used types to not miss dependencies, see the documentation of
 *  `ExtractDependencies#usedTypeTraverser`.
 *
 *  TODO: sbt does not store the full representation that we compute, instead it
 *  hashes parts of it to reduce memory usage, then to see if something changed,
 *  it compares the hashes instead of comparing the representations. We should
 *  investigate whether we can just directly compute hashes in this phase
 *  without going through an intermediate representation, see
 *  http://www.scala-sbt.org/0.13/docs/Understanding-Recompilation.html#Hashing+an+API+representation
 */
private class ExtractAPICollector(nonLocalClassSymbols: mutable.HashSet[Symbol])(using Context) extends ThunkHolder {
  import tpd.*
  import xsbti.api

  /** This cache is necessary for correctness, see the comment about inherited
   *  members in `apiClassStructure`
   */
  private val classLikeCache = new mutable.HashMap[ClassSymbol, api.ClassLikeDef]
  /** This cache is optional, it avoids recomputing representations */
  private val typeCache = new mutable.HashMap[Type, api.Type]
  /** This cache is necessary to avoid unstable name hashing when `typeCache` is present,
   *  see the comment in the `RefinedType` case in `computeType`
   *  The cache key is (api of RefinedType#parent, api of RefinedType#refinedInfo).
   */
  private val refinedTypeCache = new mutable.HashMap[(api.Type, api.Definition), api.Structure]

  /** This cache is necessary to avoid infinite loops when hashing an inline "Body" annotation.
   *  Its values are transitively seen inline references within a call chain starting from a single "origin" inline
   *  definition. Avoid hashing an inline "Body" annotation if its associated definition is already in the cache.
   *  Precondition: the cache is empty whenever we hash a new "origin" inline "Body" annotation.
   */
  private val seenInlineCache = mutable.HashSet.empty[Symbol]

  /** This cache is optional, it avoids recomputing hashes of inline "Body" annotations,
   *  e.g. when a concrete inline method is inherited by a subclass.
   */
  private val inlineBodyCache = mutable.HashMap.empty[Symbol, Int]

  private val allNonLocalClassesInSrc = new mutable.HashSet[xsbti.api.ClassLike]
  private val _mainClasses = new mutable.HashSet[String]

  private object Constants {
    val emptyStringArray = Array[String]()
    val local            = api.ThisQualifier.create()
    val public           = api.Public.create()
    val privateLocal     = api.Private.create(local)
    val protectedLocal   = api.Protected.create(local)
    val unqualified      = api.Unqualified.create()
    val thisPath         = api.This.create()
    val emptyType        = api.EmptyType.create()
    val emptyModifiers   =
      new api.Modifiers(false, false, false, false, false,false, false, false)
  }

  /** Some Dotty types do not have a corresponding type in xsbti.api.* that
   *  represents them. Until this is fixed we can workaround this by using
   *  special annotations that can never appear in the source code to
   *  represent these types.
   *
   *  @param tp      An approximation of the type we're trying to represent
   *  @param marker  A special annotation to differentiate our type
   */
  private def withMarker(tp: api.Type, marker: api.Annotation) =
    api.Annotated.of(tp, Array(marker))
  private def marker(name: String) =
    api.Annotation.of(api.Constant.of(Constants.emptyType, name), Array())
  private val orMarker = marker("Or")
  private val byNameMarker = marker("ByName")
  private val matchMarker = marker("Match")
  private val superMarker = marker("Super")

  /** Extract the API representation of a source file */
  def apiSource(tree: Tree): Seq[api.ClassLike] = {
    def apiClasses(tree: Tree): Unit = tree match {
      case PackageDef(_, stats) =>
        stats.foreach(apiClasses)
      case tree: TypeDef =>
        apiClass(tree.symbol.asClass)
      case _ =>
    }

    apiClasses(tree)
    forceThunks()

    allNonLocalClassesInSrc.toSeq
  }

  def apiClass(sym: ClassSymbol): api.ClassLikeDef =
    classLikeCache.getOrElseUpdate(sym, computeClass(sym))

  def mainClasses: Set[String] = {
    forceThunks()
    _mainClasses.toSet
  }

  private def computeClass(sym: ClassSymbol): api.ClassLikeDef = {
    import xsbti.api.{DefinitionType => dt}
    val defType =
      if (sym.is(Trait)) dt.Trait
      else if (sym.is(ModuleClass)) {
        if (sym.is(PackageClass)) dt.PackageModule
        else dt.Module
      } else dt.ClassDef

    val selfType = apiType(sym.givenSelfType)

    val name = ExtractDependencies.classNameAsString(sym)
      // We strip module class suffix. Zinc relies on a class and its companion having the same name

    val tparams = sym.typeParams.map(apiTypeParameter).toArray

    val structure = apiClassStructure(sym)
    val acc = apiAccess(sym)
    val modifiers = apiModifiers(sym)
    val anns = apiAnnotations(sym, inlineOrigin = NoSymbol).toArray
    val topLevel = sym.isTopLevelClass
    val childrenOfSealedClass = sym.sealedDescendants.sorted(classFirstSort).map(c =>
      if (c.isClass)
        apiType(c.typeRef)
      else
        apiType(c.termRef)
    ).toArray

    val cl = api.ClassLike.of(
      name, acc, modifiers, anns, defType, api.SafeLazy.strict(selfType), api.SafeLazy.strict(structure), Constants.emptyStringArray,
      childrenOfSealedClass, topLevel, tparams)

    allNonLocalClassesInSrc += cl
    if !sym.isLocal then
      nonLocalClassSymbols += sym

    if (sym.isStatic && !sym.is(Trait) && ctx.platform.hasMainMethod(sym)) {
       // If sym is an object, all main methods count, otherwise only @static ones count.
      _mainClasses += name
    }

    api.ClassLikeDef.of(name, acc, modifiers, anns, tparams, defType)
  }

  def apiClassStructure(csym: ClassSymbol): api.Structure = {
    val cinfo = csym.classInfo

    val bases = {
      val ancestorTypes0 =
        try linearizedAncestorTypes(cinfo)
        catch {
          case ex: TypeError =>
            // See neg/i1750a for an example where a cyclic error can arise.
            // The root cause in this example is an illegal "override" of an inner trait
            report.error(ex, csym.sourcePos)
            defn.ObjectType :: Nil
        }
      if (csym.isDerivedValueClass) {
        val underlying = ValueClasses.valueClassUnbox(csym).info.finalResultType
        // The underlying type of a value class should be part of the name hash
        // of the value class (see the test `value-class-underlying`), this is accomplished
        // by adding the underlying type to the list of parent types.
        underlying :: ancestorTypes0
      } else
        ancestorTypes0
    }

    val apiBases = bases.map(apiType)

    // Synthetic methods that are always present do not affect the API
    // and can therefore be ignored.
    def alwaysPresent(s: Symbol) = csym.is(ModuleClass) && s.isConstructor
    val decls = cinfo.decls.filter(!alwaysPresent(_))
    val apiDecls = apiDefinitions(decls)

    val declSet = decls.toSet
    // TODO: We shouldn't have to compute inherited members. Instead, `Structure`
    // should have a lazy `parentStructures` field.
    val inherited = cinfo.baseClasses
      .filter(bc => !bc.is(Scala2x))
      .flatMap(_.classInfo.decls.filter(s => !(s.is(Private) || declSet.contains(s))))
    // Inherited members need to be computed lazily because a class might contain
    // itself as an inherited member, like in `class A { class B extends A }`,
    // this works because of `classLikeCache`
    val apiInherited = lzy(apiDefinitions(inherited).toArray)

    api.Structure.of(api.SafeLazy.strict(apiBases.toArray), api.SafeLazy.strict(apiDecls.toArray), apiInherited)
  }

  def linearizedAncestorTypes(info: ClassInfo): List[Type] = {
    val ref = info.appliedRef
    // Note that the ordering of classes in `baseClasses` is important.
    info.baseClasses.tail.map(ref.baseType)
  }

  // The hash generated by sbt for definitions is supposed to be symmetric so
  // we shouldn't have to sort them, but it actually isn't symmetric for
  // definitions which are classes, therefore we need to sort classes to
  // ensure a stable hash.
  // Modules and classes come first and are sorted by name, all other
  // definitions come later and are not sorted.
  private object classFirstSort extends Ordering[Symbol] {
    override def compare(a: Symbol, b: Symbol) = {
      val aIsClass = a.isClass
      val bIsClass = b.isClass
      if (aIsClass == bIsClass) {
        if (aIsClass) {
          if (a.is(Module) == b.is(Module))
            a.fullName.toString.compareTo(b.fullName.toString)
          else if (a.is(Module))
            -1
          else
            1
        } else
          0
      } else if (aIsClass)
      -1
    else
      1
    }
  }

  def apiDefinitions(defs: List[Symbol]): List[api.ClassDefinition] =
    defs.sorted(classFirstSort).map(apiDefinition(_, inlineOrigin = NoSymbol))

  /** `inlineOrigin` denotes an optional inline method that we are
   *  currently hashing the body of. If it exists, include extra information
   *  that is missing after erasure
   */
  def apiDefinition(sym: Symbol, inlineOrigin: Symbol): api.ClassDefinition = {
    if (sym.isClass) {
      apiClass(sym.asClass)
    } else if (sym.isType) {
      apiTypeMember(sym.asType)
    } else if (sym.is(Mutable, butNot = Accessor)) {
      api.Var.of(sym.name.toString, apiAccess(sym), apiModifiers(sym),
        apiAnnotations(sym, inlineOrigin).toArray, apiType(sym.info))
    } else if (sym.isStableMember && !sym.isRealMethod) {
      api.Val.of(sym.name.toString, apiAccess(sym), apiModifiers(sym),
        apiAnnotations(sym, inlineOrigin).toArray, apiType(sym.info))
    } else {
      apiDef(sym.asTerm, inlineOrigin)
    }
  }

  /** `inlineOrigin` denotes an optional inline method that we are
   *  currently hashing the body of. If it exists, include extra information
   *  that is missing after erasure
   */
  def apiDef(sym: TermSymbol, inlineOrigin: Symbol): api.Def = {

    var seenInlineExtras = false
    var inlineExtras = 41

    def mixInlineParam(p: Symbol): Unit =
      if inlineOrigin.exists && p.is(Inline) then
        seenInlineExtras = true
        inlineExtras = hashInlineParam(p, inlineExtras)

    def inlineExtrasAnnot: Option[api.Annotation] =
      val h = inlineExtras
      Option.when(seenInlineExtras) {
        marker(s"${MurmurHash3.finalizeHash(h, "inlineExtras".hashCode)}")
      }

    def tparamList(pt: TypeLambda): List[api.TypeParameter] =
      pt.paramNames.lazyZip(pt.paramInfos).map((pname, pbounds) =>
        apiTypeParameter(pname.toString, 0, pbounds.lo, pbounds.hi)
      )

    def paramList(mt: MethodType, params: List[Symbol]): api.ParameterList =
      val apiParams = params.lazyZip(mt.paramInfos).map((param, ptype) =>
        mixInlineParam(param)
        api.MethodParameter.of(
          param.name.toString, apiType(ptype), param.is(HasDefault), api.ParameterModifier.Plain))
      api.ParameterList.of(apiParams.toArray, mt.isImplicitMethod)

    def paramLists(t: Type, paramss: List[List[Symbol]]): List[api.ParameterList] = t match {
      case pt: TypeLambda =>
        paramLists(pt.resultType, paramss.drop(1))
      case mt @ MethodTpe(pnames, ptypes, restpe) =>
        assert(paramss.nonEmpty && paramss.head.hasSameLengthAs(pnames),
          i"mismatch for $sym, ${sym.info}, ${sym.paramSymss}")
        paramList(mt, paramss.head) :: paramLists(restpe, paramss.tail)
      case _ =>
        Nil
    }

    /** returns list of pairs of 1: the position in all parameter lists, and 2: a type parameter list */
    def tparamLists(t: Type, index: Int): List[(Int, List[api.TypeParameter])] = t match
      case pt: TypeLambda =>
        (index, tparamList(pt)) :: tparamLists(pt.resultType, index + 1)
      case mt: MethodType =>
        tparamLists(mt.resultType, index + 1)
      case _ =>
        Nil

    val (tparams, tparamsExtras) = sym.info match
      case pt: TypeLambda =>
        (tparamList(pt), tparamLists(pt.resultType, index = 1))
      case mt: MethodType =>
        (Nil, tparamLists(mt.resultType, index = 1))
      case _ =>
        (Nil, Nil)

    val vparamss = paramLists(sym.info, sym.paramSymss)
    val retTp = sym.info.finalResultType.widenExpr

    val tparamsExtraAnnot = Option.when(tparamsExtras.nonEmpty) {
      marker(s"${hashTparamsExtras(tparamsExtras)("tparamsExtra".hashCode)}")
    }

    val annotations = inlineExtrasAnnot ++: tparamsExtraAnnot ++: apiAnnotations(sym, inlineOrigin)

    api.Def.of(sym.zincMangledName.toString, apiAccess(sym), apiModifiers(sym),
      annotations.toArray, tparams.toArray, vparamss.toArray, apiType(retTp))
  }

  def apiTypeMember(sym: TypeSymbol): api.TypeMember = {
    val typeParams = Array[api.TypeParameter]()
    val name = sym.name.toString
    val access = apiAccess(sym)
    val modifiers = apiModifiers(sym)
    val as = apiAnnotations(sym, inlineOrigin = NoSymbol)
    val tpe = sym.info

    if (sym.isAliasType)
      api.TypeAlias.of(name, access, modifiers, as.toArray, typeParams, apiType(tpe.bounds.hi))
    else {
      assert(sym.isAbstractOrParamType)
      api.TypeDeclaration.of(name, access, modifiers, as.toArray, typeParams, apiType(tpe.bounds.lo), apiType(tpe.bounds.hi))
    }
  }

  // Hack to represent dotty types which don't have an equivalent in xsbti
  def combineApiTypes(apiTps: api.Type*): api.Type = {
    api.Structure.of(api.SafeLazy.strict(apiTps.toArray),
      api.SafeLazy.strict(Array()), api.SafeLazy.strict(Array()))
  }

  def apiType(tp: Type): api.Type = {
    typeCache.getOrElseUpdate(tp, computeType(tp))
  }

  private def computeType(tp: Type): api.Type = {
    // TODO: Never dealias. We currently have to dealias because
    // sbt main class discovery relies on the signature of the main
    // method being fully dealiased. See https://github.com/sbt/zinc/issues/102
    val tp2 = if (!tp.isLambdaSub) tp.dealiasKeepAnnots else tp
    tp2 match {
      case NoPrefix | NoType =>
        Constants.emptyType
      case tp: NamedType =>
        val sym = tp.symbol
        // A type can sometimes be represented by multiple different NamedTypes
        // (they will be `=:=` to each other, but not `==`), and the compiler
        // may choose to use any of these representation, there is no stability
        // guarantee. We avoid this instability by always normalizing the
        // prefix: if it's a package, if we didn't do this sbt might conclude
        // that some API changed when it didn't, leading to overcompilation
        // (recompiling more things than what is needed for incremental
        // compilation to be correct).
        val prefix = if (sym.maybeOwner.is(Package)) // { type T } here T does not have an owner
          sym.owner.thisType
        else
          tp.prefix
        api.Projection.of(apiType(prefix), sym.name.toString)
      case AppliedType(tycon, args) =>
        def processArg(arg: Type): api.Type = arg match {
          case arg @ TypeBounds(lo, hi) => // Handle wildcard parameters
            if (lo.isDirectRef(defn.NothingClass) && hi.isDirectRef(defn.AnyClass))
              Constants.emptyType
            else {
              val name = "_"
              val ref = api.ParameterRef.of(name)
              api.Existential.of(ref,
                Array(apiTypeParameter(name, 0, lo, hi)))
            }
          case _ =>
            apiType(arg)
        }

        val apiTycon = apiType(tycon)
        val apiArgs = args.map(processArg)
        api.Parameterized.of(apiTycon, apiArgs.toArray)
      case tl: TypeLambda =>
        val apiTparams = tl.typeParams.map(apiTypeParameter)
        val apiRes = apiType(tl.resType)
        api.Polymorphic.of(apiRes, apiTparams.toArray)
      case rt: RefinedType =>
        val name = rt.refinedName.toString
        val parent = apiType(rt.parent)

        def typeRefinement(name: String, tp: TypeBounds): api.TypeMember = tp match {
          case TypeAlias(alias) =>
            api.TypeAlias.of(name,
              Constants.public, Constants.emptyModifiers, Array(), Array(), apiType(alias))
          case TypeBounds(lo, hi) =>
            api.TypeDeclaration.of(name,
              Constants.public, Constants.emptyModifiers, Array(), Array(), apiType(lo), apiType(hi))
        }
        val decl = rt.refinedInfo match {
          case rinfo: TypeBounds =>
            typeRefinement(name, rinfo)
          case _ =>
            report.debuglog(i"sbt-api: skipped structural refinement in $rt")
            null
        }

        // Aggressive caching for RefinedTypes: `typeCache` is enough as long as two
        // RefinedType are `==`, but this is only the case when their `refinedInfo`
        // are `==` and this is not always the case, consider:
        //
        //     val foo: { type Bla = a.b.T }
        //     val bar: { type Bla = a.b.T }
        //
        // The sbt API representations of `foo` and `bar` (let's call them `apiFoo`
        // and `apiBar`) will both be instances of `Structure`. If `typeCache` was
        // the only cache, then in some cases we would have `apiFoo eq apiBar` and
        // in other cases we would just have `apiFoo == apiBar` (this happens
        // because the dotty representation of `a.b.T` is unstable, see the comment
        // in the `NamedType` case above).
        //
        // The fact that we may or may not have `apiFoo eq apiBar` is more than
        // an optimisation issue: it will determine whether the sbt name hash for
        // `Bla` contains one or two entries (because sbt `NameHashing` will not
        // traverse both `apiFoo` and `apiBar` if they are `eq`), therefore the
        // name hash of `Bla` will be unstable, unless we make sure that
        // `apiFoo == apiBar` always imply `apiFoo eq apiBar`. This is what
        // `refinedTypeCache` is for.
        refinedTypeCache.getOrElseUpdate((parent, decl), {
          val adecl: Array[api.ClassDefinition] = if (decl == null) Array() else Array(decl)
          api.Structure.of(api.SafeLazy.strict(Array(parent)), api.SafeLazy.strict(adecl), api.SafeLazy.strict(Array()))
        })
      case tp: RecType =>
        apiType(tp.parent)
      case RecThis(recType) =>
        // `tp` must be present inside `recType`, so calling `apiType` on
        // `recType` would lead to an infinite recursion, we avoid this by
        //  computing the representation of `recType` lazily.
        apiLazy(recType)
      case tp: AndType =>
        combineApiTypes(apiType(tp.tp1), apiType(tp.tp2))
      case tp: OrType =>
        val s = combineApiTypes(apiType(tp.tp1), apiType(tp.tp2))
        withMarker(s, orMarker)
      case tp: FlexibleType =>
        apiType(tp.underlying)
      case ExprType(resultType) =>
        withMarker(apiType(resultType), byNameMarker)
      case MatchType(bound, scrut, cases) =>
        val s = combineApiTypes(apiType(bound) :: apiType(scrut) :: cases.map(apiType)*)
        withMarker(s, matchMarker)
      case ConstantType(constant) =>
        api.Constant.of(apiType(constant.tpe), constant.stringValue)
      case AnnotatedType(tpe, annot) =>
        api.Annotated.of(apiType(tpe), Array(apiAnnotation(annot)))
      case tp: ThisType =>
        apiThis(tp.cls)
      case tp: ParamRef =>
        // TODO: Distinguishing parameters based on their names alone is not enough,
        // the binder is also needed (at least for type lambdas).
        api.ParameterRef.of(tp.paramName.toString)
      case tp: LazyRef =>
        apiType(tp.ref)
      case tp: TypeVar =>
        apiType(tp.underlying)
      case SuperType(thistpe, supertpe) =>
        val s = combineApiTypes(apiType(thistpe), apiType(supertpe))
        withMarker(s, superMarker)
      case _ => {
        internalError(i"Unhandled type $tp of class ${tp.getClass}")
        Constants.emptyType
      }
    }
  }

  def apiLazy(tp: => Type): api.Type = {
    // TODO: The sbt api needs a convenient way to make a lazy type.
    // For now, we repurpose Structure for this.
    val apiTp = lzy(Array(apiType(tp)))
    api.Structure.of(apiTp, api.SafeLazy.strict(Array()), api.SafeLazy.strict(Array()))
  }

  def apiThis(sym: Symbol): api.Singleton = {
    val pathComponents = sym.ownersIterator.takeWhile(!_.isEffectiveRoot)
      .map(s => api.Id.of(s.name.toString))
    api.Singleton.of(api.Path.of(pathComponents.toArray.reverse ++ Array(Constants.thisPath)))
  }

  def apiTypeParameter(tparam: ParamInfo): api.TypeParameter =
    apiTypeParameter(tparam.paramName.toString, tparam.paramVarianceSign,
      tparam.paramInfo.bounds.lo, tparam.paramInfo.bounds.hi)

  def apiTypeParameter(name: String, variance: Int, lo: Type, hi: Type): api.TypeParameter =
    api.TypeParameter.of(name, Array(), Array(), apiVariance(variance),
      apiType(lo), apiType(hi))

  def apiVariance(v: Int): api.Variance = {
    import api.Variance.*
    if (v < 0) Contravariant
    else if (v > 0) Covariant
    else Invariant
  }

  def apiAccess(sym: Symbol): api.Access = {
    // Symbols which are private[foo] do not have the flag Private set,
    // but their `privateWithin` exists, see `Parsers#ParserCommon#normalize`.
    if (!sym.isOneOf(Protected | Private) && !sym.privateWithin.exists)
      Constants.public
    else if (sym.isAllOf(PrivateLocal))
      Constants.privateLocal
    else if (sym.isAllOf(ProtectedLocal))
      Constants.protectedLocal
    else {
      val qualifier =
        if (sym.privateWithin eq NoSymbol)
          Constants.unqualified
        else
          api.IdQualifier.of(sym.privateWithin.fullName.toString)
      if (sym.is(Protected))
        api.Protected.of(qualifier)
      else
        api.Private.of(qualifier)
    }
  }

  def apiModifiers(sym: Symbol): api.Modifiers = {
    val absOver = sym.is(AbsOverride)
    val abs = absOver || sym.isOneOf(Trait | Abstract | Deferred)
    val over = absOver || sym.is(Override)
    new api.Modifiers(abs, over, sym.is(Final), sym.is(Sealed),
      sym.isOneOf(GivenOrImplicit), sym.is(Lazy), sym.is(Macro), sym.isSuperAccessor)
  }

  /** `inlineOrigin` denotes an optional inline method that we are
   *  currently hashing the body of.
   */
  def apiAnnotations(s: Symbol, inlineOrigin: Symbol): List[api.Annotation] = {
    val annots = new mutable.ListBuffer[api.Annotation]
    val inlineBody = Inlines.bodyToInline(s)
    if !inlineBody.isEmpty then
      // If the body of an inline def changes, all the reverse dependencies of
      // this method need to be recompiled. sbt has no way of tracking method
      // bodies, so we include the hash of the body of the method as part of the
      // signature we send to sbt.

      def hash[U](inlineOrigin: Symbol): Int =
        assert(seenInlineCache.add(s)) // will fail if already seen, guarded by treeHash
        treeHash(inlineBody, inlineOrigin)

      val inlineHash =
        if inlineOrigin.exists then hash(inlineOrigin)
        else inlineBodyCache.getOrElseUpdate(s, hash(inlineOrigin = s).tap(_ => seenInlineCache.clear()))

      annots += marker(inlineHash.toString)

    end if

    // In the Scala2 ExtractAPI phase we only extract annotations that extend
    // StaticAnnotation, but in Dotty we currently pickle all annotations so we
    // extract everything, except:
    // - annotations missing from the classpath which we simply skip over
    // - inline body annotations which are handled above
    // - the Child annotation since we already extract children via
    //   `api.ClassLike#childrenOfSealedClass` and adding this annotation would
    //   lead to overcompilation when using zinc's
    //   `IncOptions#useOptimizedSealed`.
    s.annotations.foreach { annot =>
      val sym = annot.symbol
      if sym.exists && sym != defn.BodyAnnot && sym != defn.ChildAnnot then
        annots += apiAnnotation(annot)
    }

    annots.toList
  }

  /** Produce a hash for a tree that is as stable as possible:
   *  it should stay the same across compiler runs, compiler instances,
   *  JVMs, etc.
   *
   * `inlineOrigin` denotes an optional inline method that we are hashing the body of, where `tree` could be
   * its body, or the body of another method referenced in a call chain leading to `inlineOrigin`.
   *
   * If `inlineOrigin` is NoSymbol, then tree is the tree of an annotation.
   */
  def treeHash(tree: Tree, inlineOrigin: Symbol): Int =
    import core.Constants.*

    def nameHash(n: Name, initHash: Int): Int =
      val h =
        if n.isTermName then
          MurmurHash3.mix(initHash, TermNameHash)
        else
          MurmurHash3.mix(initHash, TypeNameHash)

      // The hashCode of the name itself is not stable across compiler instances
      MurmurHash3.mix(h, n.toString.hashCode)
    end nameHash

    def constantHash(c: Constant, initHash: Int): Int =
      var h = MurmurHash3.mix(initHash, c.tag)
      c.tag match
        case NullTag =>
          // No value to hash, the tag is enough.
        case ClazzTag =>
          // Go through `apiType` to get a value with a stable hash, it'd
          // be better to use Murmur here too instead of relying on
          // `hashCode`, but that would essentially mean duplicating
          // https://github.com/sbt/zinc/blob/develop/internal/zinc-apiinfo/src/main/scala/xsbt/api/HashAPI.scala
          // and at that point we might as well do type hashing on our own
          // representation.
          h = MurmurHash3.mix(h, apiType(c.typeValue).hashCode)
        case _ =>
          h = MurmurHash3.mix(h, c.value.hashCode)
      h
    end constantHash

    def cannotHash(what: String, elem: Any, pos: Positioned): Unit =
      internalError(i"Don't know how to produce a stable hash for $what", pos.sourcePos)

    def positionedHash(p: ast.Positioned, initHash: Int): Int =
      var h = initHash

      p match
        case p: WithLazyFields => p.forceFields()
        case _ =>

      if inlineOrigin.exists then
        p match
          case ref: RefTree @unchecked =>
            val sym = ref.symbol
            if sym.is(Inline, butNot = Param) && !seenInlineCache.contains(sym) then
              // An inline method that calls another inline method will eventually inline the call
              // at a non-inline callsite, in this case if the implementation of the nested call
              // changes, then the callsite will have a different API, we should hash the definition
              h = MurmurHash3.mix(h, apiDefinition(sym, inlineOrigin).hashCode)
          case _ =>

      // FIXME: If `p` is a tree we should probably take its type into account
      // when hashing it, but producing a stable hash for a type is not trivial
      // since the same type might have multiple representations, for method
      // signatures this is already handled by `computeType` and the machinery
      // in Zinc that generates hashes from that, if we can reliably produce
      // stable hashes for types ourselves then we could bypass all that and
      // send Zinc hashes directly.
      h = MurmurHash3.mix(h, p.productPrefix.hashCode)
      iteratorHash(p.productIterator, h)
    end positionedHash

    def iteratorHash(it: Iterator[Any], initHash: Int): Int =
      var h = initHash
      while it.hasNext do
        it.next() match
          case p: Positioned =>
            h = positionedHash(p, h)
          case xs: List[?] =>
            h = iteratorHash(xs.iterator, h)
          case c: Constant =>
            h = constantHash(c, h)
          case n: Name =>
            h = nameHash(n, h)
          case elem =>
            cannotHash(what = i"`${elem.tryToShow}` of unknown class ${elem.getClass}", elem, tree)
      h
    end iteratorHash

    val seed = 4 // https://xkcd.com/221
    val h = positionedHash(tree, seed)
    MurmurHash3.finalizeHash(h, 0)
  end treeHash

  /** Hash secondary type parameters in separate marker annotation.
   *  We hash them separately because the position of type parameters is important.
   */
  private def hashTparamsExtras(tparamsExtras: List[(Int, List[api.TypeParameter])])(initHash: Int): Int =

    def mixTparams(tparams: List[api.TypeParameter])(initHash: Int) =
      var h = initHash
      var elems = tparams
      while elems.nonEmpty do
        h = MurmurHash3.mix(h, elems.head.hashCode)
        elems = elems.tail
      h

    def mixIndexAndTparams(index: Int, tparams: List[api.TypeParameter])(initHash: Int) =
      mixTparams(tparams)(MurmurHash3.mix(initHash, index))

    var h = initHash
    var extras = tparamsExtras
    var len = 0
    while extras.nonEmpty do
      h = mixIndexAndTparams(index = extras.head(0), tparams = extras.head(1))(h)
      extras = extras.tail
      len += 1
    MurmurHash3.finalizeHash(h, len)
  end hashTparamsExtras

  /** Mix in the name hash also because otherwise switching which
   *  parameter is inline will not affect the hash.
   */
  private def hashInlineParam(p: Symbol, h: Int) =
    MurmurHash3.mix(p.name.toString.hashCode, MurmurHash3.mix(h, InlineParamHash))

  def apiAnnotation(annot: Annotation): api.Annotation = {
    // Like with inline defs, the whole body of the annotation and not just its
    // type is part of its API so we need to store its hash, but Zinc wants us
    // to extract the annotation type and its arguments, so we use a dummy
    // annotation argument to store the hash of the tree. We still need to
    // extract the annotation type in the way Zinc expects because sbt uses this
    // information to find tests to run (for example junit tests are
    // annotated @org.junit.Test).
    api.Annotation.of(
      apiType(annot.tree.tpe), // Used by sbt to find tests to run
      Array(api.AnnotationArgument.of("TREE_HASH", treeHash(annot.tree, inlineOrigin = NoSymbol).toString)))
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy