From a9a15748cc757766a6a9ce582518b654f4a8fc82 Mon Sep 17 00:00:00 2001 From: Grzegorz Kossakowski Date: Tue, 2 Jul 2013 11:33:36 -0700 Subject: [PATCH 1/9] Improve failure message in `inherited-dependencies` test. When the `source-dependencies/inherited-dependencies` test fails we get a dump of a big collection of all dependencies with absolute file paths printed. This is not very readable when one needs to understand the actual difference. I decided to test dependencies of each source file separately. This way when assertion exception is thrown we get a stack trace that points us at the line which tested dependencies of a specific source file. Also, all files are relative to baseDirectory of the project. --- .../inherited-dependencies/build.sbt | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) 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") From b8371691f2b45c6c22b3926c9a9e03c3932ccb97 Mon Sep 17 00:00:00 2001 From: Grzegorz Kossakowski Date: Tue, 26 Nov 2013 18:39:13 +0100 Subject: [PATCH 2/9] Introduce `memberRef` and `inheritance` relations. Introduce an alternative source dependency tracking mechanism that is needed by upcoming name hashing algorithm. This new mechanism is implemented by introducing two new source dependency relations called `memberRef` and `inheritance`. Those relations are very similar to existing `direct` and `publicInherited` relations in some subtle ways. Those differences will be highlighted in the description below. Dependencies between source files are tracked in two distinct categories: * dependencies introduced by inheriting from a class/trait defined in other source file * dependencies introduced by referring (selecting) a member defined in other source file (that covers all other kinds of dependencies) Due to invalidation algorithm implementation details sbt would need to track inheritance dependencies of public classes only. Thus, we had relation called `publicInherited`. The name hashing algorithm which improves invalidation logic will need more precise information about dependencies introduced by inheritance including dependencies of non-public classes. That's one difference between `inheritance` and `publicInherited` relations. One surprising (to me) thing about `publicInherited` is that it includes all base classes of a given class and not just parents. In that sense `publicInherited` is transitive. This is a bit irregular because everything else in Relations doesn't include transitive dependencies. Since we are introducing new relations we have an excellent chance to make things more regular. Therefore `inheritance` relation is non-transitive and includes only extracted parent classes. The access to `direct`, `publicInherited`, `memberRef` and `inheritance` relations is dependent upon the value of `memberRefAndInheritanceDeps` flag. Check documentation of that flag for details. The two alternatives for source dependency tracking are implemented by introduction of two subclasses that implement Relations trait and one abstract class that contains some common logic shared between those two subclasses. The two new subclasses are needed for the time being when we are slowly migrating to the name hashing algorithm which requires subtle changes to dependency tracking as explained above. 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. --- .../inc/src/main/scala/sbt/inc/Analysis.scala | 10 +- .../src/main/scala/sbt/inc/Relations.scala | 342 +++++++++++++++--- .../main/scala/sbt/inc/AnalysisFormats.scala | 28 +- .../scala/sbt/inc/TextAnalysisFormat.scala | 74 +++- 4 files changed, 377 insertions(+), 77 deletions(-) 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/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/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) } } From aac19fd02be94f4ef6ba98187c9cbbc2b66a60f9 Mon Sep 17 00:00:00 2001 From: Grzegorz Kossakowski Date: Tue, 19 Nov 2013 21:16:06 +0100 Subject: [PATCH 3/9] Extract source code dependencies by tree walking. Previously incremental compiler was extracting source code dependencies by inspecting `CompilationUnit.depends` set. This set is constructed by Scala compiler and it contains all symbols that given compilation unit refers or even saw (in case of implicit search). There are a few problems with this approach: * The contract for `CompilationUnit.depend` is not clearly defined in Scala compiler and there are no tests around it. Read: it's not an official, maintained API. * Improvements to incremental compiler require more context information about given dependency. For example, we want to distinguish between dependency on a class when you just select members from it or inherit from it. The other example is that we might want to know dependencies of a given class instead of the whole compilation unit to make the invalidation logic more precise. That led to the idea of pushing dependency extracting logic to incremental compiler side so it can evolve indepedently from Scala compiler releases and can be refined as needed. We extract dependencies of a compilation unit by walking a type-checked tree and gathering symbols attached to them. Specifically, the tree walk is implemented as a separate phase that runs after pickler and extracts symbols from following tree nodes: * `Import` so we can track dependencies on unused imports * `Select` which is used for selecting all terms * `Ident` used for referring to local terms, package-local terms and top-level packages * `TypeTree` which is used for referring to all types Note that we do not extract just a single symbol assigned to `TypeTree` node because it might represent a complex type that mentions several symbols. We collect all those symbols by traversing the type with CollectTypeTraverser. The implementation of the traverser is inspired by `CollectTypeCollector` from Scala 2.10. The `source-dependencies/typeref-only` test covers a scenario where the dependency is introduced through a TypeRef only. --- .../inc/src/main/scala/sbt/inc/Compile.scala | 2 + .../src/main/scala/xsbt/Dependency.scala | 120 +++++++++++++++++- .../src/main/java/xsbti/AnalysisCallback.java | 12 ++ .../src/test/scala/xsbti/TestCallback.scala | 2 +- .../source-dependencies/typeref-only/A.scala | 5 + .../source-dependencies/typeref-only/B.scala | 1 + .../typeref-only/build.sbt | 5 + .../source-dependencies/typeref-only/test | 7 + 8 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 sbt/src/sbt-test/source-dependencies/typeref-only/A.scala create mode 100644 sbt/src/sbt-test/source-dependencies/typeref-only/B.scala create mode 100644 sbt/src/sbt-test/source-dependencies/typeref-only/build.sbt create mode 100644 sbt/src/sbt-test/source-dependencies/typeref-only/test diff --git a/compile/inc/src/main/scala/sbt/inc/Compile.scala b/compile/inc/src/main/scala/sbt/inc/Compile.scala index 7b36b8b97..76ca59b8b 100644 --- a/compile/inc/src/main/scala/sbt/inc/Compile.scala +++ b/compile/inc/src/main/scala/sbt/inc/Compile.scala @@ -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/interface/src/main/scala/xsbt/Dependency.scala b/compile/interface/src/main/scala/xsbt/Dependency.scala index 8035574e6..907f62419 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) @@ -71,4 +86,105 @@ final class Dependency(val global: CallbackGlobal) extends LocateClassFile } } + /** + * 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/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/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 From 89914975e102bbffee7034421bd3b0628a40415e Mon Sep 17 00:00:00 2001 From: Grzegorz Kossakowski Date: Tue, 19 Nov 2013 22:34:05 +0100 Subject: [PATCH 4/9] Refactor ScalaCompilerForUnitTesting. Refactor ScalaCompilerForUnitTesting by introducing a new method `extractApiFromSrc` which better describes the intent than `compileSrc`. The `compileSrc` becomes a private, utility method. Also, `compileSrc` method changed it's signature so it can take multiple source code snippets as input. This functionality will be used in future commits. --- .../scala/xsbt/ExtractAPISpecification.scala | 2 +- .../xsbt/ScalaCompilerForUnitTesting.scala | 39 ++++++++++++++----- 2 files changed, 31 insertions(+), 10 deletions(-) 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..8b4d67d23 100644 --- a/compile/interface/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala +++ b/compile/interface/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala @@ -23,24 +23,45 @@ class ScalaCompilerForUnitTesting { * 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 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 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 = { + import java.io.FileWriter + val srcFile = new File(baseDir, fileName) + srcFile.createNewFile() + val fw = new FileWriter(srcFile) + fw.write(src) + fw.close() + srcFile + } + private def prepareCompiler(outputDir: File, analysisCallback: AnalysisCallback): CachedCompiler0#Compiler = { val args = Array.empty[String] object output extends SingleOutput { From de1c5a4aedd27bb880e33e519d636e2607b35412 Mon Sep 17 00:00:00 2001 From: Grzegorz Kossakowski Date: Sun, 24 Nov 2013 23:27:54 +0100 Subject: [PATCH 5/9] Add support for unit testing of extracted source dependencies. Add `extractDependenciesFromSrcs` method to ScalaCompilerForUnitTest class which allows us to unit test dependency extraction logic. See the comment attached to the method that explain the details of how it should be used. --- .../xsbt/ScalaCompilerForUnitTesting.scala | 53 ++++++++++++++++--- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/compile/interface/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala b/compile/interface/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala index 8b4d67d23..61fb08078 100644 --- a/compile/interface/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala +++ b/compile/interface/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala @@ -13,11 +13,13 @@ 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 @@ -28,6 +30,43 @@ class ScalaCompilerForUnitTesting { 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. @@ -37,7 +76,7 @@ class ScalaCompilerForUnitTesting { */ 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) @@ -53,12 +92,8 @@ class ScalaCompilerForUnitTesting { } private def prepareSrcFile(baseDir: File, fileName: String, src: String): File = { - import java.io.FileWriter val srcFile = new File(baseDir, fileName) - srcFile.createNewFile() - val fw = new FileWriter(srcFile) - fw.write(src) - fw.close() + sbt.IO.write(srcFile, src) srcFile } @@ -90,3 +125,7 @@ class ScalaCompilerForUnitTesting { } } + +object ScalaCompilerForUnitTesting { + case class ExtractedSourceDependencies(memberRef: Map[Symbol, Set[Symbol]], inheritance: Map[Symbol, Set[Symbol]]) +} From 2551eb2a63100b7b250e782f746c74313228a54f Mon Sep 17 00:00:00 2001 From: Grzegorz Kossakowski Date: Thu, 21 Nov 2013 01:45:42 +0100 Subject: [PATCH 6/9] Do not add source dependencies on itself. Adding source dependency on itself doesn't really bring any value so there's no reason to do it. We avoided recording that kind of dependencies by performing a check in `AnalysisCallback` implementation. However, if we have another implementation like `TestCallback` used for testing we do not benefit from that check. Therefore, the check has been moved to dependency phase were dependencies are collected. --- compile/inc/src/main/scala/sbt/inc/Compile.scala | 2 +- compile/interface/src/main/scala/xsbt/Dependency.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compile/inc/src/main/scala/sbt/inc/Compile.scala b/compile/inc/src/main/scala/sbt/inc/Compile.scala index 76ca59b8b..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) } diff --git a/compile/interface/src/main/scala/xsbt/Dependency.scala b/compile/interface/src/main/scala/xsbt/Dependency.scala index 907f62419..535a6b822 100644 --- a/compile/interface/src/main/scala/xsbt/Dependency.scala +++ b/compile/interface/src/main/scala/xsbt/Dependency.scala @@ -79,7 +79,7 @@ final class Dependency(val global: CallbackGlobal) extends LocateClassFile case None => () } } - else + else if (onSource.file != sourceFile) callback.sourceDependency(onSource.file, sourceFile, inherited) } } From 533a5b8c238ff9b14043d3e2f178183bdade0496 Mon Sep 17 00:00:00 2001 From: Grzegorz Kossakowski Date: Sun, 24 Nov 2013 23:53:09 +0100 Subject: [PATCH 7/9] Add specification for extracted source dependencies. Add specs2 specification (unit test) which documents current dependency extraction logic's behavior. It exercises `direct` and `publicInherited` relations. This test is akin to `source-dependencies/inherited-dependencies` scripted test. We keep both because this test will diverge in next commit to test `memberRef` and `inheritance` relations. The idea behind adding this test and then modifying the `memberRefAndInheritanceDeps` flag so we test `memberRef` and `inheritance` is that we can show precisely the differences between those two dependency tracking mechanisms. --- .../scala/xsbt/DependencySpecification.scala | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 compile/interface/src/test/scala/xsbt/DependencySpecification.scala 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..81c930430 --- /dev/null +++ b/compile/interface/src/test/scala/xsbt/DependencySpecification.scala @@ -0,0 +1,82 @@ +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, 'C, 'E) + memberRef('H) === Set('G, 'E) + // aliases and applied type constructors are expanded so we have inheritance dependency on B + inheritance('H) === Set('D, 'E, 'B) + } + + "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.empty + memberRef('D) === Set('B) + inheritance('D) === Set.empty + } + + 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 = false) + 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 = false) + val sourceDependencies = + compilerForTesting.extractDependenciesFromSrcs('A -> srcA, 'B -> srcB, 'C -> srcC, 'D -> srcD) + sourceDependencies + } +} From 2226fccd4f0cc01eacdcf62190d3631dcb82446e Mon Sep 17 00:00:00 2001 From: Grzegorz Kossakowski Date: Mon, 25 Nov 2013 00:15:01 +0100 Subject: [PATCH 8/9] Test `memberRef` and `inheritance` in DependencySpecification. Flip `memberRefAndInheritanceDeps` flag to true which allows us to test `memberRef` and `inheritance` relations instead of `direct` and `publicInherited` as it was previously done. There a few changes to extracted dependencies from public members: * F doesn't depend on C by inheritance anymore. The dependency on C was coming from self type. This shows that dependencies from self types are not considered to be dependencies introduces by inheritance anymore. * G depends on B by member reference now. This dependency is introduced by applying type constructor `G.T` and expanding the result of the application. * H doesn't depend on D by inheritance anymore. That dependency was introduced through B which inherits from D. This shows that only parents (and not all base classes) are included in `inheritance` relation. NOTE: The second bullet highlights a bug in the old dependency tracking logic. The dependency on B was recorded in `publicInherited` but not in `direct` relation. This breaks the contract which says that `publicInherited` is a subset of `direct` relation. This a change to dependencies extracted from non-public members: * C depends on A by inheritance and D depends on B by inheritance now; both changes are of the same kind: dependencies introduced by inheritance are tracked for non-public members now. This is necessary for name hashing correctness algorithm --- .../test/scala/xsbt/DependencySpecification.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/compile/interface/src/test/scala/xsbt/DependencySpecification.scala b/compile/interface/src/test/scala/xsbt/DependencySpecification.scala index 81c930430..f2dd08114 100644 --- a/compile/interface/src/test/scala/xsbt/DependencySpecification.scala +++ b/compile/interface/src/test/scala/xsbt/DependencySpecification.scala @@ -27,10 +27,10 @@ class DependencySpecification extends Specification { memberRef('E) === Set.empty inheritance('E) === Set.empty memberRef('F) === Set('A, 'B, 'C, 'D, 'E) - inheritance('F) === Set('A, 'C, 'E) - memberRef('H) === Set('G, '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('D, 'E, 'B) + inheritance('H) === Set('B, 'E) } "Extracted source dependencies from private members" in { @@ -42,9 +42,9 @@ class DependencySpecification extends Specification { memberRef('B) === Set.empty inheritance('B) === Set.empty memberRef('C) === Set('A) - inheritance('C) === Set.empty + inheritance('C) === Set('A) memberRef('D) === Set('B) - inheritance('D) === Set.empty + inheritance('D) === Set('B) } private def extractSourceDependenciesPublic: ExtractedSourceDependencies = { @@ -62,7 +62,7 @@ class DependencySpecification extends Specification { // 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 = false) + 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 @@ -74,7 +74,7 @@ class DependencySpecification extends Specification { 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 = false) + val compilerForTesting = new ScalaCompilerForUnitTesting(memberRefAndInheritanceDeps = true) val sourceDependencies = compilerForTesting.extractDependenciesFromSrcs('A -> srcA, 'B -> srcB, 'C -> srcC, 'D -> srcD) sourceDependencies From 331fffbb194acac5ef93298064a5936f2e102727 Mon Sep 17 00:00:00 2001 From: Grzegorz Kossakowski Date: Tue, 26 Nov 2013 18:33:58 +0100 Subject: [PATCH 9/9] Add test for trait as a first parent scenario in dep tracking. The documentation of `Relations.inheritance` mentions an oddity of Scala's type checker which manifests itself in what is being tracked by that relation in case of traits being first parent for a class/trait. Add a test case which verifies that this oddity actually exists and it's not harmful because it doesn't break an invariant between `memberRef` and `inheritance` relations. --- .../scala/xsbt/DependencySpecification.scala | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/compile/interface/src/test/scala/xsbt/DependencySpecification.scala b/compile/interface/src/test/scala/xsbt/DependencySpecification.scala index f2dd08114..89f465143 100644 --- a/compile/interface/src/test/scala/xsbt/DependencySpecification.scala +++ b/compile/interface/src/test/scala/xsbt/DependencySpecification.scala @@ -47,6 +47,24 @@ class DependencySpecification extends Specification { 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]" @@ -79,4 +97,16 @@ class DependencySpecification extends Specification { 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 + } }