Move API extraction logic to a separate class.

This way we have a little bit more clear separation
between compiler phase logic and the core logic responsible for
processing each compilation unit and extracting an api for it.

As added benefit, we have a little bit less of mutable state
(e.g. sourceFile doesn't need to be a var anymore).

The API extraction logic contains some internal caches that are
required for correctness. It wasn't very clear if they have to
be maintained during entire phase run or just during single compilation
unit processing. It looks like they have to be maintained during
single compilation unit processing and refactored code both
documents that contracts and implements it in the API phase.
This commit is contained in:
Grzegorz Kossakowski 2013-07-24 15:18:44 -07:00
parent 7d4cf7b8ab
commit beea6a9b4a
2 changed files with 461 additions and 432 deletions

View File

@ -20,7 +20,7 @@ object API
final class API(val global: CallbackGlobal) extends Compat
{
import global._
def error(msg: String) = throw new RuntimeException(msg)
@inline def debug(msg: => String) = if(settings.verbose.value) inform(msg)
def newPhase(prev: Phase) = new ApiPhase(prev)
@ -39,393 +39,25 @@ final class API(val global: CallbackGlobal) extends Compat
def processScalaUnit(unit: CompilationUnit)
{
val sourceFile = unit.source.file.file
currentSourceFile = sourceFile
debug("Traversing " + sourceFile)
val traverser = new TopLevelHandler(sourceFile)
val extractApi = new ExtractAPI[global.type](global, sourceFile)
val traverser = new TopLevelHandler(extractApi)
traverser.apply(unit.body)
val packages = traverser.packages.toArray[String].map(p => new xsbti.api.Package(p))
val source = new xsbti.api.SourceAPI(packages, traverser.definitions.toArray[xsbti.api.Definition])
forceStructures()
clearCaches()
extractApi.forceStructures()
callback.api(sourceFile, source)
}
}
// Tracks the source file associated with the CompilationUnit currently being processed by the API phase.
// This is used when recording inheritance dependencies.
private[this] var currentSourceFile: File = _
// this cache reduces duplicate work both here and when persisting
// caches on other structures had minimal effect on time and cache size
// (tried: Definition, Modifier, Path, Id, String)
private[this] val typeCache = new HashMap[(Symbol,Type), xsbti.api.Type]
// these caches are necessary for correctness
private[this] val structureCache = new HashMap[Symbol, xsbti.api.Structure]
private[this] val classLikeCache = new HashMap[(Symbol,Symbol), xsbti.api.ClassLike]
private[this] val pending = new HashSet[xsbti.api.Lazy[_]]
private[this] val emptyStringArray = new Array[String](0)
// to mitigate "temporary leaks" like that caused by NoPhase in 2.8.0,
// this ensures this class is not retaining objects
private def clearCaches()
{
typeCache.clear()
structureCache.clear()
classLikeCache.clear()
}
// call back to the xsbti.SafeLazy class in main sbt code to construct a SafeLazy instance
// we pass a thunk, whose class is loaded by the interface class loader (this class's loader)
// SafeLazy ensures that once the value is forced, the thunk is nulled out and so
// references to the thunk's classes are not retained. Specifically, it allows the interface classes
// (those in this subproject) to be garbage collected after compilation.
private[this] val safeLazy = Class.forName("xsbti.SafeLazy").getMethod("apply", classOf[xsbti.F0[_]])
private def lzy[S <: AnyRef](s: => S): xsbti.api.Lazy[S] =
{
val z = safeLazy.invoke(null, Message(s)).asInstanceOf[xsbti.api.Lazy[S]]
pending += z
z
}
// force all lazy structures. This is necessary so that we see the symbols/types at this phase and
// so that we don't hold on to compiler objects and classes
private def forceStructures(): Unit =
if(pending.isEmpty)
structureCache.clear()
else
{
val toProcess = pending.toList
pending.clear()
toProcess foreach { _.get() }
forceStructures()
}
private def thisPath(sym: Symbol) = path(pathComponents(sym, Constants.thisPath :: Nil))
private def path(components: List[PathComponent]) = new xsbti.api.Path(components.toArray[PathComponent])
private def pathComponents(sym: Symbol, postfix: List[PathComponent]): List[PathComponent] =
{
if(sym == NoSymbol || sym.isRoot || sym.isEmptyPackageClass || sym.isRootPackage) postfix
else pathComponents(sym.owner, new xsbti.api.Id(simpleName(sym)) :: postfix)
}
private def simpleType(in: Symbol, t: Type): SimpleType =
processType(in, t) match
{
case s: SimpleType => s
case x => warning("Not a simple type:\n\tType: " + t + " (" + t.getClass + ")\n\tTransformed: " + x.getClass); Constants.emptyType
}
private def types(in: Symbol, t: List[Type]): Array[xsbti.api.Type] = t.toArray[Type].map(processType(in, _))
private def projectionType(in: Symbol, pre: Type, sym: Symbol) =
{
if(pre == NoPrefix)
{
if(sym.isLocalClass || sym.isRoot || sym.isRootPackage) Constants.emptyType
else if(sym.isTypeParameterOrSkolem || sym.isExistentiallyBound) reference(sym)
else {
// this appears to come from an existential type in an inherited member- not sure why isExistential is false here
/*println("Warning: Unknown prefixless type: " + sym + " in " + sym.owner + " in " + sym.enclClass)
println("\tFlags: " + sym.flags + ", istype: " + sym.isType + ", absT: " + sym.isAbstractType + ", alias: " + sym.isAliasType + ", nonclass: " + isNonClassType(sym))*/
reference(sym)
}
}
else if(sym.isRoot || sym.isRootPackage) Constants.emptyType
else new xsbti.api.Projection(simpleType(in, pre), simpleName(sym))
}
private def reference(sym: Symbol): xsbti.api.ParameterRef = new xsbti.api.ParameterRef(tparamID(sym))
private def annotations(in: Symbol, as: List[AnnotationInfo]): Array[xsbti.api.Annotation] = as.toArray[AnnotationInfo].map(annotation(in,_))
private def annotation(in: Symbol, a: AnnotationInfo) =
new xsbti.api.Annotation(processType(in, a.atp),
if(a.assocs.isEmpty) Array(new xsbti.api.AnnotationArgument("", a.args.mkString("(", ",", ")"))) // what else to do with a Tree?
else a.assocs.map { case (name, value) => new xsbti.api.AnnotationArgument(name.toString, value.toString) }.toArray[xsbti.api.AnnotationArgument]
)
private def annotated(in: Symbol, as: List[AnnotationInfo], tpe: Type) = new xsbti.api.Annotated(processType(in, tpe), annotations(in, as))
private def viewer(s: Symbol) = (if(s.isModule) s.moduleClass else s).thisType
private def printMember(label: String, in: Symbol, t: Type) = println(label + " in " + in + " : " + t + " (debug: " + debugString(t) + " )")
private def defDef(in: Symbol, s: Symbol) =
{
def build(t: Type, typeParams: Array[xsbti.api.TypeParameter], valueParameters: List[xsbti.api.ParameterList]): xsbti.api.Def =
{
def parameterList(syms: List[Symbol]): xsbti.api.ParameterList =
{
val isImplicitList = syms match { case head :: _ => isImplicit(head); case _ => false }
new xsbti.api.ParameterList(syms.map(parameterS).toArray, isImplicitList)
}
t match
{
case PolyType(typeParams0, base) =>
assert(typeParams.isEmpty)
assert(valueParameters.isEmpty)
build(base, typeParameters(in, typeParams0), Nil)
case MethodType(params, resultType) =>
build(resultType, typeParams, parameterList(params) :: valueParameters)
case Nullary(resultType) => // 2.9 and later
build(resultType, typeParams, valueParameters)
case returnType =>
val t2 = processType(in, dropConst(returnType))
new xsbti.api.Def(valueParameters.reverse.toArray, t2, typeParams, simpleName(s), getAccess(s), getModifiers(s), annotations(in,s))
}
}
def parameterS(s: Symbol): xsbti.api.MethodParameter =
makeParameter(simpleName(s), s.info, s.info.typeSymbol, s)
// paramSym is only for 2.8 and is to determine if the parameter has a default
def makeParameter(name: String, tpe: Type, ts: Symbol, paramSym: Symbol): xsbti.api.MethodParameter =
{
import xsbti.api.ParameterModifier._
val (t, special) =
if(ts == definitions.RepeatedParamClass)// || s == definitions.JavaRepeatedParamClass)
(tpe.typeArgs(0), Repeated)
else if(ts == definitions.ByNameParamClass)
(tpe.typeArgs(0), ByName)
else
(tpe, Plain)
new xsbti.api.MethodParameter(name, processType(in, t), hasDefault(paramSym), special)
}
val t = viewer(in).memberInfo(s)
build(t, Array(), Nil)
}
private def hasDefault(s: Symbol) = s != NoSymbol && s.hasFlag(Flags.DEFAULTPARAM)
private def fieldDef[T](in: Symbol, s: Symbol, keepConst: Boolean, create: (xsbti.api.Type, String, xsbti.api.Access, xsbti.api.Modifiers, Array[xsbti.api.Annotation]) => T): T =
{
val t = dropNullary(viewer(in).memberType(s))
val t2 = if(keepConst) t else dropConst(t)
create(processType(in, t2), simpleName(s), getAccess(s), getModifiers(s), annotations(in, s))
}
private def dropConst(t: Type): Type = t match {
case ConstantType(constant) => constant.tpe
case _ => t
}
private def dropNullary(t: Type): Type = t match {
case Nullary(un) => un
case _ => t
}
private def typeDef(in: Symbol, s: Symbol): xsbti.api.TypeMember =
{
val (typeParams, tpe) =
viewer(in).memberInfo(s) match
{
case PolyType(typeParams0, base) => (typeParameters(in, typeParams0), base)
case t => (Array[xsbti.api.TypeParameter](), t)
}
val name = simpleName(s)
val access = getAccess(s)
val modifiers = getModifiers(s)
val as = annotations(in, s)
if(s.isAliasType)
new xsbti.api.TypeAlias(processType(in, tpe), typeParams, name, access, modifiers, as)
else if(s.isAbstractType)
{
val bounds = tpe.bounds
new xsbti.api.TypeDeclaration(processType(in, bounds.lo), processType(in, bounds.hi), typeParams, name, access, modifiers, as)
}
else
error("Unknown type member" + s)
}
private def structure(in: Symbol, s: Symbol): xsbti.api.Structure = structure(viewer(in).memberInfo(s), s, true)
private def structure(info: Type): xsbti.api.Structure = structure(info, info.typeSymbol, false)
private def structure(info: Type, s: Symbol, inherit: Boolean): xsbti.api.Structure =
structureCache.getOrElseUpdate( s, mkStructure(info, s, inherit))
private def removeConstructors(ds: List[Symbol]): List[Symbol] = ds filter { !_.isConstructor}
private def mkStructure(info: Type, s: Symbol, inherit: Boolean): xsbti.api.Structure =
{
val (declared, inherited) = info.members.reverse.partition(_.owner == s)
val baseTypes = info.baseClasses.tail.map(info.baseType)
val ds = if(s.isModuleClass) removeConstructors(declared) else declared
val is = if(inherit) removeConstructors(inherited) else Nil
mkStructure(s, baseTypes, ds, is)
}
// If true, this template is publicly visible and should be processed as a public inheritance dependency.
// Local classes and local refinements will never be traversed by the api phase, so we don't need to check for that.
private[this] def isPublicStructure(s: Symbol): Boolean =
s.isStructuralRefinement ||
// do not consider templates that are private[this] or private
!(s.isPrivate && (s.privateWithin == NoSymbol || s.isLocal))
private def mkStructure(s: Symbol, bases: List[Type], declared: List[Symbol], inherited: List[Symbol]): xsbti.api.Structure = {
if(isPublicStructure(s))
addInheritedDependencies(currentSourceFile, bases.map(_.dealias.typeSymbol))
new xsbti.api.Structure(lzy(types(s, bases)), lzy(processDefinitions(s, declared)), lzy(processDefinitions(s, inherited)))
}
private def processDefinitions(in: Symbol, defs: List[Symbol]): Array[xsbti.api.Definition] =
sort(defs.toArray).flatMap( (d: Symbol) => definition(in, d))
private[this] def sort(defs: Array[Symbol]): Array[Symbol] = {
Arrays.sort(defs, sortClasses)
defs
}
private def definition(in: Symbol, sym: Symbol): Option[xsbti.api.Definition] =
{
def mkVar = Some(fieldDef(in, sym, false, new xsbti.api.Var(_,_,_,_,_)))
def mkVal = Some(fieldDef(in, sym, true, new xsbti.api.Val(_,_,_,_,_)))
if(isClass(sym))
if(ignoreClass(sym)) None else Some(classLike(in, sym))
else if(sym.isNonClassType)
Some(typeDef(in, sym))
else if(sym.isVariable)
if(isSourceField(sym)) mkVar else None
else if(sym.isStable)
if(isSourceField(sym)) mkVal else None
else if(sym.isSourceMethod && !sym.isSetter)
if(sym.isGetter) mkVar else Some(defDef(in, sym))
else
None
}
private def ignoreClass(sym: Symbol): Boolean =
sym.isLocalClass || sym.isAnonymousClass || sym.fullName.endsWith(LocalChild.toString)
// This filters private[this] vals/vars that were not in the original source.
// The getter will be used for processing instead.
private def isSourceField(sym: Symbol): Boolean =
{
val getter = sym.getter(sym.enclClass)
// the check `getter eq sym` is a precaution against infinite recursion
// `isParamAccessor` does not exist in all supported versions of Scala, so the flag check is done directly
(getter == NoSymbol && !sym.hasFlag(Flags.PARAMACCESSOR)) || (getter eq sym)
}
private def getModifiers(s: Symbol): xsbti.api.Modifiers =
{
import Flags._
val absOver = s.hasFlag(ABSOVERRIDE)
val abs = s.hasFlag(ABSTRACT) || s.hasFlag(DEFERRED) || absOver
val over = s.hasFlag(OVERRIDE) || absOver
new xsbti.api.Modifiers(abs, over, s.isFinal, s.hasFlag(SEALED), isImplicit(s), s.hasFlag(LAZY), hasMacro(s))
}
private def isImplicit(s: Symbol) = s.hasFlag(Flags.IMPLICIT)
private def getAccess(c: Symbol): xsbti.api.Access =
{
if(c.isPublic) Constants.public
else if(c.isPrivateLocal) Constants.privateLocal
else if(c.isProtectedLocal) Constants.protectedLocal
else
{
val within = c.privateWithin
val qualifier = if(within == NoSymbol) Constants.unqualified else new xsbti.api.IdQualifier(within.fullName)
if(c.hasFlag(Flags.PROTECTED)) new xsbti.api.Protected(qualifier)
else new xsbti.api.Private(qualifier)
}
}
/**
* Replace all types that directly refer to the `forbidden` symbol by `NoType`.
* (a specialized version of substThisAndSym)
*/
class SuppressSymbolRef(forbidden: Symbol) extends TypeMap {
def apply(tp: Type) =
if (tp.typeSymbolDirect == forbidden) NoType
else mapOver(tp)
}
private def processType(in: Symbol, t: Type): xsbti.api.Type = typeCache.getOrElseUpdate((in, t), makeType(in, t))
private def makeType(in: Symbol, t: Type): xsbti.api.Type =
{
val dealiased = t match {
case TypeRef(_, sym, _) if sym.isAliasType => t.dealias
case _ => t
}
dealiased match
{
case NoPrefix => Constants.emptyType
case ThisType(sym) => new xsbti.api.Singleton(thisPath(sym))
case SingleType(pre, sym) => projectionType(in, pre, sym)
case ConstantType(constant) => new xsbti.api.Constant(processType(in, constant.tpe), constant.stringValue)
/* explaining the special-casing of references to refinement classes (https://support.typesafe.com/tickets/1882)
*
* goal: a representation of type references to refinement classes that's stable across compilation runs
* (and thus insensitive to typing from source or unpickling from bytecode)
*
* problem: the current representation, which corresponds to the owner chain of the refinement:
* 1. is affected by pickling, so typing from source or using unpickled symbols give different results (because the unpickler "localizes" owners -- this could be fixed in the compiler)
* 2. can't distinguish multiple refinements in the same owner (this is a limitation of SBT's internal representation and cannot be fixed in the compiler)
*
* potential solutions:
* - simply drop the reference: won't work as collapsing all refinement types will cause recompilation to be skipped when a refinement is changed to another refinement
* - represent the symbol in the api: can't think of a stable way of referring to an anonymous symbol whose owner changes when pickled
* + expand the reference to the corresponding refinement type: doing that recursively may not terminate, but we can deal with that by approximating recursive references
* (all we care about is being sound for recompilation: recompile iff a dependency changes, and this will happen as long as we have one unrolling of the reference to the refinement)
*/
case TypeRef(pre, sym, Nil) if sym.isRefinementClass =>
// Since we only care about detecting changes reliably, we unroll a reference to a refinement class once.
// Recursive references are simply replaced by NoType -- changes to the type will be seen in the first unrolling.
// The API need not be type correct, so this truncation is acceptable. Most of all, the API should be compact.
val unrolling = pre.memberInfo(sym) // this is a refinement type
// in case there are recursive references, suppress them -- does this ever happen?
// we don't have a test case for this, so warn and hope we'll get a contribution for it :-)
val withoutRecursiveRefs = new SuppressSymbolRef(sym).mapOver(unrolling)
if (unrolling ne withoutRecursiveRefs)
reporter.warning(sym.pos, "sbt-api: approximated refinement ref"+ t +" (== "+ unrolling +") to "+ withoutRecursiveRefs +"\nThis is currently untested, please report the code you were compiling.")
structure(withoutRecursiveRefs)
case tr @ TypeRef(pre, sym, args) =>
val base = projectionType(in, pre, sym)
if(args.isEmpty)
if(isRawType(tr))
processType(in, rawToExistential(tr))
else
base
else
new xsbti.api.Parameterized(base, types(in, args))
case SuperType(thistpe: Type, supertpe: Type) => warning("sbt-api: Super type (not implemented): this=" + thistpe + ", super=" + supertpe); Constants.emptyType
case at: AnnotatedType => annotatedType(in, at)
case rt: CompoundType => structure(rt)
case ExistentialType(tparams, result) => new xsbti.api.Existential(processType(in, result), typeParameters(in, tparams))
case NoType => Constants.emptyType // this can happen when there is an error that will be reported by a later phase
case PolyType(typeParams, resultType) => new xsbti.api.Polymorphic(processType(in, resultType), typeParameters(in, typeParams))
case Nullary(resultType) => warning("sbt-api: Unexpected nullary method type " + in + " in " + in.owner); Constants.emptyType
case _ => warning("sbt-api: Unhandled type " + t.getClass + " : " + t); Constants.emptyType
}
}
private def typeParameters(in: Symbol, s: Symbol): Array[xsbti.api.TypeParameter] = typeParameters(in, s.typeParams)
private def typeParameters(in: Symbol, s: List[Symbol]): Array[xsbti.api.TypeParameter] = s.map(typeParameter(in,_)).toArray[xsbti.api.TypeParameter]
private def typeParameter(in: Symbol, s: Symbol): xsbti.api.TypeParameter =
{
val varianceInt = s.variance
import xsbti.api.Variance._
val annots = annotations(in, s)
val variance = if(varianceInt < 0) Contravariant else if(varianceInt > 0) Covariant else Invariant
viewer(in).memberInfo(s) match
{
case TypeBounds(low, high) => new xsbti.api.TypeParameter( tparamID(s), annots, typeParameters(in, s), variance, processType(in, low), processType(in, high) )
case PolyType(typeParams, base) => new xsbti.api.TypeParameter( tparamID(s), annots, typeParameters(in, typeParams), variance, processType(in, base.bounds.lo), processType(in, base.bounds.hi))
case x => error("Unknown type parameter info: " + x.getClass)
}
}
private def tparamID(s: Symbol) = s.fullName
private def selfType(in: Symbol, s: Symbol): xsbti.api.Type = processType(in, s.thisSym.typeOfThis)
private def classLike(in: Symbol, c: Symbol): ClassLike = classLikeCache.getOrElseUpdate( (in,c), mkClassLike(in, c))
private def mkClassLike(in: Symbol, c: Symbol): ClassLike =
{
val name = c.fullName
val isModule = c.isModuleClass || c.isModule
val struct = if(isModule) c.moduleClass else c
val defType =
if(c.isTrait) DefinitionType.Trait
else if(isModule)
{
if(c.isPackage) DefinitionType.PackageModule
else DefinitionType.Module
}
else DefinitionType.ClassDef
new xsbti.api.ClassLike(defType, lzy(selfType(in, c)), lzy(structure(in, struct)), emptyStringArray, typeParameters(in, c), name, getAccess(c), getModifiers(c), annotations(in, c))
}
private final class TopLevelHandler(sourceFile: File) extends TopLevelTraverser
private final class TopLevelHandler(extractApi: ExtractAPI[global.type]) extends TopLevelTraverser
{
val packages = new HashSet[String]
val definitions = new ListBuffer[xsbti.api.Definition]
def `class`(c: Symbol): Unit = definitions += classLike(c.owner, c)
def `class`(c: Symbol): Unit = {
definitions += extractApi.classLike(c.owner, c)
}
/** Record packages declared in the source file*/
def `package`(p: Symbol)
{
@ -438,41 +70,7 @@ final class API(val global: CallbackGlobal) extends Compat
}
}
}
private[this] def isClass(s: Symbol) = s.isClass || s.isModule
// necessary to ensure a stable ordering of classes in the definitions list:
// modules and classes come first and are sorted by name
// all other definitions come later and are not sorted
private[this] val sortClasses = new Comparator[Symbol] {
def compare(a: Symbol, b: Symbol) = {
val aIsClass = isClass(a)
val bIsClass = isClass(b)
if(aIsClass == bIsClass)
if(aIsClass)
if(a.isModule == b.isModule)
a.fullName.compareTo(b.fullName)
else if(a.isModule)
-1
else
1
else
0 // substantial performance hit if fullNames are compared here
else if(aIsClass)
-1
else
1
}
}
private object Constants
{
val local = new xsbti.api.ThisQualifier
val public = new xsbti.api.Public
val privateLocal = new xsbti.api.Private(local)
val protectedLocal = new xsbti.api.Protected(local)
val unqualified = new xsbti.api.Unqualified
val emptyPath = new xsbti.api.Path(Array())
val thisPath = new xsbti.api.This
val emptyType = new xsbti.api.EmptyType
}
private abstract class TopLevelTraverser extends Traverser
{
def `class`(s: Symbol)
@ -493,25 +91,6 @@ final class API(val global: CallbackGlobal) extends Compat
!sym.hasFlag(Flags.SYNTHETIC) && !sym.hasFlag(Flags.JAVA)
}
private def annotations(in: Symbol, s: Symbol): Array[xsbti.api.Annotation] =
atPhase(currentRun.typerPhase) {
val base = if(s.hasFlag(Flags.ACCESSOR)) s.accessed else NoSymbol
val b = if(base == NoSymbol) s else base
// annotations from bean methods are not handled because:
// a) they are recorded as normal source methods anyway
// b) there is no way to distinguish them from user-defined methods
val associated = List(b, b.getter(b.enclClass), b.setter(b.enclClass)).filter(_ != NoSymbol)
associated.flatMap( ss => annotations(in, ss.annotations) ).distinct.toArray ;
}
private def annotatedType(in: Symbol, at: AnnotatedType): xsbti.api.Type =
{
val annots = at.annotations
if(annots.isEmpty) processType(in, at.underlying) else annotated(in, annots, at.underlying)
}
private def simpleName(s: Symbol): String =
{
val n = s.originalName
val n2 = if(n.toString == "<init>") n else n.decode
n2.toString.trim
}
}

View File

@ -0,0 +1,450 @@
package xsbt
import java.io.File
import java.util.{Arrays,Comparator}
import scala.tools.nsc.{io, plugins, symtab, Global, Phase}
import io.{AbstractFile, PlainFile, ZipArchive}
import plugins.{Plugin, PluginComponent}
import symtab.Flags
import scala.collection.mutable.{HashMap, HashSet, ListBuffer}
import xsbti.api.{ClassLike, DefinitionType, PathComponent, SimpleType}
/**
* Extracts API representation out of Symbols and Types.
*
* Each compilation unit should be processed by a fresh instance of this class.
*
* This class depends on instance of CallbackGlobal instead of regular Global because
* it has a call to `addInheritedDependencies` method defined in CallbackGlobal. In the future
* we should refactor this code so inherited dependencies are just accumulated in a buffer and
* exposed to a client that can pass them to an instance of CallbackGlobal it holds.
*/
class ExtractAPI[GlobalType <: CallbackGlobal](val global: GlobalType,
// Tracks the source file associated with the CompilationUnit currently being processed by the API phase.
// This is used when recording inheritance dependencies.
sourceFile: File) extends Compat {
import global._
private def error(msg: String) = throw new RuntimeException(msg)
// this cache reduces duplicate work both here and when persisting
// caches on other structures had minimal effect on time and cache size
// (tried: Definition, Modifier, Path, Id, String)
private[this] val typeCache = new HashMap[(Symbol,Type), xsbti.api.Type]
// these caches are necessary for correctness
private[this] val structureCache = new HashMap[Symbol, xsbti.api.Structure]
private[this] val classLikeCache = new HashMap[(Symbol,Symbol), xsbti.api.ClassLike]
private[this] val pending = new HashSet[xsbti.api.Lazy[_]]
private[this] val emptyStringArray = new Array[String](0)
// call back to the xsbti.SafeLazy class in main sbt code to construct a SafeLazy instance
// we pass a thunk, whose class is loaded by the interface class loader (this class's loader)
// SafeLazy ensures that once the value is forced, the thunk is nulled out and so
// references to the thunk's classes are not retained. Specifically, it allows the interface classes
// (those in this subproject) to be garbage collected after compilation.
private[this] val safeLazy = Class.forName("xsbti.SafeLazy").getMethod("apply", classOf[xsbti.F0[_]])
private def lzy[S <: AnyRef](s: => S): xsbti.api.Lazy[S] =
{
val z = safeLazy.invoke(null, Message(s)).asInstanceOf[xsbti.api.Lazy[S]]
pending += z
z
}
/**
* Force all lazy structures. This is necessary so that we see the symbols/types at this phase and
* so that we don't hold on to compiler objects and classes
*/
def forceStructures(): Unit =
if(pending.isEmpty)
structureCache.clear()
else
{
val toProcess = pending.toList
pending.clear()
toProcess foreach { _.get() }
forceStructures()
}
private def thisPath(sym: Symbol) = path(pathComponents(sym, Constants.thisPath :: Nil))
private def path(components: List[PathComponent]) = new xsbti.api.Path(components.toArray[PathComponent])
private def pathComponents(sym: Symbol, postfix: List[PathComponent]): List[PathComponent] =
{
if(sym == NoSymbol || sym.isRoot || sym.isEmptyPackageClass || sym.isRootPackage) postfix
else pathComponents(sym.owner, new xsbti.api.Id(simpleName(sym)) :: postfix)
}
private def simpleType(in: Symbol, t: Type): SimpleType =
processType(in, t) match
{
case s: SimpleType => s
case x => warning("Not a simple type:\n\tType: " + t + " (" + t.getClass + ")\n\tTransformed: " + x.getClass); Constants.emptyType
}
private def types(in: Symbol, t: List[Type]): Array[xsbti.api.Type] = t.toArray[Type].map(processType(in, _))
private def projectionType(in: Symbol, pre: Type, sym: Symbol) =
{
if(pre == NoPrefix)
{
if(sym.isLocalClass || sym.isRoot || sym.isRootPackage) Constants.emptyType
else if(sym.isTypeParameterOrSkolem || sym.isExistentiallyBound) reference(sym)
else {
// this appears to come from an existential type in an inherited member- not sure why isExistential is false here
/*println("Warning: Unknown prefixless type: " + sym + " in " + sym.owner + " in " + sym.enclClass)
println("\tFlags: " + sym.flags + ", istype: " + sym.isType + ", absT: " + sym.isAbstractType + ", alias: " + sym.isAliasType + ", nonclass: " + isNonClassType(sym))*/
reference(sym)
}
}
else if(sym.isRoot || sym.isRootPackage) Constants.emptyType
else new xsbti.api.Projection(simpleType(in, pre), simpleName(sym))
}
private def reference(sym: Symbol): xsbti.api.ParameterRef = new xsbti.api.ParameterRef(tparamID(sym))
private def annotations(in: Symbol, as: List[AnnotationInfo]): Array[xsbti.api.Annotation] = as.toArray[AnnotationInfo].map(annotation(in,_))
private def annotation(in: Symbol, a: AnnotationInfo) =
new xsbti.api.Annotation(processType(in, a.atp),
if(a.assocs.isEmpty) Array(new xsbti.api.AnnotationArgument("", a.args.mkString("(", ",", ")"))) // what else to do with a Tree?
else a.assocs.map { case (name, value) => new xsbti.api.AnnotationArgument(name.toString, value.toString) }.toArray[xsbti.api.AnnotationArgument]
)
private def annotated(in: Symbol, as: List[AnnotationInfo], tpe: Type) = new xsbti.api.Annotated(processType(in, tpe), annotations(in, as))
private def viewer(s: Symbol) = (if(s.isModule) s.moduleClass else s).thisType
private def printMember(label: String, in: Symbol, t: Type) = println(label + " in " + in + " : " + t + " (debug: " + debugString(t) + " )")
private def defDef(in: Symbol, s: Symbol) =
{
def build(t: Type, typeParams: Array[xsbti.api.TypeParameter], valueParameters: List[xsbti.api.ParameterList]): xsbti.api.Def =
{
def parameterList(syms: List[Symbol]): xsbti.api.ParameterList =
{
val isImplicitList = syms match { case head :: _ => isImplicit(head); case _ => false }
new xsbti.api.ParameterList(syms.map(parameterS).toArray, isImplicitList)
}
t match
{
case PolyType(typeParams0, base) =>
assert(typeParams.isEmpty)
assert(valueParameters.isEmpty)
build(base, typeParameters(in, typeParams0), Nil)
case MethodType(params, resultType) =>
build(resultType, typeParams, parameterList(params) :: valueParameters)
case Nullary(resultType) => // 2.9 and later
build(resultType, typeParams, valueParameters)
case returnType =>
val t2 = processType(in, dropConst(returnType))
new xsbti.api.Def(valueParameters.reverse.toArray, t2, typeParams, simpleName(s), getAccess(s), getModifiers(s), annotations(in,s))
}
}
def parameterS(s: Symbol): xsbti.api.MethodParameter =
makeParameter(simpleName(s), s.info, s.info.typeSymbol, s)
// paramSym is only for 2.8 and is to determine if the parameter has a default
def makeParameter(name: String, tpe: Type, ts: Symbol, paramSym: Symbol): xsbti.api.MethodParameter =
{
import xsbti.api.ParameterModifier._
val (t, special) =
if(ts == definitions.RepeatedParamClass)// || s == definitions.JavaRepeatedParamClass)
(tpe.typeArgs(0), Repeated)
else if(ts == definitions.ByNameParamClass)
(tpe.typeArgs(0), ByName)
else
(tpe, Plain)
new xsbti.api.MethodParameter(name, processType(in, t), hasDefault(paramSym), special)
}
val t = viewer(in).memberInfo(s)
build(t, Array(), Nil)
}
private def hasDefault(s: Symbol) = s != NoSymbol && s.hasFlag(Flags.DEFAULTPARAM)
private def fieldDef[T](in: Symbol, s: Symbol, keepConst: Boolean, create: (xsbti.api.Type, String, xsbti.api.Access, xsbti.api.Modifiers, Array[xsbti.api.Annotation]) => T): T =
{
val t = dropNullary(viewer(in).memberType(s))
val t2 = if(keepConst) t else dropConst(t)
create(processType(in, t2), simpleName(s), getAccess(s), getModifiers(s), annotations(in, s))
}
private def dropConst(t: Type): Type = t match {
case ConstantType(constant) => constant.tpe
case _ => t
}
private def dropNullary(t: Type): Type = t match {
case Nullary(un) => un
case _ => t
}
private def typeDef(in: Symbol, s: Symbol): xsbti.api.TypeMember =
{
val (typeParams, tpe) =
viewer(in).memberInfo(s) match
{
case PolyType(typeParams0, base) => (typeParameters(in, typeParams0), base)
case t => (Array[xsbti.api.TypeParameter](), t)
}
val name = simpleName(s)
val access = getAccess(s)
val modifiers = getModifiers(s)
val as = annotations(in, s)
if(s.isAliasType)
new xsbti.api.TypeAlias(processType(in, tpe), typeParams, name, access, modifiers, as)
else if(s.isAbstractType)
{
val bounds = tpe.bounds
new xsbti.api.TypeDeclaration(processType(in, bounds.lo), processType(in, bounds.hi), typeParams, name, access, modifiers, as)
}
else
error("Unknown type member" + s)
}
private def structure(in: Symbol, s: Symbol): xsbti.api.Structure = structure(viewer(in).memberInfo(s), s, true)
private def structure(info: Type): xsbti.api.Structure = structure(info, info.typeSymbol, false)
private def structure(info: Type, s: Symbol, inherit: Boolean): xsbti.api.Structure =
structureCache.getOrElseUpdate( s, mkStructure(info, s, inherit))
private def removeConstructors(ds: List[Symbol]): List[Symbol] = ds filter { !_.isConstructor}
private def mkStructure(info: Type, s: Symbol, inherit: Boolean): xsbti.api.Structure =
{
val (declared, inherited) = info.members.reverse.partition(_.owner == s)
val baseTypes = info.baseClasses.tail.map(info.baseType)
val ds = if(s.isModuleClass) removeConstructors(declared) else declared
val is = if(inherit) removeConstructors(inherited) else Nil
mkStructure(s, baseTypes, ds, is)
}
// If true, this template is publicly visible and should be processed as a public inheritance dependency.
// Local classes and local refinements will never be traversed by the api phase, so we don't need to check for that.
private[this] def isPublicStructure(s: Symbol): Boolean =
s.isStructuralRefinement ||
// do not consider templates that are private[this] or private
!(s.isPrivate && (s.privateWithin == NoSymbol || s.isLocal))
private def mkStructure(s: Symbol, bases: List[Type], declared: List[Symbol], inherited: List[Symbol]): xsbti.api.Structure = {
if(isPublicStructure(s))
addInheritedDependencies(sourceFile, bases.map(_.dealias.typeSymbol))
new xsbti.api.Structure(lzy(types(s, bases)), lzy(processDefinitions(s, declared)), lzy(processDefinitions(s, inherited)))
}
private def processDefinitions(in: Symbol, defs: List[Symbol]): Array[xsbti.api.Definition] =
sort(defs.toArray).flatMap( (d: Symbol) => definition(in, d))
private[this] def sort(defs: Array[Symbol]): Array[Symbol] = {
Arrays.sort(defs, sortClasses)
defs
}
private def definition(in: Symbol, sym: Symbol): Option[xsbti.api.Definition] =
{
def mkVar = Some(fieldDef(in, sym, false, new xsbti.api.Var(_,_,_,_,_)))
def mkVal = Some(fieldDef(in, sym, true, new xsbti.api.Val(_,_,_,_,_)))
if(isClass(sym))
if(ignoreClass(sym)) None else Some(classLike(in, sym))
else if(sym.isNonClassType)
Some(typeDef(in, sym))
else if(sym.isVariable)
if(isSourceField(sym)) mkVar else None
else if(sym.isStable)
if(isSourceField(sym)) mkVal else None
else if(sym.isSourceMethod && !sym.isSetter)
if(sym.isGetter) mkVar else Some(defDef(in, sym))
else
None
}
private def ignoreClass(sym: Symbol): Boolean =
sym.isLocalClass || sym.isAnonymousClass || sym.fullName.endsWith(LocalChild.toString)
// This filters private[this] vals/vars that were not in the original source.
// The getter will be used for processing instead.
private def isSourceField(sym: Symbol): Boolean =
{
val getter = sym.getter(sym.enclClass)
// the check `getter eq sym` is a precaution against infinite recursion
// `isParamAccessor` does not exist in all supported versions of Scala, so the flag check is done directly
(getter == NoSymbol && !sym.hasFlag(Flags.PARAMACCESSOR)) || (getter eq sym)
}
private def getModifiers(s: Symbol): xsbti.api.Modifiers =
{
import Flags._
val absOver = s.hasFlag(ABSOVERRIDE)
val abs = s.hasFlag(ABSTRACT) || s.hasFlag(DEFERRED) || absOver
val over = s.hasFlag(OVERRIDE) || absOver
new xsbti.api.Modifiers(abs, over, s.isFinal, s.hasFlag(SEALED), isImplicit(s), s.hasFlag(LAZY), hasMacro(s))
}
private def isImplicit(s: Symbol) = s.hasFlag(Flags.IMPLICIT)
private def getAccess(c: Symbol): xsbti.api.Access =
{
if(c.isPublic) Constants.public
else if(c.isPrivateLocal) Constants.privateLocal
else if(c.isProtectedLocal) Constants.protectedLocal
else
{
val within = c.privateWithin
val qualifier = if(within == NoSymbol) Constants.unqualified else new xsbti.api.IdQualifier(within.fullName)
if(c.hasFlag(Flags.PROTECTED)) new xsbti.api.Protected(qualifier)
else new xsbti.api.Private(qualifier)
}
}
/**
* Replace all types that directly refer to the `forbidden` symbol by `NoType`.
* (a specialized version of substThisAndSym)
*/
class SuppressSymbolRef(forbidden: Symbol) extends TypeMap {
def apply(tp: Type) =
if (tp.typeSymbolDirect == forbidden) NoType
else mapOver(tp)
}
private def processType(in: Symbol, t: Type): xsbti.api.Type = typeCache.getOrElseUpdate((in, t), makeType(in, t))
private def makeType(in: Symbol, t: Type): xsbti.api.Type =
{
val dealiased = t match {
case TypeRef(_, sym, _) if sym.isAliasType => t.dealias
case _ => t
}
dealiased match
{
case NoPrefix => Constants.emptyType
case ThisType(sym) => new xsbti.api.Singleton(thisPath(sym))
case SingleType(pre, sym) => projectionType(in, pre, sym)
case ConstantType(constant) => new xsbti.api.Constant(processType(in, constant.tpe), constant.stringValue)
/* explaining the special-casing of references to refinement classes (https://support.typesafe.com/tickets/1882)
*
* goal: a representation of type references to refinement classes that's stable across compilation runs
* (and thus insensitive to typing from source or unpickling from bytecode)
*
* problem: the current representation, which corresponds to the owner chain of the refinement:
* 1. is affected by pickling, so typing from source or using unpickled symbols give different results (because the unpickler "localizes" owners -- this could be fixed in the compiler)
* 2. can't distinguish multiple refinements in the same owner (this is a limitation of SBT's internal representation and cannot be fixed in the compiler)
*
* potential solutions:
* - simply drop the reference: won't work as collapsing all refinement types will cause recompilation to be skipped when a refinement is changed to another refinement
* - represent the symbol in the api: can't think of a stable way of referring to an anonymous symbol whose owner changes when pickled
* + expand the reference to the corresponding refinement type: doing that recursively may not terminate, but we can deal with that by approximating recursive references
* (all we care about is being sound for recompilation: recompile iff a dependency changes, and this will happen as long as we have one unrolling of the reference to the refinement)
*/
case TypeRef(pre, sym, Nil) if sym.isRefinementClass =>
// Since we only care about detecting changes reliably, we unroll a reference to a refinement class once.
// Recursive references are simply replaced by NoType -- changes to the type will be seen in the first unrolling.
// The API need not be type correct, so this truncation is acceptable. Most of all, the API should be compact.
val unrolling = pre.memberInfo(sym) // this is a refinement type
// in case there are recursive references, suppress them -- does this ever happen?
// we don't have a test case for this, so warn and hope we'll get a contribution for it :-)
val withoutRecursiveRefs = new SuppressSymbolRef(sym).mapOver(unrolling)
if (unrolling ne withoutRecursiveRefs)
reporter.warning(sym.pos, "sbt-api: approximated refinement ref"+ t +" (== "+ unrolling +") to "+ withoutRecursiveRefs +"\nThis is currently untested, please report the code you were compiling.")
structure(withoutRecursiveRefs)
case tr @ TypeRef(pre, sym, args) =>
val base = projectionType(in, pre, sym)
if(args.isEmpty)
if(isRawType(tr))
processType(in, rawToExistential(tr))
else
base
else
new xsbti.api.Parameterized(base, types(in, args))
case SuperType(thistpe: Type, supertpe: Type) => warning("sbt-api: Super type (not implemented): this=" + thistpe + ", super=" + supertpe); Constants.emptyType
case at: AnnotatedType => annotatedType(in, at)
case rt: CompoundType => structure(rt)
case ExistentialType(tparams, result) => new xsbti.api.Existential(processType(in, result), typeParameters(in, tparams))
case NoType => Constants.emptyType // this can happen when there is an error that will be reported by a later phase
case PolyType(typeParams, resultType) => new xsbti.api.Polymorphic(processType(in, resultType), typeParameters(in, typeParams))
case Nullary(resultType) => warning("sbt-api: Unexpected nullary method type " + in + " in " + in.owner); Constants.emptyType
case _ => warning("sbt-api: Unhandled type " + t.getClass + " : " + t); Constants.emptyType
}
}
private def typeParameters(in: Symbol, s: Symbol): Array[xsbti.api.TypeParameter] = typeParameters(in, s.typeParams)
private def typeParameters(in: Symbol, s: List[Symbol]): Array[xsbti.api.TypeParameter] = s.map(typeParameter(in,_)).toArray[xsbti.api.TypeParameter]
private def typeParameter(in: Symbol, s: Symbol): xsbti.api.TypeParameter =
{
val varianceInt = s.variance
import xsbti.api.Variance._
val annots = annotations(in, s)
val variance = if(varianceInt < 0) Contravariant else if(varianceInt > 0) Covariant else Invariant
viewer(in).memberInfo(s) match
{
case TypeBounds(low, high) => new xsbti.api.TypeParameter( tparamID(s), annots, typeParameters(in, s), variance, processType(in, low), processType(in, high) )
case PolyType(typeParams, base) => new xsbti.api.TypeParameter( tparamID(s), annots, typeParameters(in, typeParams), variance, processType(in, base.bounds.lo), processType(in, base.bounds.hi))
case x => error("Unknown type parameter info: " + x.getClass)
}
}
private def tparamID(s: Symbol) = s.fullName
private def selfType(in: Symbol, s: Symbol): xsbti.api.Type = processType(in, s.thisSym.typeOfThis)
def classLike(in: Symbol, c: Symbol): ClassLike = classLikeCache.getOrElseUpdate( (in,c), mkClassLike(in, c))
private def mkClassLike(in: Symbol, c: Symbol): ClassLike =
{
val name = c.fullName
val isModule = c.isModuleClass || c.isModule
val struct = if(isModule) c.moduleClass else c
val defType =
if(c.isTrait) DefinitionType.Trait
else if(isModule)
{
if(c.isPackage) DefinitionType.PackageModule
else DefinitionType.Module
}
else DefinitionType.ClassDef
new xsbti.api.ClassLike(defType, lzy(selfType(in, c)), lzy(structure(in, struct)), emptyStringArray, typeParameters(in, c), name, getAccess(c), getModifiers(c), annotations(in, c))
}
private[this] def isClass(s: Symbol) = s.isClass || s.isModule
// necessary to ensure a stable ordering of classes in the definitions list:
// modules and classes come first and are sorted by name
// all other definitions come later and are not sorted
private[this] val sortClasses = new Comparator[Symbol] {
def compare(a: Symbol, b: Symbol) = {
val aIsClass = isClass(a)
val bIsClass = isClass(b)
if(aIsClass == bIsClass)
if(aIsClass)
if(a.isModule == b.isModule)
a.fullName.compareTo(b.fullName)
else if(a.isModule)
-1
else
1
else
0 // substantial performance hit if fullNames are compared here
else if(aIsClass)
-1
else
1
}
}
private object Constants
{
val local = new xsbti.api.ThisQualifier
val public = new xsbti.api.Public
val privateLocal = new xsbti.api.Private(local)
val protectedLocal = new xsbti.api.Protected(local)
val unqualified = new xsbti.api.Unqualified
val emptyPath = new xsbti.api.Path(Array())
val thisPath = new xsbti.api.This
val emptyType = new xsbti.api.EmptyType
}
private def simpleName(s: Symbol): String =
{
val n = s.originalName
val n2 = if(n.toString == "<init>") n else n.decode
n2.toString.trim
}
private def annotations(in: Symbol, s: Symbol): Array[xsbti.api.Annotation] =
atPhase(currentRun.typerPhase) {
val base = if(s.hasFlag(Flags.ACCESSOR)) s.accessed else NoSymbol
val b = if(base == NoSymbol) s else base
// annotations from bean methods are not handled because:
// a) they are recorded as normal source methods anyway
// b) there is no way to distinguish them from user-defined methods
val associated = List(b, b.getter(b.enclClass), b.setter(b.enclClass)).filter(_ != NoSymbol)
associated.flatMap( ss => annotations(in, ss.annotations) ).distinct.toArray ;
}
private def annotatedType(in: Symbol, at: AnnotatedType): xsbti.api.Type =
{
val annots = at.annotations
if(annots.isEmpty) processType(in, at.underlying) else annotated(in, annots, at.underlying)
}
}