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

sbt.internal.inc.ClassToAPI.scala Maven / Gradle / Ivy

There is a newer version: 1.10.4
Show newest version
/*
 * Zinc - The incremental compiler for Scala.
 * Copyright Scala Center, Lightbend, and Mark Harrah
 *
 * Licensed under Apache License 2.0
 * SPDX-License-Identifier: Apache-2.0
 *
 * See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 */

package sbt
package internal
package inc

import java.lang.reflect.{ Array => _, _ }
import java.lang.annotation.Annotation
import annotation.tailrec
import inc.classfile.ClassFile
import xsbti.api
import xsbti.api.SafeLazyProxy
import collection.mutable
import sbt.io.IO
import sbt.util.Logger

object ClassToAPI {
  def apply(c: Seq[Class[?]]): Seq[api.ClassLike] = process(c)._1

  // (api, public inherited classes)
  def process(
      classes: Seq[Class[?]]
  ): (Seq[api.ClassLike], Seq[String], Set[(Class[?], Class[?])]) = {
    val cmap = emptyClassMap
    classes.foreach(toDefinitions(cmap)) // force recording of class definitions
    cmap.lz.toList
      .foreach(_.get()) // force thunks to ensure all inherited dependencies are recorded
    val classApis = cmap.allNonLocalClasses.toSeq
    val mainClasses = cmap.mainClasses.toSeq
    val inDeps = cmap.inherited.toSet
    cmap.clear()
    (classApis, mainClasses, inDeps)
  }

  // Avoiding implicit allocation.
  private def arrayMap[T <: AnyRef, U <: AnyRef: reflect.ClassTag](
      xs: Array[T]
  )(f: T => U): Array[U] = {
    val len = xs.length
    var i = 0
    val res = new Array[U](len)
    while (i < len) {
      res(i) = f(xs(i))
      i += 1
    }
    res
  }

  def packages(c: Seq[Class[?]]): Set[String] =
    c.flatMap(packageName).toSet

  def isTopLevel(c: Class[?]): Boolean =
    c.getEnclosingClass eq null

  final class ClassMap private[sbt] (
      private[sbt] val memo: mutable.Map[String, Seq[api.ClassLikeDef]],
      private[sbt] val inherited: mutable.Set[(Class[?], Class[?])],
      private[sbt] val lz: mutable.Buffer[xsbti.api.Lazy[?]],
      private[sbt] val allNonLocalClasses: mutable.Set[api.ClassLike],
      private[sbt] val mainClasses: mutable.Set[String]
  ) {
    def clear(): Unit = {
      memo.clear()
      inherited.clear()
      lz.clear()
    }
  }
  def emptyClassMap: ClassMap =
    new ClassMap(
      new mutable.HashMap,
      new mutable.HashSet,
      new mutable.ListBuffer,
      new mutable.HashSet,
      new mutable.HashSet
    )

  /**
   * Returns the canonical name given a class based on https://docs.oracle.com/javase/specs/jls/se11/html/jls-6.html#jls-6.7
   *
   * 1. A named package returns its package name.
   * 2A. A top-level class returns package name + "." + simple name.
   * 2B. A top-level Scala object returns object's name + "$".
   * 3A. Nested class M of a class C returns C's canonical name + "." + M's simple name.
   * 3B. Nested class M of a top-level Scala object O returns O's name + "." + M's simple name.
   * 3C. Nested class M of a non-top-level Scala object O returns's O's canonical name + "." + M's simple name.
   *
   * For example OOO (object in object in object) returns `p1.O1.O2$.O3$`.
   * @return The canonical name if not null, the blank string otherwise.
   */
  def classCanonicalName(c: Class[?]): String = {
    def handleMalformedNameOf(c: Class[?]): String = {
      if (c == null) "" // Return nothing if it hits the top-level class
      else {
        val className = c.getName
        try {
          val canonicalName = c.getCanonicalName
          if (canonicalName == null) className
          else canonicalName
        } catch {
          case malformedError: java.lang.InternalError
              if malformedError.getMessage.contains("Malformed class name") =>
            val enclosingClass = c.getEnclosingClass
            val enclosingName = enclosingClass.getName
            val restOfName = c.getName.stripPrefix(enclosingName)
            // https://docs.oracle.com/javase/specs/jls/se11/html/jls-6.html#jls-6.7
            // A member class or member interface M declared in another class or interface C has a canonical name if and only if C has a canonical name.
            // In that case, the canonical name of M consists of the canonical name of C, followed by ".", followed by the simple name of M.
            handleMalformedNameOf(enclosingClass) + "." + restOfName
        }
      }
    }
    handleMalformedNameOf(c)
  }

  def toDefinitions(cmap: ClassMap)(c: Class[?]): Seq[api.ClassLikeDef] =
    cmap.memo.getOrElseUpdate(classCanonicalName(c), toDefinitions0(c, cmap))

  def toDefinitions0(c: Class[?], cmap: ClassMap): Seq[api.ClassLikeDef] = {
    import api.DefinitionType.{ ClassDef, Module, Trait }
    val enclPkg = packageName(c)
    val mods = modifiers(c.getModifiers)
    val acc = access(c.getModifiers, enclPkg)
    val annots = annotations(c.getAnnotations)
    val children = childrenOfSealedClass(c)
    val topLevel = c.getEnclosingClass == null
    val name = classCanonicalName(c)
    val tpe = if (Modifier.isInterface(c.getModifiers)) Trait else ClassDef
    lazy val (static, instance) = structure(c, enclPkg, cmap)
    val cls = api.ClassLike.of(
      name,
      acc,
      mods,
      annots,
      tpe,
      lzyS(Empty),
      lzy(instance, cmap),
      emptyStringArray,
      children.toArray,
      topLevel,
      typeParameters(typeParameterTypes(c))
    )
    val clsDef =
      api.ClassLikeDef.of(name, acc, mods, annots, typeParameters(typeParameterTypes(c)), tpe)
    val stat = api.ClassLike.of(
      name,
      acc,
      mods,
      annots,
      Module,
      lzyS(Empty),
      lzy(static, cmap),
      emptyStringArray,
      emptyTypeArray,
      topLevel,
      emptyTypeParameterArray
    )
    val statDef = api.ClassLikeDef.of(name, acc, mods, annots, emptyTypeParameterArray, Module)
    val defs = cls :: stat :: Nil
    val defsEmptyMembers = clsDef :: statDef :: Nil
    cmap.memo(name) = defsEmptyMembers
    cmap.allNonLocalClasses ++= defs

    if (
      c.getMethods.exists(meth =>
        meth.getName == "main" &&
          Modifier.isStatic(meth.getModifiers) &&
          meth.getParameterTypes.length == 1 &&
          meth.getParameterTypes.head == classOf[Array[String]] &&
          meth.getReturnType == java.lang.Void.TYPE
      )
    ) {
      cmap.mainClasses += name
    }

    defsEmptyMembers
  }

  /** Returns the (static structure, instance structure, inherited classes) for `c`. */
  def structure(
      c: Class[?],
      enclPkg: Option[String],
      cmap: ClassMap
  ): (api.Structure, api.Structure) = {
    lazy val cf = classFileForClass(c)
    val methods = mergeMap(c, c.getDeclaredMethods, c.getMethods, methodToDef(enclPkg))
    val fields = mergeMap(c, c.getDeclaredFields, c.getFields, fieldToDef(c, cf, enclPkg))
    val constructors =
      mergeMap(c, c.getDeclaredConstructors, c.getConstructors, constructorToDef(enclPkg))
    val classes = merge[Class[?]](
      c,
      c.getDeclaredClasses,
      c.getClasses,
      toDefinitions(cmap),
      (_: Seq[Class[?]]).partition(isStatic),
      _.getEnclosingClass != c
    )
    val all = methods ++ fields ++ constructors ++ classes
    val parentJavaTypes = allSuperTypes(c)
    if (!Modifier.isPrivate(c.getModifiers))
      cmap.inherited ++= parentJavaTypes.collect { case parent: Class[?] => c -> parent }
    val parentTypes = types(parentJavaTypes)
    val instanceStructure =
      api.Structure.of(lzyS(parentTypes), lzyS(all.declared.toArray), lzyS(all.inherited.toArray))
    val staticStructure = api.Structure.of(
      lzyEmptyTpeArray,
      lzyS(all.staticDeclared.toArray),
      lzyS(all.staticInherited.toArray)
    )
    (staticStructure, instanceStructure)
  }

  /** TODO: over time, ClassToAPI should switch the majority of access to the classfile parser */
  private[this] def classFileForClass(c: Class[?]): ClassFile =
    classfile.Parser.apply(IO.classfileLocation(c), Logger.Null)

  @inline private[this] def lzyS[T <: AnyRef](t: T): xsbti.api.Lazy[T] = SafeLazyProxy.strict(t)
  @inline final def lzy[T <: AnyRef](t: => T): xsbti.api.Lazy[T] = SafeLazyProxy(t)
  private[this] def lzy[T <: AnyRef](t: => T, cmap: ClassMap): xsbti.api.Lazy[T] = {
    val s = lzy(t)
    cmap.lz += s
    s
  }

  private val emptyStringArray = new Array[String](0)
  private val emptyTypeArray = new Array[xsbti.api.Type](0)
  private val emptyAnnotationArray = new Array[xsbti.api.Annotation](0)
  private val emptyTypeParameterArray = new Array[xsbti.api.TypeParameter](0)
  private val lzyEmptyTpeArray = lzyS(emptyTypeArray)
  private val lzyEmptyDefArray = lzyS(new Array[xsbti.api.ClassDefinition](0))

  private def allSuperTypes(t: Type): Seq[Type] = {
    @tailrec def accumulate(t: Type, accum: Seq[Type] = Seq.empty): Seq[Type] = t match {
      case c: Class[?] =>
        val (parent, interfaces) = (c.getGenericSuperclass, c.getGenericInterfaces)
        accumulate(parent, (accum :+ parent) ++ flattenAll(interfaces))
      case p: ParameterizedType =>
        accumulate(p.getRawType, accum)
      case _ =>
        accum
    }
    @tailrec def flattenAll(interfaces: Seq[Type], accum: Seq[Type] = Seq.empty): Seq[Type] = {
      if (interfaces.nonEmpty) {
        val raw = interfaces map { case p: ParameterizedType => p.getRawType; case i => i }
        val children = raw flatMap {
          case i: Class[?] => i.getGenericInterfaces; case _ => Seq.empty
        }
        flattenAll(children, accum ++ interfaces ++ children)
      } else
        accum
    }
    accumulate(t).filterNot(_ == null).distinct
  }

  def types(ts: Seq[Type]): Array[api.Type] =
    ts.filter(_ ne null).map(reference).toArray
  def upperBounds(ts: Array[Type]): api.Type =
    api.Structure.of(lzy(types(ts)), lzyEmptyDefArray, lzyEmptyDefArray)

  @deprecated("No longer used", "0.13.0")
  def parents(c: Class[?]): Seq[api.Type] = types(allSuperTypes(c))

  @deprecated("Use fieldToDef[4] instead", "0.13.9")
  def fieldToDef(enclPkg: Option[String])(f: Field): api.FieldLike = {
    val c = f.getDeclaringClass
    fieldToDef(c, classFileForClass(c), enclPkg)(f)
  }

  def fieldToDef(c: Class[?], cf: => ClassFile, enclPkg: Option[String])(
      f: Field
  ): api.FieldLike = {
    val name = f.getName
    val accs = access(f.getModifiers, enclPkg)
    val mods = modifiers(f.getModifiers)
    val annots = annotations(f.getDeclaredAnnotations)
    val fieldTpe = reference(returnType(f))
    // generate a more specific type for constant fields
    val specificTpe: Option[api.Type] =
      if (mods.isFinal) {
        try {
          cf.constantValue(name).map(singletonForConstantField(c, f, _))
        } catch {
          case e: Throwable =>
            throw new IllegalStateException(
              s"Failed to parse class $c: this may mean your classfiles are corrupted. Please clean and try again.",
              e
            )
        }
      } else {
        None
      }
    val tpe = specificTpe.getOrElse(fieldTpe)
    if (mods.isFinal) {
      api.Val.of(name, accs, mods, annots, tpe)
    } else {
      api.Var.of(name, accs, mods, annots, tpe)
    }
  }

  /**
   * Creates a Singleton type that includes both the type and ConstantValue for the given Field.
   *
   * Since java compilers are allowed to inline constant (static final primitive) fields in
   * downstream classfiles, we generate a type that will cause APIs to match only when both
   * the type and value of the field match. We include the classname mostly for readability.
   *
   * Because this type is purely synthetic, it's fine that the name might contain filename-
   * banned characters.
   */
  private def singletonForConstantField(c: Class[?], field: Field, constantValue: AnyRef) =
    api.Singleton.of(
      pathFromStrings(
        c.getName
          .split("\\.")
          .toSeq :+ (field.getName + "$" + returnType(field) + "$" + constantValue)
      )
    )

  def methodToDef(enclPkg: Option[String])(m: Method): api.Def =
    defLike(
      m.getName,
      m.getModifiers,
      m.getDeclaredAnnotations,
      typeParameterTypes(m),
      m.getParameterAnnotations,
      parameterTypes(m),
      Option(returnType(m)),
      exceptionTypes(m),
      m.isVarArgs,
      enclPkg
    )

  /** Use the unique constructor format defined in [[xsbt.ClassName.constructorName]]. */
  private def uniqueConstructorName(constructor: Constructor[?]): String =
    s"${name(constructor).replace('.', ';')};init;"
  def constructorToDef(enclPkg: Option[String])(c: Constructor[?]): api.Def =
    defLike(
      uniqueConstructorName(c),
      c.getModifiers,
      c.getDeclaredAnnotations,
      typeParameterTypes(c),
      c.getParameterAnnotations,
      parameterTypes(c),
      None,
      exceptionTypes(c),
      c.isVarArgs,
      enclPkg
    )

  def defLike[T <: GenericDeclaration](
      name: String,
      mods: Int,
      annots: Array[Annotation],
      tps: Array[TypeVariable[T]],
      paramAnnots: Array[Array[Annotation]],
      paramTypes: Array[Type],
      retType: Option[Type],
      exceptions: Array[Type],
      varArgs: Boolean,
      enclPkg: Option[String]
  ): api.Def = {
    val varArgPosition = if (varArgs) paramTypes.length - 1 else -1
    val isVarArg = List.tabulate(paramTypes.length)(_ == varArgPosition)
    val pa = (paramAnnots, paramTypes, isVarArg).zipped map {
      case (a, p, v) => parameter(a, p, v)
    }
    val params = api.ParameterList.of(pa.toArray, false)
    val ret = retType match { case Some(rt) => reference(rt); case None => Empty }
    api.Def.of(
      name,
      access(mods, enclPkg),
      modifiers(mods),
      annotations(annots) ++ exceptionAnnotations(exceptions),
      typeParameters(tps),
      Array(params),
      ret
    )
  }

  def exceptionAnnotations(exceptions: Array[Type]): Array[api.Annotation] =
    if (exceptions.length == 0) emptyAnnotationArray
    else
      arrayMap(exceptions)(t =>
        api.Annotation.of(Throws, Array(api.AnnotationArgument.of("value", t.toString)))
      )

  def parameter(annots: Array[Annotation], parameter: Type, varArgs: Boolean): api.MethodParameter =
    api.MethodParameter.of(
      "",
      annotated(reference(parameter), annots),
      false,
      if (varArgs) api.ParameterModifier.Repeated else api.ParameterModifier.Plain
    )

  def annotated(t: api.Type, annots: Array[Annotation]): api.Type =
    (
      if (annots.length == 0) t
      else api.Annotated.of(t, annotations(annots))
    )

  case class Defs(
      declared: Seq[api.ClassDefinition],
      inherited: Seq[api.ClassDefinition],
      staticDeclared: Seq[api.ClassDefinition],
      staticInherited: Seq[api.ClassDefinition]
  ) {
    def ++(o: Defs) =
      Defs(
        declared ++ o.declared,
        inherited ++ o.inherited,
        staticDeclared ++ o.staticDeclared,
        staticInherited ++ o.staticInherited
      )
  }
  def mergeMap[T <: Member](
      of: Class[?],
      self: Seq[T],
      public: Seq[T],
      f: T => api.ClassDefinition
  ): Defs =
    merge[T](of, self, public, x => f(x) :: Nil, splitStatic, _.getDeclaringClass != of)

  def merge[T](
      of: Class[?],
      self: Seq[T],
      public: Seq[T],
      f: T => Seq[api.ClassDefinition],
      splitStatic: Seq[T] => (Seq[T], Seq[T]),
      isInherited: T => Boolean
  ): Defs = {
    val (selfStatic, selfInstance) = splitStatic(self)
    val (inheritedStatic, inheritedInstance) = splitStatic(public filter isInherited)
    Defs(
      selfInstance flatMap f,
      inheritedInstance flatMap f,
      selfStatic flatMap f,
      inheritedStatic flatMap f
    )
  }

  def splitStatic[T <: Member](defs: Seq[T]): (Seq[T], Seq[T]) =
    defs partition isStatic

  def isStatic(c: Class[?]): Boolean = Modifier.isStatic(c.getModifiers)
  def isStatic(a: Member): Boolean = Modifier.isStatic(a.getModifiers)

  def typeParameters[T <: GenericDeclaration](
      tps: Array[TypeVariable[T]]
  ): Array[api.TypeParameter] =
    if (tps.length == 0) emptyTypeParameterArray
    else arrayMap(tps)(typeParameter)

  def typeParameter[T <: GenericDeclaration](tp: TypeVariable[T]): api.TypeParameter =
    api.TypeParameter.of(
      typeVariable(tp),
      emptyAnnotationArray,
      emptyTypeParameterArray,
      api.Variance.Invariant,
      NothingRef,
      upperBounds(tp.getBounds)
    )

  // needs to be stable across compilations
  def typeVariable[T <: GenericDeclaration](tv: TypeVariable[T]): String =
    name(tv.getGenericDeclaration) + " " + tv.getName

  def reduceHash(in: Array[Byte]): Int =
    in.foldLeft(0)((acc, b) => (acc * 43) ^ b)

  def name(gd: GenericDeclaration): String =
    gd match {
      case c: Class[?]       => classCanonicalName(c)
      case m: Method         => m.getName
      case c: Constructor[?] => c.getName
    }

  def modifiers(i: Int): api.Modifiers = {
    import Modifier.{ isAbstract, isFinal }
    new api.Modifiers(isAbstract(i), false, isFinal(i), false, false, false, false, false)
  }
  def access(i: Int, pkg: Option[String]): api.Access = {
    import Modifier.{ isPublic, isPrivate, isProtected }
    if (isPublic(i)) Public
    else if (isPrivate(i)) Private
    else if (isProtected(i)) Protected
    else packagePrivate(pkg)
  }

  def annotations(a: Array[Annotation]): Array[api.Annotation] =
    if (a.length == 0) emptyAnnotationArray else arrayMap(a)(annotation)
  def annotation(a: Annotation): api.Annotation =
    api.Annotation.of(reference(a.annotationType), Array(javaAnnotation(a.toString)))

  /**
   * This method mimics Scala compiler's behavior of `Symbol.children` method when Symbol corresponds to
   * a Java-defined enum class. Java's enum is modelled as a sealed class and enum's constants are modelled as
   * children.
   *
   * We need this logic to trigger recompilation due to changes to pattern exhaustivity checking results.
   */
  private def childrenOfSealedClass(c: Class[?]): Seq[api.Type] =
    if (!c.isEnum) emptyTypeArray
    else {
      // Calling getCanonicalName() on classes from enum constants yields same string as enumClazz.getCanonicalName
      // Moreover old behaviour create new instance of enum - what may fail (e.g. in static block )
      Array(reference(c))
    }

  // full information not available from reflection
  def javaAnnotation(s: String): api.AnnotationArgument =
    api.AnnotationArgument.of("toString", s)

  def array(tpe: api.Type): api.Type = api.Parameterized.of(ArrayRef, Array(tpe))
  def reference(c: Class[?]): api.Type =
    if (c.isArray) array(reference(c.getComponentType))
    else if (c.isPrimitive) primitive(c.getName)
    else reference(classCanonicalName(c))

  // does not handle primitives
  def reference(s: String): api.Type = {
    val (pkg, cls) = packageAndName(s)
    pkg match {
      // translate all primitives?
      case None => api.Projection.of(Empty, cls)
      case Some(p) =>
        api.Projection.of(api.Singleton.of(pathFromString(p)), cls)
    }
  }

  // sbt/zinc#389: Ignore nulls coming from generic parameter types of lambdas
  private[this] def ignoreNulls[T](genericTypes: Array[T]): Array[T] =
    genericTypes.filter(_ != null)

  def referenceP(t: ParameterizedType): api.Parameterized = {
    val targs = ignoreNulls(t.getActualTypeArguments)
    val args = if (targs.isEmpty) emptyTypeArray else arrayMap(targs)(t => reference(t): api.Type)
    val base = reference(t.getRawType)
    api.Parameterized.of(base, args)
  }

  def reference(t: Type): api.Type =
    t match {
      case _: WildcardType       => reference("_")
      case tv: TypeVariable[?]   => api.ParameterRef.of(typeVariable(tv))
      case pt: ParameterizedType => referenceP(pt)
      case gat: GenericArrayType => array(reference(gat.getGenericComponentType))
      case c: Class[?]           => reference(c)
    }

  def pathFromString(s: String): api.Path =
    pathFromStrings(s.split("\\."))
  def pathFromStrings(ss: Seq[String]): api.Path =
    api.Path.of((ss.map(api.Id.of(_)) :+ ThisRef).toArray)
  def packageName(c: Class[?]) = packageAndName(c)._1
  def packageAndName(c: Class[?]): (Option[String], String) =
    packageAndName(c.getName)
  def packageAndName(name: String): (Option[String], String) = {
    val lastDot = name.lastIndexOf('.')
    if (lastDot >= 0)
      (Some(name.substring(0, lastDot)), name.substring(lastDot + 1))
    else
      (None, name)
  }

  val Empty = api.EmptyType.of()
  val ThisRef = api.This.of()

  val Public = api.Public.of()
  val Unqualified = api.Unqualified.of()
  val Private = api.Private.of(Unqualified)
  val Protected = api.Protected.of(Unqualified)
  def packagePrivate(pkg: Option[String]): api.Access =
    api.Private.of(api.IdQualifier.of(pkg getOrElse ""))

  val ArrayRef = reference("scala.Array")
  val Throws = reference("scala.throws")
  val NothingRef = reference("scala.Nothing")

  private[this] def PrimitiveNames =
    Seq("boolean", "byte", "char", "short", "int", "long", "float", "double")
  private[this] def PrimitiveMap = PrimitiveNames.map(j => (j, j.capitalize)) :+ ("void" -> "Unit")
  private[this] val PrimitiveRefs = PrimitiveMap.map {
    case (n, sn) => (n, reference("scala." + sn))
  }.toMap
  def primitive(name: String): api.Type = PrimitiveRefs(name)

  private[this] def returnType(f: Field): Type = f.getGenericType
  private[this] def returnType(m: Method): Type = m.getGenericReturnType
  private[this] def exceptionTypes(c: Constructor[?]): Array[Type] = c.getGenericExceptionTypes

  private[this] def exceptionTypes(m: Method): Array[Type] = m.getGenericExceptionTypes

  private[this] def parameterTypes(m: Method): Array[Type] =
    ignoreNulls(m.getGenericParameterTypes)

  private[this] def parameterTypes(c: Constructor[?]): Array[Type] =
    ignoreNulls(c.getGenericParameterTypes)

  private[this] def typeParameterTypes[T](m: Constructor[T]): Array[TypeVariable[Constructor[T]]] =
    m.getTypeParameters
  private[this] def typeParameterTypes[T](m: Class[T]): Array[TypeVariable[Class[T]]] =
    m.getTypeParameters
  private[this] def typeParameterTypes(m: Method): Array[TypeVariable[Method]] =
    m.getTypeParameters
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy