package dotty.tools.dotc package sbt import ast.{Trees, tpd} import core._, core.Decorators._ import Contexts._, Flags._, Phases._, Trees._, Types._, Symbols._ import Names._, NameOps._, StdNames._ import scala.collection.{Set, mutable} import dotty.tools.io.{AbstractFile, Path, PlainFile, ZipArchive} import java.io.File import java.util.{Arrays, Comparator} import xsbti.DependencyContext /** This phase sends information on classes' dependencies to sbt via callbacks. * * This is used by sbt for incremental recompilation. Briefly, when a file * changes sbt will recompile it, if its API has changed (determined by what * `ExtractAPI` sent) then sbt will determine which reverse-dependencies * (determined by what `ExtractDependencies` sent) of the API have to be * recompiled depending on what changed. * * See the documentation of `ExtractDependenciesCollector`, `ExtractAPI`, * `ExtractAPICollector` and * http://www.scala-sbt.org/0.13/docs/Understanding-Recompilation.html for more * information on how sbt incremental compilation works. * * The following flags affect this phase: * -Yforce-sbt-phases * -Ydump-sbt-inc * * @see ExtractAPI */ class ExtractDependencies extends Phase { override def phaseName: String = "sbt-deps" // This phase should be run directly after `Frontend`, if it is run after // `PostTyper`, some dependencies will be lost because trees get simplified. // See the scripted test `constants` for an example where this matters. // TODO: Add a `Phase#runsBefore` method ? override def run(implicit ctx: Context): Unit = { val unit = ctx.compilationUnit val dumpInc = ctx.settings.YdumpSbtInc.value val forceRun = dumpInc || ctx.settings.YforceSbtPhases.value if ((ctx.sbtCallback != null || forceRun) && !unit.isJava) { val sourceFile = unit.source.file.file val extractDeps = new ExtractDependenciesCollector extractDeps.traverse(unit.tpdTree) if (dumpInc) { val names = extractDeps.usedNames.map(_.toString).toArray[Object] val deps = extractDeps.topLevelDependencies.map(_.toString).toArray[Object] val inhDeps = extractDeps.topLevelInheritanceDependencies.map(_.toString).toArray[Object] Arrays.sort(names) Arrays.sort(deps) Arrays.sort(inhDeps) val pw = Path(sourceFile).changeExtension("inc").toFile.printWriter() try { pw.println(s"// usedNames: ${names.mkString(",")}") pw.println(s"// topLevelDependencies: ${deps.mkString(",")}") pw.println(s"// topLevelInheritanceDependencies: ${inhDeps.mkString(",")}") } finally pw.close() } if (ctx.sbtCallback != null) { extractDeps.usedNames.foreach(name => ctx.sbtCallback.usedName(sourceFile, name.toString)) extractDeps.topLevelDependencies.foreach(dep => recordDependency(sourceFile, dep, DependencyContext.DependencyByMemberRef)) extractDeps.topLevelInheritanceDependencies.foreach(dep => recordDependency(sourceFile, dep, DependencyContext.DependencyByInheritance)) } } } /** Record that `currentSourceFile` depends on the file where `dep` was loaded from. * * @param currentSourceFile The source file of the current unit * @param dep The dependency * @param context Describes how `currentSourceFile` depends on `dep` */ def recordDependency(currentSourceFile: File, dep: Symbol, context: DependencyContext) (implicit ctx: Context) = { val depFile = dep.associatedFile if (depFile != null) { if (depFile.path.endsWith(".class")) { /** Transform `List(java, lang, String.class)` into `java.lang.String` */ def className(classSegments: List[String]) = classSegments.mkString(".").stripSuffix(".class") def binaryDependency(file: File, className: String) = ctx.sbtCallback.binaryDependency(file, className, currentSourceFile, context) depFile match { case ze: ZipArchive#Entry => for (zip <- ze.underlyingSource; zipFile <- Option(zip.file)) { val classSegments = Path(ze.path).segments binaryDependency(zipFile, className(classSegments)) } case pf: PlainFile => val packages = dep.ownersIterator .filter(x => x.is(PackageClass) && !x.isEffectiveRoot).length // We can recover the fully qualified name of a classfile from // its path val classSegments = pf.givenPath.segments.takeRight(packages + 1) binaryDependency(pf.file, className(classSegments)) case _ => } } else if (depFile.file != currentSourceFile) { ctx.sbtCallback.sourceDependency(depFile.file, currentSourceFile, context) } } } } /** Extract the dependency information of a compilation unit. * * To understand why we track the used names see the section "Name hashing * algorithm" in http://www.scala-sbt.org/0.13/docs/Understanding-Recompilation.html * To understand why we need to track dependencies introduced by inheritance * specially, see the subsection "Dependencies introduced by member reference and * inheritance" in the "Name hashing algorithm" section. */ private class ExtractDependenciesCollector(implicit val ctx: Context) extends tpd.TreeTraverser { import tpd._ private[this] val _usedNames = new mutable.HashSet[Name] private[this] val _topLevelDependencies = new mutable.HashSet[Symbol] private[this] val _topLevelInheritanceDependencies = new mutable.HashSet[Symbol] /** The names used in this class, this does not include names which are only * defined and not referenced. */ def usedNames: Set[Name] = _usedNames /** The set of top-level classes that the compilation unit depends on * because it refers to these classes or something defined in them. * This is always a superset of `topLevelInheritanceDependencies` by definition. */ def topLevelDependencies: Set[Symbol] = _topLevelDependencies /** The set of top-level classes that the compilation unit extends or that * contain a non-top-level class that the compilaion unit extends. */ def topLevelInheritanceDependencies: Set[Symbol] = _topLevelInheritanceDependencies private def addUsedName(name: Name) = _usedNames += name private def addDependency(sym: Symbol): Unit = if (!ignoreDependency(sym)) { val tlClass = sym.topLevelClass if (tlClass.ne(NoSymbol)) // Some synthetic type aliases like AnyRef do not belong to any class _topLevelDependencies += sym.topLevelClass addUsedName(sym.name) } private def ignoreDependency(sym: Symbol) = sym.eq(NoSymbol) || sym.isEffectiveRoot || sym.isAnonymousFunction || sym.isAnonymousClass private def addInheritanceDependency(sym: Symbol): Unit = _topLevelInheritanceDependencies += sym.topLevelClass /** Traverse the tree of a source file and record the dependencies which * can be retrieved using `topLevelDependencies`, `topLevelInheritanceDependencies`, * and `usedNames` */ override def traverse(tree: Tree)(implicit ctx: Context): Unit = { tree match { case Import(expr, selectors) => def lookupImported(name: Name) = expr.tpe.member(name).symbol def addImported(name: Name) = { // importing a name means importing both a term and a type (if they exist) addDependency(lookupImported(name.toTermName)) addDependency(lookupImported(name.toTypeName)) } selectors foreach { case Ident(name) => addImported(name) case Thicket(Ident(name) :: Ident(rename) :: Nil) => addImported(name) if (rename ne nme.WILDCARD) addUsedName(rename) case _ => } case Inlined(call, _, _) => // The inlined call is normally ignored by TreeTraverser but we need to // record it as a dependency traverse(call) case t: TypeTree => usedTypeTraverser.traverse(t.tpe) case ref: RefTree => addDependency(ref.symbol) usedTypeTraverser.traverse(ref.tpe) case t @ Template(_, parents, _, _) => t.parents.foreach(p => addInheritanceDependency(p.tpe.classSymbol)) case _ => } traverseChildren(tree) } /** Traverse a used type and record all the dependencies we need to keep track * of for incremental recompilation. * * As a motivating example, given a type `T` defined as: * * type T >: L <: H * type L <: A1 * type H <: B1 * class A1 extends A0 * class B1 extends B0 * * We need to record a dependency on `T`, `L`, `H`, `A1`, `B1`. This is * necessary because the API representation that `ExtractAPI` produces for * `T` just refers to the strings "L" and "H", it does not contain their API * representation. Therefore, the name hash of `T` does not change if for * example the definition of `L` changes. * * We do not need to keep track of superclasses like `A0` and `B0` because * the API representation of a class (and therefore its name hash) already * contains all necessary information on superclasses. * * A natural question to ask is: Since traversing all referenced types to * find all these names is costly, why not change the API representation * produced by `ExtractAPI` to contain that information? This way the name * hash of `T` would change if any of the types it depends on change, and we * would only need to record a dependency on `T`. Unfortunately there is no * simple answer to the question "what does T depend on?" because it depends * on the prefix and `ExtractAPI` does not compute types as seen from every * possible prefix, the documentation of `ExtractAPI` explains why. * * The tests in sbt `types-in-used-names-a`, `types-in-used-names-b`, * `as-seen-from-a` and `as-seen-from-b` rely on this. */ private object usedTypeTraverser extends TypeTraverser { val seen = new mutable.HashSet[Type] def traverse(tp: Type): Unit = if (!seen.contains(tp)) { seen += tp tp match { case tp: NamedType => val sym = tp.symbol if (!sym.is(Package)) { addDependency(sym) if (!sym.isClass) traverse(tp.info) traverse(tp.prefix) } case tp: ThisType => traverse(tp.underlying) case tp: ConstantType => traverse(tp.underlying) case tp: ParamRef => traverse(tp.underlying) case _ => traverseChildren(tp) } } } }