diff --git a/compile/inc/src/main/scala/sbt/inc/Analysis.scala b/compile/inc/src/main/scala/sbt/inc/Analysis.scala index f5e2ec735..956c146cb 100644 --- a/compile/inc/src/main/scala/sbt/inc/Analysis.scala +++ b/compile/inc/src/main/scala/sbt/inc/Analysis.scala @@ -41,7 +41,7 @@ trait Analysis def copy(stamps: Stamps = stamps, apis: APIs = apis, relations: Relations = relations, infos: SourceInfos = infos, compilations: Compilations = compilations): Analysis - + def addSource(src: File, api: Source, stamp: Stamp, directInternal: Iterable[File], inheritedInternal: Iterable[File], info: SourceInfo): Analysis def addBinaryDep(src: File, dep: File, className: String, stamp: Stamp): Analysis def addExternalDep(src: File, dep: String, api: Source, inherited: Boolean): Analysis @@ -59,6 +59,10 @@ object Analysis /** Merge multiple analysis objects into one. Deps will be internalized as needed. */ def merge(analyses: Traversable[Analysis]): Analysis = { + if (analyses.exists(_.relations.memberRefAndInheritanceDeps)) + throw new IllegalArgumentException("Merging of Analyses that have" + + "`relations.memberRefAndInheritanceDeps` set to `true` is not supported.") + // Merge the Relations, internalizing deps as needed. val mergedSrcProd = Relation.merge(analyses map { _.relations.srcProd }) val mergedBinaryDep = Relation.merge(analyses map { _.relations.binaryDep }) @@ -156,6 +160,10 @@ private class MAnalysis(val stamps: Stamps, val apis: APIs, val relations: Relat copy( stamps.markProduct(product, stamp), apis, relations.addProduct(src, product, name), infos ) def groupBy[K](discriminator: File => K): Map[K, Analysis] = { + if (relations.memberRefAndInheritanceDeps) + throw new UnsupportedOperationException("Grouping of Analyses that have" + + "`relations.memberRefAndInheritanceDeps` set to `true` is not supported.") + def discriminator1(x: (File, _)) = discriminator(x._1) // Apply the discriminator to the first coordinate. val kSrcProd = relations.srcProd.groupBy(discriminator1) diff --git a/compile/inc/src/main/scala/sbt/inc/Compile.scala b/compile/inc/src/main/scala/sbt/inc/Compile.scala index 7b36b8b97..d9ad4cf51 100644 --- a/compile/inc/src/main/scala/sbt/inc/Compile.scala +++ b/compile/inc/src/main/scala/sbt/inc/Compile.scala @@ -97,7 +97,7 @@ private final class AnalysisCallback(internalMap: File => Option[File], external } def sourceDependency(dependsOn: File, source: File, inherited: Boolean) = - if(source != dependsOn) { + { add(sourceDeps, source, dependsOn) if(inherited) add(inheritedSourceDeps, source, dependsOn) } @@ -151,6 +151,8 @@ private final class AnalysisCallback(internalMap: File => Option[File], external apis(sourceFile) = (HashAPI(source), savedSource) } + def memberRefAndInheritanceDeps: Boolean = false // TODO: define the flag in IncOptions which controls this + def get: Analysis = addCompilation( addExternals( addBinaries( addProducts( addSources(Analysis.Empty) ) ) ) ) def addProducts(base: Analysis): Analysis = addAll(base, classes) { case (a, src, (prod, name)) => a.addProduct(src, prod, current product prod, name ) } def addBinaries(base: Analysis): Analysis = addAll(base, binaryDeps)( (a, src, bin) => a.addBinaryDep(src, bin, binaryClassName(bin), current binary bin) ) diff --git a/compile/inc/src/main/scala/sbt/inc/Relations.scala b/compile/inc/src/main/scala/sbt/inc/Relations.scala index 56d661845..3f1b34725 100644 --- a/compile/inc/src/main/scala/sbt/inc/Relations.scala +++ b/compile/inc/src/main/scala/sbt/inc/Relations.scala @@ -6,6 +6,7 @@ package inc import java.io.File import Relations.Source +import Relations.SourceDependencies /** Provides mappings between source files, generated classes (products), and binaries. @@ -18,7 +19,7 @@ trait Relations { /** All sources _with at least one product_ . */ def allSources: collection.Set[File] - + /** All products associated with sources. */ def allProducts: collection.Set[File] @@ -36,12 +37,12 @@ trait Relations /** Source files that generated a class with the given fully qualified `name`. This is typically a set containing a single file. */ def definesClass(name: String): Set[File] - + /** The classes that were generated for source file `src`. */ def products(src: File): Set[File] /** The source files that generated class file `prod`. This is typically a set containing a single file. */ def produced(prod: File): Set[File] - + /** The binary dependencies for the source file `src`. */ def binaryDeps(src: File): Set[File] /** The source files that depend on binary file `dep`. */ @@ -51,12 +52,12 @@ trait Relations def internalSrcDeps(src: File): Set[File] /** Internal source files that depend on internal source `dep`. This includes both direct and inherited dependencies. */ def usesInternalSrc(dep: File): Set[File] - + /** External source dependencies that internal source file `src` depends on. This includes both direct and inherited dependencies. */ def externalDeps(src: File): Set[String] /** Internal source dependencies that depend on external source file `dep`. This includes both direct and inherited dependencies. */ def usesExternal(dep: String): Set[File] - + /** Records internal source file `src` as generating class file `prod` with top-level class `name`. */ def addProduct(src: File, prod: File, name: String): Relations @@ -82,10 +83,10 @@ trait Relations @deprecated("OK to remove in 0.14", "0.13.1") def groupBy[K](f: (File => K)): Map[K, Relations] - /** The relation between internal sources and generated class files. */ + /** The relation between internal sources and generated class files. */ def srcProd: Relation[File, File] - /** The dependency relation between internal sources and binaries. */ + /** The dependency relation between internal sources and binaries. */ def binaryDep: Relation[File, File] /** The dependency relation between internal sources. This includes both direct and inherited dependencies.*/ @@ -94,6 +95,48 @@ trait Relations /** The dependency relation between internal and external sources. This includes both direct and inherited dependencies.*/ def externalDep: Relation[File, String] + /** + * The source dependency relation between source files introduced by member reference. + * + * NOTE: All inheritance dependencies are included in this relation because in order to + * inherit from a member you have to refer to it. If you check documentation of `inheritance` + * you'll see that there's small oddity related to traits being the first parent of a + * class/trait that results in additional parents being introduced due to normalization. + * This relation properly accounts for that so the invariant that `memberRef` is a superset + * of `inheritance` is preserved. + */ + def memberRef: SourceDependencies + + /** + * The source dependency relation between source files introduced by inheritance. + * The dependency by inheritance is introduced when a template (class or trait) mentions + * a given type in a parent position. + * + * NOTE: Due to an oddity in how Scala's type checker works there's one unexpected dependency + * on a class being introduced. An example illustrates the best the problem. Let's consider + * the following structure: + * + * trait A extends B + * trait B extends C + * trait C extends D + * class D + * + * We are interested in dependencies by inheritance of `A`. One would expect it to be just `B` + * but the answer is `B` and `D`. The reason is because Scala's type checker performs a certain + * normalization so the first parent of a type is a class. Therefore the example above is normalized + * to the following form: + * + * trait A extends D with B + * trait B extends D with C + * trait C extends D + * class D + * + * Therefore if you inherit from a trait you'll get an additional dependency on a class that is + * resolved transitively. You should not rely on this behavior, though. + * + */ + def inheritance: SourceDependencies + /** The dependency relations between sources. These include both direct and inherited dependencies.*/ def direct: Source @@ -102,6 +145,19 @@ trait Relations /** The relation between a source file and the fully qualified names of classes generated from it.*/ def classes: Relation[File, String] + + /** + * Flag which indicates whether the new style (based on `memberRef` and `inheritance` source dependencies) + * of dependency tracking is enabled. When this flag is enabled access to `direct` and `publicInherited` + * relations is illegal and will cause runtime exception being thrown. + * + * Conversely, when `memberRefAndInheritanceDeps` flag is disabled access to `memberRef` and `inheritance` + * relations is illegal and will cause runtime exception being thrown. + * + * The name of this flag is ugly but it's private to incremental compiler and it's temporary measure during + * our migration to the new dependency tracking. + */ + private[inc] def memberRefAndInheritanceDeps: Boolean } @@ -133,6 +189,22 @@ object Relations override def hashCode = (internal, external).hashCode } + /** Tracks internal and external source dependencies for a specific dependency type, such as direct or inherited.*/ + private[inc] final class SourceDependencies(val internal: Relation[File,File], val external: Relation[File,String]) { + def addInternal(source: File, dependsOn: Iterable[File]): SourceDependencies = new SourceDependencies(internal + (source, dependsOn), external) + def addExternal(source: File, dependsOn: String): SourceDependencies = new SourceDependencies(internal, external + (source, dependsOn)) + /** Drops all dependency mappings from `sources`. Acts naively, i.e., doesn't externalize internal deps on removed files.*/ + def --(sources: Iterable[File]): SourceDependencies = new SourceDependencies(internal -- sources, external -- sources) + def ++(o: SourceDependencies): SourceDependencies = new SourceDependencies(internal ++ o.internal, external ++ o.external) + + override def equals(other: Any) = other match { + case o: SourceDependencies => internal == o.internal && external == o.external + case _ => false + } + + override def hashCode = (internal, external).hashCode + } + private[sbt] def getOrEmpty[A,B,K](m: Map[K, Relation[A,B]], k: K): Relation[A,B] = m.getOrElse(k, Relation.empty) private[this] lazy val e = Relation.empty[File, File] @@ -140,101 +212,181 @@ object Relations private[this] lazy val es = new Source(e, estr) def emptySource: Source = es - def empty: Relations = new MRelations(e, e, es, es, estr) + private[inc] lazy val emptySourceDependencies: SourceDependencies = new SourceDependencies(e, estr) + def empty: Relations = empty(memberRefAndInheritanceDeps = false) + def empty(memberRefAndInheritanceDeps: Boolean): Relations = + if (memberRefAndInheritanceDeps) + new MRelationsMemberRefAndInheritance(e, e, emptySourceDependencies, emptySourceDependencies, estr) + else + new MRelationsDirectAndPublicInherited(e, e, es, es, estr) def make(srcProd: Relation[File, File], binaryDep: Relation[File, File], direct: Source, publicInherited: Source, classes: Relation[File, String]): Relations = - new MRelations(srcProd, binaryDep, direct = direct, publicInherited = publicInherited, classes) - def makeSource(internal: Relation[File,File], external: Relation[File,String]): Source = new Source(internal, external) -} -/** -* `srcProd` is a relation between a source file and a product: (source, product). -* Note that some source files may not have a product and will not be included in this relation. -* -* `binaryDeps` is a relation between a source file and a binary dependency: (source, binary dependency). -* This only includes dependencies on classes and jars that do not have a corresponding source/API to track instead. -* A class or jar with a corresponding source should only be tracked in one of the source dependency relations. -* -* `direct` defines relations for dependencies between internal and external source dependencies. It includes all types of -* dependencies, including inheritance. -* -* `publicInherited` defines relations for internal and external source dependencies, only including dependencies -* introduced by inheritance. -* -* `classes` is a relation between a source file and its generated fully-qualified class names. -*/ -private class MRelations(val srcProd: Relation[File, File], val binaryDep: Relation[File, File], - // direct should include everything in inherited - val direct: Source, val publicInherited: Source, val classes: Relation[File, String]) extends Relations -{ - def internalSrcDep: Relation[File, File] = direct.internal - def externalDep: Relation[File, String] = direct.external + new MRelationsDirectAndPublicInherited(srcProd, binaryDep, direct = direct, publicInherited = publicInherited, classes) + private[inc] def make(srcProd: Relation[File, File], binaryDep: Relation[File, File], + memberRef: SourceDependencies, inheritance: SourceDependencies, classes: Relation[File, String]): Relations = + new MRelationsMemberRefAndInheritance(srcProd, binaryDep, memberRef = memberRef, inheritance = inheritance, classes) + def makeSource(internal: Relation[File,File], external: Relation[File,String]): Source = new Source(internal, external) + private[inc] def makeSourceDependencies(internal: Relation[File,File], external: Relation[File,String]): SourceDependencies = new SourceDependencies(internal, external) +} + + +/** + * An abstract class that contains common functionality inherited by two implementations of Relations trait. + * + * A little note why we have two different implementations of Relations trait. This is needed for the time + * being when we are slowly migrating to the new invalidation algorithm called "name hashing" which requires + * some subtle changes to dependency tracking. For some time we plan to keep both algorithms side-by-side + * and have a runtime switch which allows to pick one. So we need logic for both old and new dependency + * tracking to be available. That's exactly what two subclasses of MRelationsCommon implement. Once name + * hashing is proven to be stable and reliable we'll phase out the old algorithm and the old dependency tracking + * logic. + * + * `srcProd` is a relation between a source file and a product: (source, product). + * Note that some source files may not have a product and will not be included in this relation. + * + * `binaryDeps` is a relation between a source file and a binary dependency: (source, binary dependency). + * This only includes dependencies on classes and jars that do not have a corresponding source/API to track instead. + * A class or jar with a corresponding source should only be tracked in one of the source dependency relations. + * + * `classes` is a relation between a source file and its generated fully-qualified class names. + */ +private abstract class MRelationsCommon(val srcProd: Relation[File, File], val binaryDep: Relation[File, File], + val classes: Relation[File, String]) extends Relations +{ def allSources: collection.Set[File] = srcProd._1s def allProducts: collection.Set[File] = srcProd._2s def allBinaryDeps: collection.Set[File] = binaryDep._2s - def allInternalSrcDeps: collection.Set[File] = direct.internal._2s - def allExternalDeps: collection.Set[String] = direct.external._2s + def allInternalSrcDeps: collection.Set[File] = internalSrcDep._2s + def allExternalDeps: collection.Set[String] = externalDep._2s def classNames(src: File): Set[String] = classes.forward(src) def definesClass(name: String): Set[File] = classes.reverse(name) - + def products(src: File): Set[File] = srcProd.forward(src) def produced(prod: File): Set[File] = srcProd.reverse(prod) - + def binaryDeps(src: File): Set[File] = binaryDep.forward(src) def usesBinary(dep: File): Set[File] = binaryDep.reverse(dep) - - def internalSrcDeps(src: File): Set[File] = direct.internal.forward(src) - def usesInternalSrc(dep: File): Set[File] = direct.internal.reverse(dep) - def externalDeps(src: File): Set[String] = direct.external.forward(src) - def usesExternal(dep: String): Set[File] = direct.external.reverse(dep) + def internalSrcDeps(src: File): Set[File] = internalSrcDep.forward(src) + def usesInternalSrc(dep: File): Set[File] = internalSrcDep.reverse(dep) + + def externalDeps(src: File): Set[String] = externalDep.forward(src) + def usesExternal(dep: String): Set[File] = externalDep.reverse(dep) + + /** Making large Relations a little readable. */ + private val userDir = sys.props("user.dir").stripSuffix("/") + "/" + private def nocwd(s: String) = s stripPrefix userDir + private def line_s(kv: (Any, Any)) = " " + nocwd("" + kv._1) + " -> " + nocwd("" + kv._2) + "\n" + private def relation_s(r: Relation[_, _]) = ( + if (r.forwardMap.isEmpty) "Relation [ ]" + else (r.all.toSeq map line_s sorted) mkString ("Relation [\n", "", "]") + ) + override def toString = ( + """ + |Relations: + | products: %s + | bin deps: %s + | src deps: %s + | ext deps: %s + | class names: %s + """.trim.stripMargin.format(List(srcProd, binaryDep, internalSrcDep, externalDep, classes) map relation_s : _*) + ) +} + + +/** + * This class implements Relations trait with support for tracking of `direct` and `publicInherited` source + * dependencies. Therefore this class preserves the "old" (from sbt 0.13.0) dependency tracking logic and it's + * a default implementation. + * + * `direct` defines relations for dependencies between internal and external source dependencies. It includes all types of + * dependencies, including inheritance. + * + * `publicInherited` defines relations for internal and external source dependencies, only including dependencies + * introduced by inheritance. + * + */ +private class MRelationsDirectAndPublicInherited(srcProd: Relation[File, File], binaryDep: Relation[File, File], + // direct should include everything in inherited + val direct: Source, val publicInherited: Source, + classes: Relation[File, String]) extends MRelationsCommon(srcProd, binaryDep, classes) +{ + def internalSrcDep: Relation[File, File] = direct.internal + def externalDep: Relation[File, String] = direct.external + + def memberRefAndInheritanceDeps: Boolean = false + + def memberRef: SourceDependencies = + throw new UnsupportedOperationException("The `memberRef` source dependencies relation is not supported " + + "when `memberRefAndInheritanceDeps` is disabled. Do you have name hashing algorithm disabled?") + def inheritance: SourceDependencies = + throw new UnsupportedOperationException("The `memberRef` source dependencies relation is not supported " + + "when `memberRefAndInheritanceDeps` is disabled. Do you have name hashing algorithm disabled?") def addProduct(src: File, prod: File, name: String): Relations = - new MRelations( srcProd + (src, prod), binaryDep, direct = direct, publicInherited = publicInherited, classes + (src, name) ) + new MRelationsDirectAndPublicInherited(srcProd + (src, prod), binaryDep, direct = direct, + publicInherited = publicInherited, classes + (src, name)) def addExternalDep(src: File, dependsOn: String, inherited: Boolean): Relations = { val newI = if(inherited) publicInherited.addExternal(src, dependsOn) else publicInherited val newD = direct.addExternal(src, dependsOn) - new MRelations( srcProd, binaryDep, direct = newD, publicInherited = newI, classes ) + new MRelationsDirectAndPublicInherited( srcProd, binaryDep, direct = newD, publicInherited = newI, classes) } - def addInternalSrcDeps(src: File, dependsOn: Iterable[File], inherited: Iterable[File]): Relations = { - val newI = publicInherited.addInternal(src, inherited) - val newD = direct.addInternal(src, dependsOn) - new MRelations( srcProd, binaryDep, direct = newD, publicInherited = newI, classes ) - } + def addInternalSrcDeps(src: File, dependsOn: Iterable[File], inherited: Iterable[File]): Relations = + { + val newI = publicInherited.addInternal(src, inherited) + val newD = direct.addInternal(src, dependsOn) + new MRelationsDirectAndPublicInherited( srcProd, binaryDep, direct = newD, publicInherited = newI, classes) + } def addBinaryDep(src: File, dependsOn: File): Relations = - new MRelations( srcProd, binaryDep + (src, dependsOn), direct = direct, publicInherited = publicInherited, classes ) - def ++ (o: Relations): Relations = - new MRelations(srcProd ++ o.srcProd, binaryDep ++ o.binaryDep, direct = direct ++ o.direct, publicInherited = publicInherited ++ o.publicInherited, classes ++ o.classes) + new MRelationsDirectAndPublicInherited( srcProd, binaryDep + (src, dependsOn), direct = direct, + publicInherited = publicInherited, classes) + + def ++ (o: Relations): Relations = { + if (memberRefAndInheritanceDeps != o.memberRefAndInheritanceDeps) + throw new UnsupportedOperationException("The `++` operation is not supported for relations " + + "with different values of `memberRefAndInheritanceDeps` flag.") + new MRelationsDirectAndPublicInherited(srcProd ++ o.srcProd, binaryDep ++ o.binaryDep, direct ++ o.direct, + publicInherited ++ o.publicInherited, classes ++ o.classes) + } def -- (sources: Iterable[File]) = - new MRelations(srcProd -- sources, binaryDep -- sources, direct = direct -- sources, publicInherited = publicInherited -- sources, classes -- sources) + new MRelationsDirectAndPublicInherited(srcProd -- sources, binaryDep -- sources, direct = direct -- sources, + publicInherited = publicInherited -- sources, classes -- sources) @deprecated("Broken implementation. OK to remove in 0.14", "0.13.1") def groupBy[K](f: File => K): Map[K, Relations] = { type MapRel[T] = Map[K, Relation[File, T]] - def outerJoin(srcProdMap: MapRel[File], binaryDepMap: MapRel[File], direct: Map[K, Source], inherited: Map[K, Source], - classesMap: MapRel[String]): Map[K, Relations] = + def outerJoin(srcProdMap: MapRel[File], binaryDepMap: MapRel[File], direct: Map[K, Source], + inherited: Map[K, Source], classesMap: MapRel[String]): Map[K, Relations] = { def kRelations(k: K): Relations = { def get[T](m: Map[K, Relation[File, T]]) = Relations.getOrEmpty(m, k) def getSrc(m: Map[K, Source]): Source = m.getOrElse(k, Relations.emptySource) - new MRelations( get(srcProdMap), get(binaryDepMap), getSrc(direct), getSrc(inherited), get(classesMap) ) + def getSrcDeps(m: Map[K, SourceDependencies]): SourceDependencies = + m.getOrElse(k, Relations.emptySourceDependencies) + new MRelationsDirectAndPublicInherited( get(srcProdMap), get(binaryDepMap), getSrc(direct), getSrc(inherited), + get(classesMap)) } val keys = (srcProdMap.keySet ++ binaryDepMap.keySet ++ direct.keySet ++ inherited.keySet ++ classesMap.keySet).toList Map( keys.map( (k: K) => (k, kRelations(k)) ) : _*) } def f1[B](item: (File, B)): K = f(item._1) - outerJoin(srcProd.groupBy(f1), binaryDep.groupBy(f1), direct.groupBySource(f), publicInherited.groupBySource(f), classes.groupBy(f1)) - } + + outerJoin(srcProd.groupBy(f1), binaryDep.groupBy(f1), direct.groupBySource(f), + publicInherited.groupBySource(f), classes.groupBy(f1)) + } override def equals(other: Any) = other match { - case o: MRelations => srcProd == o.srcProd && binaryDep == o.binaryDep && direct == o.direct && publicInherited == o.publicInherited && classes == o.classes + case o: MRelationsDirectAndPublicInherited => + srcProd == o.srcProd && binaryDep == o.binaryDep && direct == o.direct && + publicInherited == o.publicInherited && classes == o.classes case _ => false } @@ -258,4 +410,74 @@ private class MRelations(val srcProd: Relation[File, File], val binaryDep: Relat | class names: %s """.trim.stripMargin.format(List(srcProd, binaryDep, internalSrcDep, externalDep, classes) map relation_s : _*) ) -} \ No newline at end of file +} + +/** + * This class implements Relations trait with support for tracking of `memberRef` and `inheritance` source + * dependencies. Therefore this class implements the new (compared to sbt 0.13.0) dependency tracking logic + * needed by the name hashing invalidation algorithm. + */ +private class MRelationsMemberRefAndInheritance(srcProd: Relation[File, File], binaryDep: Relation[File, File], + // memberRef should include everything in inherited + val memberRef: SourceDependencies, val inheritance: SourceDependencies, + classes: Relation[File, String]) extends MRelationsCommon(srcProd, binaryDep, classes) +{ + def direct: Source = + throw new UnsupportedOperationException("The `direct` source dependencies relation is not supported " + + "when `memberRefAndInheritanceDeps` is disabled. Do you have name hashing algorithm disabled?") + def publicInherited: Source = + throw new UnsupportedOperationException("The `publicInherited` source dependencies relation is not supported " + + "when `memberRefAndInheritanceDeps` is disabled. Do you have name hashing algorithm disabled?") + + val memberRefAndInheritanceDeps: Boolean = true + + def internalSrcDep: Relation[File, File] = memberRef.internal + def externalDep: Relation[File, String] = memberRef.external + + def addProduct(src: File, prod: File, name: String): Relations = + new MRelationsMemberRefAndInheritance(srcProd + (src, prod), binaryDep, memberRef = memberRef, + inheritance = inheritance, classes + (src, name)) + + def addExternalDep(src: File, dependsOn: String, inherited: Boolean): Relations = { + val newIH = if(inherited) inheritance.addExternal(src, dependsOn) else inheritance + val newMR = memberRef.addExternal(src, dependsOn) + new MRelationsMemberRefAndInheritance( srcProd, binaryDep, memberRef = newMR, inheritance = newIH, classes) + } + + def addInternalSrcDeps(src: File, dependsOn: Iterable[File], inherited: Iterable[File]): Relations = { + val newIH = inheritance.addInternal(src, inherited) + val newMR = memberRef.addInternal(src, dependsOn) + new MRelationsMemberRefAndInheritance( srcProd, binaryDep, memberRef = newMR, inheritance = newIH, classes) + } + + def addBinaryDep(src: File, dependsOn: File): Relations = + new MRelationsMemberRefAndInheritance(srcProd, binaryDep + (src, dependsOn), memberRef = memberRef, + inheritance = inheritance, classes) + + def ++ (o: Relations): Relations = { + if (!o.memberRefAndInheritanceDeps) + throw new UnsupportedOperationException("The `++` operation is not supported for relations " + + "with different values of `memberRefAndInheritanceDeps` flag.") + new MRelationsMemberRefAndInheritance(srcProd ++ o.srcProd, binaryDep ++ o.binaryDep, + memberRef = memberRef ++ o.memberRef, inheritance = inheritance ++ o.inheritance, + classes ++ o.classes) + } + def -- (sources: Iterable[File]) = + new MRelationsMemberRefAndInheritance(srcProd -- sources, binaryDep -- sources, + memberRef = memberRef -- sources, inheritance = inheritance -- sources, classes -- sources) + + def groupBy[K](f: File => K): Map[K, Relations] = { + throw new UnsupportedOperationException("Merging of Analyses that have" + + "`relations.memberRefAndInheritanceDeps` set to `true` is not supported.") + } + + override def equals(other: Any) = other match { + case o: MRelationsMemberRefAndInheritance => + srcProd == o.srcProd && binaryDep == o.binaryDep && memberRef == o.memberRef && + inheritance == o.inheritance && classes == o.classes + case _ => false + } + + override def hashCode = (srcProd :: binaryDep :: memberRef :: inheritance :: classes :: Nil).hashCode + +} diff --git a/compile/interface/src/main/scala/xsbt/Dependency.scala b/compile/interface/src/main/scala/xsbt/Dependency.scala index 8035574e6..535a6b822 100644 --- a/compile/interface/src/main/scala/xsbt/Dependency.scala +++ b/compile/interface/src/main/scala/xsbt/Dependency.scala @@ -43,8 +43,23 @@ final class Dependency(val global: CallbackGlobal) extends LocateClassFile { // build dependencies structure val sourceFile = unit.source.file.file - for(on <- unit.depends) processDependency(on, inherited=false) - for(on <- inheritedDependencies.getOrElse(sourceFile, Nil: Iterable[Symbol])) processDependency(on, inherited=true) + if (global.callback.memberRefAndInheritanceDeps) { + val dependenciesByMemberRef = extractDependenciesByMemberRef(unit) + for(on <- dependenciesByMemberRef) + processDependency(on, inherited=false) + + val dependenciesByInheritance = extractDependenciesByInheritance(unit) + for(on <- dependenciesByInheritance) + processDependency(on, inherited=true) + } else { + for(on <- unit.depends) processDependency(on, inherited=false) + for(on <- inheritedDependencies.getOrElse(sourceFile, Nil: Iterable[Symbol])) processDependency(on, inherited=true) + } + /** + * Handles dependency on given symbol by trying to figure out if represents a term + * that is coming from either source code (not necessarily compiled in this compilation + * run) or from class file and calls respective callback method. + */ def processDependency(on: Symbol, inherited: Boolean) { def binaryDependency(file: File, className: String) = callback.binaryDependency(file, className, sourceFile, inherited) @@ -64,11 +79,112 @@ final class Dependency(val global: CallbackGlobal) extends LocateClassFile case None => () } } - else + else if (onSource.file != sourceFile) callback.sourceDependency(onSource.file, sourceFile, inherited) } } } } + /** + * Traverses given type and collects result of applying a partial function `pf`. + * + * NOTE: This class exists in Scala 2.10 as CollectTypeCollector but does not in earlier + * versions (like 2.9) of Scala compiler that incremental cmpiler supports so we had to + * reimplement that class here. + */ + private final class CollectTypeTraverser[T](pf: PartialFunction[Type, T]) extends TypeTraverser { + var collected: List[T] = Nil + def traverse(tpe: Type): Unit = { + if (pf.isDefinedAt(tpe)) + collected = pf(tpe) :: collected + mapOver(tpe) + } + } + + private abstract class ExtractDependenciesTraverser extends Traverser { + protected val depBuf = collection.mutable.ArrayBuffer.empty[Symbol] + protected def addDependency(dep: Symbol): Unit = depBuf += dep + def dependencies: collection.immutable.Set[Symbol] = { + // convert to immutable set and remove NoSymbol if we have one + depBuf.toSet - NoSymbol + } + } + + private class ExtractDependenciesByMemberRefTraverser extends ExtractDependenciesTraverser { + override def traverse(tree: Tree): Unit = { + tree match { + case Import(expr, selectors) => + selectors.foreach { + case ImportSelector(nme.WILDCARD, _, null, _) => + // in case of wildcard import we do not rely on any particular name being defined + // on `expr`; all symbols that are being used will get caught through selections + case ImportSelector(name: Name, _, _, _) => + def lookupImported(name: Name) = expr.symbol.info.member(name) + // importing a name means importing both a term and a type (if they exist) + addDependency(lookupImported(name.toTermName)) + addDependency(lookupImported(name.toTypeName)) + } + case select: Select => + addDependency(select.symbol) + /* + * Idents are used in number of situations: + * - to refer to local variable + * - to refer to a top-level package (other packages are nested selections) + * - to refer to a term defined in the same package as an enclosing class; + * this looks fishy, see this thread: + * https://groups.google.com/d/topic/scala-internals/Ms9WUAtokLo/discussion + */ + case ident: Ident => + addDependency(ident.symbol) + case typeTree: TypeTree => + val typeSymbolCollector = new CollectTypeTraverser({ + case tpe if !tpe.typeSymbol.isPackage => tpe.typeSymbol + }) + typeSymbolCollector.traverse(typeTree.tpe) + val deps = typeSymbolCollector.collected.toSet + deps.foreach(addDependency) + case Template(parents, self, body) => + traverseTrees(body) + case other => () + } + super.traverse(tree) + } + } + + private def extractDependenciesByMemberRef(unit: CompilationUnit): collection.immutable.Set[Symbol] = { + val traverser = new ExtractDependenciesByMemberRefTraverser + traverser.traverse(unit.body) + val dependencies = traverser.dependencies + // we capture enclosing classes only because that's what CompilationUnit.depends does and we don't want + // to deviate from old behaviour too much for now + dependencies.map(_.toplevelClass) + } + + /** Copied straight from Scala 2.10 as it does not exist in Scala 2.9 compiler */ + private final def debuglog(msg: => String) { + if (settings.debug.value) + log(msg) + } + + private final class ExtractDependenciesByInheritanceTraverser extends ExtractDependenciesTraverser { + override def traverse(tree: Tree): Unit = tree match { + case Template(parents, self, body) => + // we are using typeSymbol and not typeSymbolDirect because we want + // type aliases to be expanded + val parentTypeSymbols = parents.map(parent => parent.tpe.typeSymbol).toSet + debuglog("Parent type symbols for " + tree.pos + ": " + parentTypeSymbols.map(_.fullName)) + parentTypeSymbols.foreach(addDependency) + traverseTrees(body) + case tree => super.traverse(tree) + } + } + + private def extractDependenciesByInheritance(unit: CompilationUnit): collection.immutable.Set[Symbol] = { + val traverser = new ExtractDependenciesByInheritanceTraverser + traverser.traverse(unit.body) + val dependencies = traverser.dependencies + dependencies.map(_.toplevelClass) + } + } diff --git a/compile/interface/src/test/scala/xsbt/DependencySpecification.scala b/compile/interface/src/test/scala/xsbt/DependencySpecification.scala new file mode 100644 index 000000000..89f465143 --- /dev/null +++ b/compile/interface/src/test/scala/xsbt/DependencySpecification.scala @@ -0,0 +1,112 @@ +package xsbt + +import org.junit.runner.RunWith +import xsbti.api.ClassLike +import xsbti.api.Def +import xsbt.api.SameAPI +import org.specs2.mutable.Specification +import org.specs2.runner.JUnitRunner + +import ScalaCompilerForUnitTesting.ExtractedSourceDependencies + +@RunWith(classOf[JUnitRunner]) +class DependencySpecification extends Specification { + + "Extracted source dependencies from public members" in { + val sourceDependencies = extractSourceDependenciesPublic + val memberRef = sourceDependencies.memberRef + val inheritance = sourceDependencies.inheritance + memberRef('A) === Set.empty + inheritance('A) === Set.empty + memberRef('B) === Set('A, 'D) + inheritance('B) === Set('D) + memberRef('C) === Set('A) + inheritance('C) === Set.empty + memberRef('D) === Set.empty + inheritance('D) === Set.empty + memberRef('E) === Set.empty + inheritance('E) === Set.empty + memberRef('F) === Set('A, 'B, 'C, 'D, 'E) + inheritance('F) === Set('A, 'E) + memberRef('H) === Set('B, 'E, 'G) + // aliases and applied type constructors are expanded so we have inheritance dependency on B + inheritance('H) === Set('B, 'E) + } + + "Extracted source dependencies from private members" in { + val sourceDependencies = extractSourceDependenciesPrivate + val memberRef = sourceDependencies.memberRef + val inheritance = sourceDependencies.inheritance + memberRef('A) === Set.empty + inheritance('A) === Set.empty + memberRef('B) === Set.empty + inheritance('B) === Set.empty + memberRef('C) === Set('A) + inheritance('C) === Set('A) + memberRef('D) === Set('B) + inheritance('D) === Set('B) + } + + "Extracted source dependencies with trait as first parent" in { + val sourceDependencies = extractSourceDependenciesTraitAsFirstPatent + val memberRef = sourceDependencies.memberRef + val inheritance = sourceDependencies.inheritance + memberRef('A) === Set.empty + inheritance('A) === Set.empty + memberRef('B) === Set('A) + inheritance('B) === Set('A) + // verify that memberRef captures the oddity described in documentation of `Relations.inheritance` + // we are mainly interested whether dependency on A is captured in `memberRef` relation so + // the invariant that says that memberRef is superset of inheritance relation is preserved + memberRef('C) === Set('A, 'B) + inheritance('C) === Set('A, 'B) + // same as above but indirect (C -> B -> A), note that only A is visible here + memberRef('D) === Set('A, 'C) + inheritance('D) === Set('A, 'C) + } + + private def extractSourceDependenciesPublic: ExtractedSourceDependencies = { + val srcA = "class A" + val srcB = "class B extends D[A]" + val srcC = """|class C { + | def a: A = null + |}""".stripMargin + val srcD = "class D[T]" + val srcE = "trait E[T]" + val srcF = "trait F extends A with E[D[B]] { self: C => }" + val srcG = "object G { type T[x] = B }" + // T is a type constructor [x]B + // B extends D + // E verifies the core type gets pulled out + val srcH = "trait H extends G.T[Int] with (E[Int] @unchecked)" + + val compilerForTesting = new ScalaCompilerForUnitTesting(memberRefAndInheritanceDeps = true) + val sourceDependencies = compilerForTesting.extractDependenciesFromSrcs('A -> srcA, 'B -> srcB, 'C -> srcC, + 'D -> srcD, 'E -> srcE, 'F -> srcF, 'G -> srcG, 'H -> srcH) + sourceDependencies + } + + private def extractSourceDependenciesPrivate: ExtractedSourceDependencies = { + val srcA = "class A" + val srcB = "class B" + val srcC = "class C { private class Inner1 extends A }" + val srcD = "class D { def foo: Unit = { class Inner2 extends B } }" + + val compilerForTesting = new ScalaCompilerForUnitTesting(memberRefAndInheritanceDeps = true) + val sourceDependencies = + compilerForTesting.extractDependenciesFromSrcs('A -> srcA, 'B -> srcB, 'C -> srcC, 'D -> srcD) + sourceDependencies + } + + private def extractSourceDependenciesTraitAsFirstPatent: ExtractedSourceDependencies = { + val srcA = "class A" + val srcB = "trait B extends A" + val srcC = "trait C extends B" + val srcD = "class D extends C" + + val compilerForTesting = new ScalaCompilerForUnitTesting(memberRefAndInheritanceDeps = true) + val sourceDependencies = + compilerForTesting.extractDependenciesFromSrcs('A -> srcA, 'B -> srcB, 'C -> srcC, 'D -> srcD) + sourceDependencies + } +} diff --git a/compile/interface/src/test/scala/xsbt/ExtractAPISpecification.scala b/compile/interface/src/test/scala/xsbt/ExtractAPISpecification.scala index f9af98966..90b5a5334 100644 --- a/compile/interface/src/test/scala/xsbt/ExtractAPISpecification.scala +++ b/compile/interface/src/test/scala/xsbt/ExtractAPISpecification.scala @@ -17,7 +17,7 @@ class ExtractAPISpecification extends Specification { def stableExistentialNames: Boolean = { def compileAndGetFooMethodApi(src: String): Def = { val compilerForTesting = new ScalaCompilerForUnitTesting - val sourceApi = compilerForTesting.compileSrc(src) + val sourceApi = compilerForTesting.extractApiFromSrc(src) val FooApi = sourceApi.definitions().find(_.name() == "Foo").get.asInstanceOf[ClassLike] val fooMethodApi = FooApi.structure().declared().find(_.name == "foo").get fooMethodApi.asInstanceOf[Def] diff --git a/compile/interface/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala b/compile/interface/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala index e077647e1..61fb08078 100644 --- a/compile/interface/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala +++ b/compile/interface/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala @@ -13,34 +13,90 @@ import xsbti.api.Def import xsbt.api.SameAPI import sbt.ConsoleLogger +import ScalaCompilerForUnitTesting.ExtractedSourceDependencies + /** * Provides common functionality needed for unit tests that require compiling * source code using Scala compiler. */ -class ScalaCompilerForUnitTesting { +class ScalaCompilerForUnitTesting(memberRefAndInheritanceDeps: Boolean = false) { /** * Compiles given source code using Scala compiler and returns API representation * extracted by ExtractAPI class. */ - def compileSrc(src: String): SourceAPI = { - import java.io.FileWriter + def extractApiFromSrc(src: String): SourceAPI = { + val (Seq(tempSrcFile), analysisCallback) = compileSrcs(src) + analysisCallback.apis(tempSrcFile) + } + + /** + * Compiles given source code snippets (passed as Strings) using Scala compiler and returns extracted + * dependencies between snippets. Source code snippets are identified by symbols. Each symbol should + * be associated with one snippet only. + * + * Symbols are used to express extracted dependencies between source code snippets. This way we have + * file system-independent way of testing dependencies between source code "files". + */ + def extractDependenciesFromSrcs(srcs: (Symbol, String)*): ExtractedSourceDependencies = { + val (symbolsForSrcs, rawSrcs) = srcs.unzip + assert(symbolsForSrcs.distinct.size == symbolsForSrcs.size, + s"Duplicate symbols for srcs detected: $symbolsForSrcs") + val (tempSrcFiles, testCallback) = compileSrcs(rawSrcs: _*) + val fileToSymbol = (tempSrcFiles zip symbolsForSrcs).toMap + val memberRefFileDeps = testCallback.sourceDependencies collect { + // false indicates that those dependencies are not introduced by inheritance + case (target, src, false) => (src, target) + } + val inheritanceFileDeps = testCallback.sourceDependencies collect { + // true indicates that those dependencies are introduced by inheritance + case (target, src, true) => (src, target) + } + def toSymbols(src: File, target: File): (Symbol, Symbol) = (fileToSymbol(src), fileToSymbol(target)) + val memberRefDeps = memberRefFileDeps map { case (src, target) => toSymbols(src, target) } + val inheritanceDeps = inheritanceFileDeps map { case (src, target) => toSymbols(src, target) } + def pairsToMultiMap[A, B](pairs: Seq[(A, B)]): Map[A, Set[B]] = { + import scala.collection.mutable.{HashMap, MultiMap} + val emptyMultiMap = new HashMap[A, scala.collection.mutable.Set[B]] with MultiMap[A, B] + val multiMap = pairs.foldLeft(emptyMultiMap) { case (acc, (key, value)) => + acc.addBinding(key, value) + } + // convert all collections to immutable variants + multiMap.toMap.mapValues(_.toSet).withDefaultValue(Set.empty) + } + ExtractedSourceDependencies(pairsToMultiMap(memberRefDeps), pairsToMultiMap(inheritanceDeps)) + } + + /** + * Compiles given source code snippets written to a temporary files. Each snippet is + * written to a separate temporary file. + * + * The sequence of temporary files corresponding to passed snippets and analysis + * callback is returned as a result. + */ + private def compileSrcs(srcs: String*): (Seq[File], TestCallback) = { withTemporaryDirectory { temp => - val analysisCallback = new TestCallback + val analysisCallback = new TestCallback(memberRefAndInheritanceDeps) val classesDir = new File(temp, "classes") classesDir.mkdir() val compiler = prepareCompiler(classesDir, analysisCallback) val run = new compiler.Run - val srcFile = new File(temp, "Test.scala") - srcFile.createNewFile() - val fw = new FileWriter(srcFile) - fw.write(src) - fw.close() - run.compile(List(srcFile.getAbsolutePath())) - analysisCallback.apis(srcFile) + val srcFiles = srcs.toSeq.zipWithIndex map { case (src, i) => + val fileName = s"Test_$i.scala" + prepareSrcFile(temp, fileName, src) + } + val srcFilePaths = srcFiles.map(srcFile => srcFile.getAbsolutePath).toList + run.compile(srcFilePaths) + (srcFiles, analysisCallback) } } + private def prepareSrcFile(baseDir: File, fileName: String, src: String): File = { + val srcFile = new File(baseDir, fileName) + sbt.IO.write(srcFile, src) + srcFile + } + private def prepareCompiler(outputDir: File, analysisCallback: AnalysisCallback): CachedCompiler0#Compiler = { val args = Array.empty[String] object output extends SingleOutput { @@ -69,3 +125,7 @@ class ScalaCompilerForUnitTesting { } } + +object ScalaCompilerForUnitTesting { + case class ExtractedSourceDependencies(memberRef: Map[Symbol, Set[Symbol]], inheritance: Map[Symbol, Set[Symbol]]) +} diff --git a/compile/persist/src/main/scala/sbt/inc/AnalysisFormats.scala b/compile/persist/src/main/scala/sbt/inc/AnalysisFormats.scala index 1416d83f2..5da25c868 100644 --- a/compile/persist/src/main/scala/sbt/inc/AnalysisFormats.scala +++ b/compile/persist/src/main/scala/sbt/inc/AnalysisFormats.scala @@ -13,7 +13,7 @@ package inc import DefaultProtocol._ import DefaultProtocol.tuple2Format import Logger.{m2o, position, problem} - import Relations.{Source => RSource} + import Relations.{Source => RSource, SourceDependencies} @deprecated("Replaced by TextAnalysisFormat. OK to remove in 0.14.", since="0.13.1") object AnalysisFormats @@ -99,12 +99,34 @@ object AnalysisFormats implicit def apisFormat(implicit internalF: Format[Map[File, Source]], externalF: Format[Map[String, Source]]): Format[APIs] = asProduct2( APIs.apply _)( as => (as.internal, as.external) )(internalF, externalF) - implicit def relationsFormat(implicit prodF: Format[RFF], binF: Format[RFF], directF: Format[RSource], inheritedF: Format[RSource], csF: Format[RFS]): Format[Relations] = - asProduct5[Relations, RFF, RFF, RSource, RSource, RFS]( (a,b,c,d,e) => Relations.make(a,b,c,d,e) )( rs => (rs.srcProd, rs.binaryDep, rs.direct, rs.publicInherited, rs.classes) )(prodF, binF, directF, inheritedF, csF) + implicit def relationsFormat(implicit prodF: Format[RFF], binF: Format[RFF], directF: Format[RSource], inheritedF: Format[RSource], memberRefF: Format[SourceDependencies], inheritanceF: Format[SourceDependencies], csF: Format[RFS]): Format[Relations] = + { + def makeRelation(srcProd: RFF, binaryDep: RFF, direct: RSource, publicInherited: RSource, + memberRef: SourceDependencies, inheritance: SourceDependencies, classes: RFS, + memberRefAndInheritanceDeps: Boolean): Relations = if (memberRefAndInheritanceDeps) { + def isEmpty(sourceDependencies: RSource): Boolean = + sourceDependencies.internal.all.isEmpty && sourceDependencies.external.all.isEmpty + // we check direct dependencies only because publicInherited dependencies are subset of direct + assert(isEmpty(direct), "Direct dependencies are not empty but `memberRefAndInheritanceDeps` flag is enabled.") + Relations.make(srcProd, binaryDep, memberRef, inheritance, classes) + } else { + def isEmpty(sourceDependencies: SourceDependencies): Boolean = + sourceDependencies.internal.all.isEmpty && sourceDependencies.external.all.isEmpty + // we check memberRef dependencies only because inheritance dependencies are subset of memberRef + assert(isEmpty(memberRef), "Direct dependencies are not empty but `memberRefAndInheritanceDeps` flag is enabled.") + Relations.make(srcProd, binaryDep, direct, publicInherited, classes) + } + asProduct8[Relations, RFF, RFF, RSource, RSource, SourceDependencies, SourceDependencies, RFS, Boolean]( (a,b,c,d,e,f,g,h) =>makeRelation(a,b,c,d,e,f,g,h) )( + rs => (rs.srcProd, rs.binaryDep, rs.direct, rs.publicInherited, rs.memberRef, rs.inheritance, rs.classes, rs.memberRefAndInheritanceDeps) )( + prodF, binF, directF, inheritedF, memberRefF, inheritanceF, csF, implicitly[Format[Boolean]]) + } implicit def relationsSourceFormat(implicit internalFormat: Format[Relation[File, File]], externalFormat: Format[Relation[File,String]]): Format[RSource] = asProduct2[RSource, RFF, RFS]( (a, b) => Relations.makeSource(a,b))( rs => (rs.internal, rs.external)) + implicit def relationsSourceDependenciesFormat(implicit internalFormat: Format[Relation[File, File]], externalFormat: Format[Relation[File,String]]): Format[SourceDependencies] = + asProduct2[SourceDependencies, RFF, RFS]( (a, b) => Relations.makeSourceDependencies(a,b))( rs => (rs.internal, rs.external)) + implicit def relationFormat[A,B](implicit af: Format[Map[A, Set[B]]], bf: Format[Map[B, Set[A]]]): Format[Relation[A,B]] = asProduct2[Relation[A,B], Map[A, Set[B]], Map[B, Set[A]]]( Relation.make _ )( r => (r.forwardMap, r.reverseMap) )(af, bf) diff --git a/compile/persist/src/main/scala/sbt/inc/TextAnalysisFormat.scala b/compile/persist/src/main/scala/sbt/inc/TextAnalysisFormat.scala index d19cb9f77..8c19fc94a 100644 --- a/compile/persist/src/main/scala/sbt/inc/TextAnalysisFormat.scala +++ b/compile/persist/src/main/scala/sbt/inc/TextAnalysisFormat.scala @@ -80,7 +80,7 @@ object TextAnalysisFormat { } private[this] object VersionF { - val currentVersion = "1" + val currentVersion = "2" def write(out: Writer) { out.write("format version: %s\n".format(currentVersion)) @@ -107,11 +107,16 @@ object TextAnalysisFormat { object Headers { val srcProd = "products" val binaryDep = "binary dependencies" - val internalSrcDep = "source dependencies" - val externalDep = "external dependencies" + val directSrcDep = "direct source dependencies" + val directExternalDep = "direct external dependencies" val internalSrcDepPI = "public inherited source dependencies" val externalDepPI = "public inherited external dependencies" val classes = "class names" + + val memberRefInternalDep = "member reference internal dependencies" + val memberRefExternalDep = "member reference external dependencies" + val inheritanceInternalDep = "inheritance internal dependencies" + val inheritanceExternalDep = "inheritance external dependencies" } def write(out: Writer, relations: Relations) { @@ -129,12 +134,29 @@ object TextAnalysisFormat { } } + val memberRefAndInheritanceDeps = relations.memberRefAndInheritanceDeps writeRelation(Headers.srcProd, relations.srcProd) writeRelation(Headers.binaryDep, relations.binaryDep) - writeRelation(Headers.internalSrcDep, relations.internalSrcDep) - writeRelation(Headers.externalDep, relations.externalDep) - writeRelation(Headers.internalSrcDepPI, relations.publicInherited.internal) - writeRelation(Headers.externalDepPI, relations.publicInherited.external) + + val direct = if (memberRefAndInheritanceDeps) Relations.emptySource else relations.direct + val publicInherited = if (memberRefAndInheritanceDeps) + Relations.emptySource else relations.publicInherited + + val memberRef = if (memberRefAndInheritanceDeps) + relations.memberRef else Relations.emptySourceDependencies + val inheritance = if (memberRefAndInheritanceDeps) + relations.inheritance else Relations.emptySourceDependencies + + writeRelation(Headers.directSrcDep, direct.internal) + writeRelation(Headers.directExternalDep, direct.external) + writeRelation(Headers.internalSrcDepPI, publicInherited.internal) + writeRelation(Headers.externalDepPI, publicInherited.external) + + writeRelation(Headers.memberRefInternalDep, memberRef.internal) + writeRelation(Headers.memberRefExternalDep, memberRef.external) + writeRelation(Headers.inheritanceInternalDep, inheritance.internal) + writeRelation(Headers.inheritanceExternalDep, inheritance.external) + writeRelation(Headers.classes, relations.classes) } @@ -164,14 +186,40 @@ object TextAnalysisFormat { val srcProd = readFileRelation(Headers.srcProd) val binaryDep = readFileRelation(Headers.binaryDep) - val internalSrcDep = readFileRelation(Headers.internalSrcDep) - val externalDep = readStringRelation(Headers.externalDep) - val internalSrcDepPI = readFileRelation(Headers.internalSrcDepPI) - val externalDepPI = readStringRelation(Headers.externalDepPI) + + import sbt.inc.Relations.{Source, SourceDependencies, makeSourceDependencies, emptySource, + makeSource, emptySourceDependencies} + val directSrcDeps: Source = { + val internalSrcDep = readFileRelation(Headers.directSrcDep) + val externalDep = readStringRelation(Headers.directExternalDep) + makeSource(internalSrcDep, externalDep) + } + val publicInheritedSrcDeps: Source = { + val internalSrcDepPI = readFileRelation(Headers.internalSrcDepPI) + val externalDepPI = readStringRelation(Headers.externalDepPI) + makeSource(internalSrcDepPI, externalDepPI) + } + val memberRefSrcDeps: SourceDependencies = { + val internalMemberRefDep = readFileRelation(Headers.memberRefInternalDep) + val externalMemberRefDep = readStringRelation(Headers.memberRefExternalDep) + makeSourceDependencies(internalMemberRefDep, externalMemberRefDep) + } + val inheritanceSrcDeps: SourceDependencies = { + val internalInheritanceDep = readFileRelation(Headers.inheritanceInternalDep) + val externalInheritanceDep = readStringRelation(Headers.inheritanceExternalDep) + makeSourceDependencies(internalInheritanceDep, externalInheritanceDep) + } + // we don't check for emptiness of publicInherited/inheritance relations because + // we assume that invariant that says they are subsets of direct/memberRef holds + assert((directSrcDeps == emptySource) || (memberRefSrcDeps == emptySourceDependencies), + "One mechanism is supported for tracking source dependencies at the time") + val memberRefAndInheritanceDeps = memberRefSrcDeps != emptySourceDependencies val classes = readStringRelation(Headers.classes) - Relations.make(srcProd, binaryDep, Relations.makeSource(internalSrcDep, externalDep), - Relations.makeSource(internalSrcDepPI, externalDepPI), classes) + if (memberRefAndInheritanceDeps) + Relations.make(srcProd, binaryDep, memberRefSrcDeps, inheritanceSrcDeps, classes) + else + Relations.make(srcProd, binaryDep, directSrcDeps, publicInheritedSrcDeps, classes) } } diff --git a/interface/src/main/java/xsbti/AnalysisCallback.java b/interface/src/main/java/xsbti/AnalysisCallback.java index 55a90f011..ff239ae74 100644 --- a/interface/src/main/java/xsbti/AnalysisCallback.java +++ b/interface/src/main/java/xsbti/AnalysisCallback.java @@ -27,4 +27,16 @@ public interface AnalysisCallback /** Provides problems discovered during compilation. These may be reported (logged) or unreported. * Unreported problems are usually unreported because reporting was not enabled via a command line switch. */ public void problem(String what, Position pos, String msg, Severity severity, boolean reported); + /** + * Determines whether member reference and inheritance dependencies should be extracted in given compiler + * run. + * + * As the signature suggests, this method's implementation is meant to be side-effect free. It's added + * to AnalysisCallback because it indicates how other callback calls should be interpreted by both + * implementation of AnalysisCallback and it's clients. + * + * NOTE: This method is an implementation detail and can be removed at any point without deprecation. + * Do not depend on it, please. + */ + public boolean memberRefAndInheritanceDeps(); } \ No newline at end of file diff --git a/interface/src/test/scala/xsbti/TestCallback.scala b/interface/src/test/scala/xsbti/TestCallback.scala index f8454336a..28bee5466 100644 --- a/interface/src/test/scala/xsbti/TestCallback.scala +++ b/interface/src/test/scala/xsbti/TestCallback.scala @@ -4,7 +4,7 @@ import java.io.File import scala.collection.mutable.ArrayBuffer import xsbti.api.SourceAPI -class TestCallback extends AnalysisCallback +class TestCallback(override val memberRefAndInheritanceDeps: Boolean = false) extends AnalysisCallback { val sourceDependencies = new ArrayBuffer[(File, File, Boolean)] val binaryDependencies = new ArrayBuffer[(File, String, File, Boolean)] diff --git a/sbt/src/sbt-test/source-dependencies/inherited-dependencies/build.sbt b/sbt/src/sbt-test/source-dependencies/inherited-dependencies/build.sbt index 15a6a4dba..62479a549 100644 --- a/sbt/src/sbt-test/source-dependencies/inherited-dependencies/build.sbt +++ b/sbt/src/sbt-test/source-dependencies/inherited-dependencies/build.sbt @@ -2,25 +2,31 @@ lazy val verifyDeps = taskKey[Unit]("verify inherited dependencies are properly verifyDeps := { val a = compile.in(Compile).value - same(a.relations.publicInherited.internal.forwardMap, expectedDeps.forwardMap) -} - -lazy val expected = Seq( - "A" -> Seq("C", "D", "E", "G", "J"), - "B" -> Seq(), - "C" -> Seq("D", "G", "J"), - "D" -> Seq("G", "J"), - "E" -> Seq(), - "F" -> Seq("C", "D", "G", "J"), - "G" -> Seq("J"), - "J" -> Seq() -) -lazy val pairs = - expected.map { case (from,tos) => - (toFile(from), tos.map(toFile)) + val baseDir = baseDirectory.value + def relative(f: java.io.File): java.io.File = f.relativeTo(baseDir) getOrElse f + def toFile(s: String) = relative(baseDir / (s + ".scala")) + def inheritedDeps(name: String): Set[File] = { + val file = (baseDir / (name + ".scala")).getAbsoluteFile + val absoluteFiles = a.relations.publicInherited.internal.forward(file) + absoluteFiles.map(relative) } -lazy val expectedDeps = (Relation.empty[File,File] /: pairs) { case (r, (x,ys)) => r + (x,ys) } -def toFile(s: String) = file(s + ".scala").getAbsoluteFile + val ADeps = Set("C", "D", "E", "G", "J").map(toFile) + same(inheritedDeps("A"), ADeps) + val BDeps = Set.empty[File] + same(inheritedDeps("B"), BDeps) + val CDeps = Set("D", "G", "J").map(toFile) + same(inheritedDeps("C"), CDeps) + val DDeps = Set("G", "J").map(toFile) + same(inheritedDeps("D"), DDeps) + val EDeps = Set.empty[File] + same(inheritedDeps("E"), EDeps) + val FDeps = Set("C", "D", "G", "J").map(toFile) + same(inheritedDeps("F"), FDeps) + val GDeps = Set("J").map(toFile) + same(inheritedDeps("G"), GDeps) + val JDeps = Set.empty[File] + same(inheritedDeps("J"), JDeps) +} def same[T](x: T, y: T) { assert(x == y, s"\nActual: $x, \nExpected: $y") diff --git a/sbt/src/sbt-test/source-dependencies/typeref-only/A.scala b/sbt/src/sbt-test/source-dependencies/typeref-only/A.scala new file mode 100644 index 000000000..3b274e4a8 --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/typeref-only/A.scala @@ -0,0 +1,5 @@ +class A[T] + +abstract class C { + def foo: A[B] +} diff --git a/sbt/src/sbt-test/source-dependencies/typeref-only/B.scala b/sbt/src/sbt-test/source-dependencies/typeref-only/B.scala new file mode 100644 index 000000000..179f0d275 --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/typeref-only/B.scala @@ -0,0 +1 @@ +class B diff --git a/sbt/src/sbt-test/source-dependencies/typeref-only/build.sbt b/sbt/src/sbt-test/source-dependencies/typeref-only/build.sbt new file mode 100644 index 000000000..02813797f --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/typeref-only/build.sbt @@ -0,0 +1,5 @@ +logLevel := Level.Debug + +// disable recompile all which causes full recompile which +// makes it more difficult to test dependency tracking +incOptions ~= { _.copy(recompileAllFraction = 1.0) } diff --git a/sbt/src/sbt-test/source-dependencies/typeref-only/test b/sbt/src/sbt-test/source-dependencies/typeref-only/test new file mode 100644 index 000000000..fb314fd7b --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/typeref-only/test @@ -0,0 +1,7 @@ +# test case for dependency tracking in case given type (`B` in our case) +# mentioned only in type ref (as a type argument) +> compile + +$ delete B.scala + +-> compile