diff --git a/compile/inc/src/main/scala/sbt/inc/Compile.scala b/compile/inc/src/main/scala/sbt/inc/Compile.scala index e37e84cd4..2d325a4d9 100644 --- a/compile/inc/src/main/scala/sbt/inc/Compile.scala +++ b/compile/inc/src/main/scala/sbt/inc/Compile.scala @@ -67,6 +67,7 @@ private final class AnalysisCallback(internalMap: File => Option[File], external import collection.mutable.{HashMap, HashSet, ListBuffer, Map, Set} private[this] val apis = new HashMap[File, (Int, SourceAPI)] + private[this] val usedNames = new HashMap[File, Set[String]] private[this] val unreporteds = new HashMap[File, ListBuffer[Problem]] private[this] val reporteds = new HashMap[File, ListBuffer[Problem]] private[this] val binaryDeps = new HashMap[File, Set[File]] @@ -151,9 +152,11 @@ private final class AnalysisCallback(internalMap: File => Option[File], external apis(sourceFile) = (HashAPI(source), savedSource) } + def usedName(sourceFile: File, name: String) = add(usedNames, sourceFile, name) + def nameHashing: Boolean = false // TODO: define the flag in IncOptions which controls this - def get: Analysis = addCompilation( addExternals( addBinaries( addProducts( addSources(Analysis.Empty) ) ) ) ) + def get: Analysis = addUsedNames( 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) ) def addSources(base: Analysis): Analysis = @@ -171,6 +174,9 @@ private final class AnalysisCallback(internalMap: File => Option[File], external def getOrNil[A,B](m: collection.Map[A, Seq[B]], a: A): Seq[B] = m.get(a).toList.flatten def addExternals(base: Analysis): Analysis = (base /: extSrcDeps) { case (a, (source, name, api, inherited)) => a.addExternalDep(source, name, api, inherited) } def addCompilation(base: Analysis): Analysis = base.copy(compilations = base.compilations.add(compilation)) + def addUsedNames(base: Analysis): Analysis = (base /: usedNames) { case (a, (src, names)) => + (a /: names) { case (a, name) => a.copy(relations = a.relations.addUsedName(src, name)) } + } def addAll[A,B](base: Analysis, m: Map[A, Set[B]])( f: (Analysis, A, B) => Analysis): Analysis = (base /: m) { case (outer, (a, bs)) => diff --git a/compile/inc/src/main/scala/sbt/inc/Relations.scala b/compile/inc/src/main/scala/sbt/inc/Relations.scala index b41804ec2..b03b519e0 100644 --- a/compile/inc/src/main/scala/sbt/inc/Relations.scala +++ b/compile/inc/src/main/scala/sbt/inc/Relations.scala @@ -58,6 +58,8 @@ trait Relations /** Internal source dependencies that depend on external source file `dep`. This includes both direct and inherited dependencies. */ def usesExternal(dep: String): Set[File] + private[inc] def usedNames(src: File): Set[String] + /** Records internal source file `src` as generating class file `prod` with top-level class `name`. */ def addProduct(src: File, prod: File, name: String): Relations @@ -74,6 +76,8 @@ trait Relations * this method does not automatically record direct dependencies like `addExternalDep` does.*/ def addInternalSrcDeps(src: File, directDependsOn: Iterable[File], inheritedDependsOn: Iterable[File]): Relations + private[inc] def addUsedName(src: File, name: String): Relations + /** Concatenates the two relations. Acts naively, i.e., doesn't internalize external deps on added files. */ def ++ (o: Relations): Relations @@ -163,6 +167,10 @@ trait Relations * relations is illegal and will cause runtime exception being thrown. */ private[inc] def nameHashing: Boolean + /** + * Relation between source files and _unqualified_ term and type names used in given source file. + */ + private[inc] def names: Relation[File, String] } @@ -221,7 +229,7 @@ object Relations def empty: Relations = empty(nameHashing = false) private[inc] def empty(nameHashing: Boolean): Relations = if (nameHashing) - new MRelationsNameHashing(e, e, emptySourceDependencies, emptySourceDependencies, estr) + new MRelationsNameHashing(e, e, emptySourceDependencies, emptySourceDependencies, estr, estr) else new MRelationsDefaultImpl(e, e, es, es, estr) @@ -229,8 +237,10 @@ object Relations new MRelationsDefaultImpl(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 MRelationsNameHashing(srcProd, binaryDep, memberRef = memberRef, inheritance = inheritance, classes) + memberRef: SourceDependencies, inheritance: SourceDependencies, classes: Relation[File, String], + names: Relation[File, String]): Relations = + new MRelationsNameHashing(srcProd, binaryDep, memberRef = memberRef, inheritance = inheritance, + classes, names) 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) } @@ -281,6 +291,8 @@ private abstract class MRelationsCommon(val srcProd: Relation[File, File], val b def externalDeps(src: File): Set[String] = externalDep.forward(src) def usesExternal(dep: String): Set[File] = externalDep.reverse(dep) + def usedNames(src: File): Set[String] = names.forward(src) + /** Making large Relations a little readable. */ private val userDir = sys.props("user.dir").stripSuffix("/") + "/" private def nocwd(s: String) = s stripPrefix userDir @@ -348,6 +360,14 @@ private class MRelationsDefaultImpl(srcProd: Relation[File, File], binaryDep: Re new MRelationsDefaultImpl( srcProd, binaryDep, direct = newD, publicInherited = newI, classes) } + def names: Relation[File, String] = + throw new UnsupportedOperationException("Tracking of used names is not supported " + + "when `nameHashing` is disabled.") + + def addUsedName(src: File, name: String): Relations = + throw new UnsupportedOperationException("Tracking of used names is not supported " + + "when `nameHashing` is disabled.") + def addBinaryDep(src: File, dependsOn: File): Relations = new MRelationsDefaultImpl( srcProd, binaryDep + (src, dependsOn), direct = direct, publicInherited = publicInherited, classes) @@ -368,7 +388,8 @@ private class MRelationsDefaultImpl(srcProd: Relation[File, File], binaryDep: Re { 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] = + inherited: Map[K, Source], classesMap: MapRel[String], + namesMap: MapRel[String]): Map[K, Relations] = { def kRelations(k: K): Relations = { def get[T](m: Map[K, Relation[File, T]]) = Relations.getOrEmpty(m, k) @@ -385,7 +406,7 @@ private class MRelationsDefaultImpl(srcProd: Relation[File, File], binaryDep: Re 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)) + publicInherited.groupBySource(f), classes.groupBy(f1), names.groupBy(f1)) } override def equals(other: Any) = other match { @@ -413,7 +434,8 @@ private class MRelationsDefaultImpl(srcProd: Relation[File, File], binaryDep: Re | src deps: %s | ext deps: %s | class names: %s - """.trim.stripMargin.format(List(srcProd, binaryDep, internalSrcDep, externalDep, classes) map relation_s : _*) + | used names: %s + """.trim.stripMargin.format(List(srcProd, binaryDep, internalSrcDep, externalDep, classes, names) map relation_s : _*) ) } @@ -425,7 +447,8 @@ private class MRelationsDefaultImpl(srcProd: Relation[File, File], binaryDep: Re private class MRelationsNameHashing(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) + classes: Relation[File, String], + val names: Relation[File, String]) extends MRelationsCommon(srcProd, binaryDep, classes) { def direct: Source = throw new UnsupportedOperationException("The `direct` source dependencies relation is not supported " + @@ -441,23 +464,29 @@ private class MRelationsNameHashing(srcProd: Relation[File, File], binaryDep: Re def addProduct(src: File, prod: File, name: String): Relations = new MRelationsNameHashing(srcProd + (src, prod), binaryDep, memberRef = memberRef, - inheritance = inheritance, classes + (src, name)) + inheritance = inheritance, classes + (src, name), names = names) 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 MRelationsNameHashing( srcProd, binaryDep, memberRef = newMR, inheritance = newIH, classes) + new MRelationsNameHashing( srcProd, binaryDep, memberRef = newMR, inheritance = newIH, classes, + names = names) } def addInternalSrcDeps(src: File, dependsOn: Iterable[File], inherited: Iterable[File]): Relations = { val newIH = inheritance.addInternal(src, inherited) val newMR = memberRef.addInternal(src, dependsOn) - new MRelationsNameHashing( srcProd, binaryDep, memberRef = newMR, inheritance = newIH, classes) + new MRelationsNameHashing( srcProd, binaryDep, memberRef = newMR, inheritance = newIH, classes, + names = names) } + def addUsedName(src: File, name: String): Relations = + new MRelationsNameHashing(srcProd, binaryDep, memberRef = memberRef, + inheritance = inheritance, classes, names = names + (src, name)) + def addBinaryDep(src: File, dependsOn: File): Relations = new MRelationsNameHashing(srcProd, binaryDep + (src, dependsOn), memberRef = memberRef, - inheritance = inheritance, classes) + inheritance = inheritance, classes, names = names) def ++ (o: Relations): Relations = { if (!o.nameHashing) @@ -465,11 +494,12 @@ private class MRelationsNameHashing(srcProd: Relation[File, File], binaryDep: Re "with different values of `nameHashing` flag.") new MRelationsNameHashing(srcProd ++ o.srcProd, binaryDep ++ o.binaryDep, memberRef = memberRef ++ o.memberRef, inheritance = inheritance ++ o.inheritance, - classes ++ o.classes) + classes ++ o.classes, names = names ++ o.names) } def -- (sources: Iterable[File]) = new MRelationsNameHashing(srcProd -- sources, binaryDep -- sources, - memberRef = memberRef -- sources, inheritance = inheritance -- sources, classes -- sources) + memberRef = memberRef -- sources, inheritance = inheritance -- sources, classes -- sources, + names = names -- sources) def groupBy[K](f: File => K): Map[K, Relations] = { throw new UnsupportedOperationException("Merging of Analyses that have" + diff --git a/compile/interface/src/main/scala/xsbt/API.scala b/compile/interface/src/main/scala/xsbt/API.scala index 9c005cfe0..c65bef3c0 100644 --- a/compile/interface/src/main/scala/xsbt/API.scala +++ b/compile/interface/src/main/scala/xsbt/API.scala @@ -43,6 +43,12 @@ final class API(val global: CallbackGlobal) extends Compat val extractApi = new ExtractAPI[global.type](global, sourceFile) val traverser = new TopLevelHandler(extractApi) traverser.apply(unit.body) + if (global.callback.nameHashing) { + val extractUsedNames = new ExtractUsedNames[global.type](global) + val names = extractUsedNames.extract(unit) + debug("The " + sourceFile + " contains the following used names " + names) + names foreach { (name: String) => callback.usedName(sourceFile, name) } + } val packages = traverser.packages.toArray[String].map(p => new xsbti.api.Package(p)) val source = new xsbti.api.SourceAPI(packages, traverser.definitions.toArray[xsbti.api.Definition]) extractApi.forceStructures() diff --git a/compile/interface/src/main/scala/xsbt/ExtractUsedNames.scala b/compile/interface/src/main/scala/xsbt/ExtractUsedNames.scala new file mode 100644 index 000000000..9f89a3459 --- /dev/null +++ b/compile/interface/src/main/scala/xsbt/ExtractUsedNames.scala @@ -0,0 +1,103 @@ +package xsbt + +import scala.tools.nsc._ + +/** + * Extracts simple names used in given compilation unit. + * + * Extracts simple (unqualified) names mentioned in given in non-definition position by collecting + * all symbols associated with non-definition trees and extracting names from all collected symbols. + * + * If given symbol is mentioned both in definition and in non-definition position (e.g. in member + * selection) then that symbol is collected. It means that names of symbols defined and used in the + * same compilation unit are extracted. We've considered not extracting names of those symbols + * as an optimization strategy. It turned out that this is not correct. Check + * https://github.com/gkossakowski/sbt/issues/3 for an example of scenario where it matters. + * + * All extracted names are returned in _decoded_ form. This way we stay consistent with the rest + * of incremental compiler which works with names in decoded form. + * + * Names mentioned in Import nodes are handled properly but require some special logic for two + * reasons: + * + * 1. import node itself has a term symbol associated with it with a name `. + * I (gkossakowski) tried to track down what role this symbol serves but I couldn't. + * It doesn't look like there are many places in Scala compiler that refer to + * that kind of symbols explicitly. + * 2. ImportSelector is not subtype of Tree therefore is not processed by `Tree.foreach` + * + * Another type of tree nodes that requires special handling is TypeTree. TypeTree nodes + * has a little bit odd representation: + * + * 1. TypeTree.hasSymbol always returns false even when TypeTree.symbol + * returns a symbol + * 2. The original tree from which given TypeTree was derived is stored + * in TypeTree.original but Tree.forech doesn't walk into original + * tree so we missed it + * + * The tree walking algorithm walks into TypeTree.original explicitly. + * + */ +class ExtractUsedNames[GlobalType <: CallbackGlobal](val global: GlobalType) { + import global._ + + def extract(unit: CompilationUnit): Set[String] = { + val tree = unit.body + val extractedByTreeWalk = extractByTreeWalk(tree) + extractedByTreeWalk + } + + private def extractByTreeWalk(tree: Tree): Set[String] = { + val namesBuffer = collection.mutable.ListBuffer.empty[String] + def addSymbol(symbol: Symbol): Unit = { + val symbolNameAsString = symbol.name.decode.trim + namesBuffer += symbolNameAsString + } + def handleTreeNode(node: Tree): Unit = node match { + case _: DefTree | _: Template => () + // turns out that Import node has a TermSymbol associated with it + // I (Grzegorz) tried to understand why it's there and what does it represent but + // that logic was introduced in 2005 without any justification I'll just ignore the + // import node altogether and just process the selectors in the import node + case Import(_, selectors: List[ImportSelector]) => + def usedNameInImportSelector(name: Name): Unit = + if ((name != null) && (name != nme.WILDCARD)) namesBuffer += name.toString + selectors foreach { selector => + usedNameInImportSelector(selector.name) + usedNameInImportSelector(selector.rename) + } + // TODO: figure out whether we should process the original tree or walk the type + // the argument for processing the original tree: we process what user wrote + // the argument for processing the type: we catch all transformations that typer applies + // to types but that might be a bad thing because it might expand aliases eagerly which + // not what we need + case t: TypeTree if t.original != null => + t.original.foreach(handleTreeNode) + case t if t.hasSymbol && eligibleAsUsedName(t.symbol) => + addSymbol(t.symbol) + case _ => () + } + tree.foreach(handleTreeNode) + namesBuffer.toSet + } + + + /** + * Needed for compatibility with Scala 2.8 which doesn't define `tpnme` + */ + private object tpnme { + val EMPTY = nme.EMPTY.toTypeName + val EMPTY_PACKAGE_NAME = nme.EMPTY_PACKAGE_NAME.toTypeName + } + + private def eligibleAsUsedName(symbol: Symbol): Boolean = { + def emptyName(name: Name): Boolean = name match { + case nme.EMPTY | nme.EMPTY_PACKAGE_NAME | tpnme.EMPTY | tpnme.EMPTY_PACKAGE_NAME => true + case _ => false + } + + (symbol != NoSymbol) && + !symbol.isSynthetic && + !emptyName(symbol.name) + } +} diff --git a/compile/interface/src/test/scala/xsbt/ExtractUsedNamesSpecification.scala b/compile/interface/src/test/scala/xsbt/ExtractUsedNamesSpecification.scala new file mode 100644 index 000000000..861edea62 --- /dev/null +++ b/compile/interface/src/test/scala/xsbt/ExtractUsedNamesSpecification.scala @@ -0,0 +1,108 @@ +package xsbt + +import org.junit.runner.RunWith +import xsbti.api.ClassLike +import xsbti.api.Def +import xsbti.api.Package +import xsbt.api.SameAPI +import org.junit.runners.JUnit4 + +import org.specs2.mutable.Specification + +@RunWith(classOf[JUnit4]) +class ExtractUsedNamesSpecification extends Specification { + + /** + * Standard names that appear in every compilation unit that has any class + * definition. + */ + private val standardNames = Set( + // AnyRef is added as default parent of a class + "scala", "AnyRef", + // class receives a default constructor which is internally called "" + "") + + "imported name" in { + val src = """ + |package a { class A } + |package b { + | import a.{A => A2} + |}""".stripMargin + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) + val usedNames = compilerForTesting.extractUsedNamesFromSrc(src) + val expectedNames = standardNames ++ Set("a", "A", "A2", "b") + usedNames === expectedNames + } + + // test covers https://github.com/gkossakowski/sbt/issues/6 + "names in type tree" in { + val srcA = """| + |package a { + | class A { + | class C { class D } + | } + | class B[T] + | class BB + |}""".stripMargin + val srcB = """| + |package b { + | abstract class X { + | def foo: a.A#C#D + | def bar: a.B[a.BB] + | } + |}""".stripMargin + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) + val usedNames = compilerForTesting.extractUsedNamesFromSrc(srcA, srcB) + val expectedNames = standardNames ++ Set("a", "A", "B", "C", "D", "b", "X", "BB") + usedNames === expectedNames + } + + // test for https://github.com/gkossakowski/sbt/issues/5 + "symbolic names" in { + val srcA = """| + |class A { + | def `=`: Int = 3 + |}""".stripMargin + val srcB = """| + |class B { + | def foo(a: A) = a.`=` + |}""".stripMargin + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) + val usedNames = compilerForTesting.extractUsedNamesFromSrc(srcA, srcB) + val expectedNames = standardNames ++ Set("A", "a", "B", "=") + usedNames === expectedNames + } + + // test for https://github.com/gkossakowski/sbt/issues/3 + "used names from the same compilation unit" in { + val src = "class A { def foo: Int = 0; def bar: Int = foo }" + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) + val usedNames = compilerForTesting.extractUsedNamesFromSrc(src) + val expectedNames = standardNames ++ Set("A", "foo", "Int") + usedNames === expectedNames + } + + // pending test for https://issues.scala-lang.org/browse/SI-7173 + "names of constants" in { + val src = "class A { final val foo = 12; def bar: Int = foo }" + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) + val usedNames = compilerForTesting.extractUsedNamesFromSrc(src) + val expectedNames = standardNames ++ Set("A", "foo", "Int") + usedNames === expectedNames + }.pendingUntilFixed("Scala's type checker inlines constants so we can't see the original name.") + + // pending test for https://github.com/gkossakowski/sbt/issues/4 + // TODO: we should fix it by having special treatment of `selectDynamic` and `applyDynamic` calls + "names from method calls on Dynamic" in { + val srcA = """|import scala.language.dynamics + |class A extends Dynamic { + | def selectDynamic(name: String): Int = name.length + |}""".stripMargin + val srcB = "class B { def foo(a: A): Int = a.bla }" + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) + val usedNames = compilerForTesting.extractUsedNamesFromSrc(srcA, srcB) + val expectedNames = standardNames ++ Set("B", "A", "a", "Int", "selectDynamic", "bla") + usedNames === expectedNames + }.pendingUntilFixed("Call to Dynamic is desugared in type checker so Select nodes is turned into string literal.") + +} diff --git a/compile/interface/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala b/compile/interface/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala index 91b3830d6..5362b1ca6 100644 --- a/compile/interface/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala +++ b/compile/interface/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala @@ -30,6 +30,24 @@ class ScalaCompilerForUnitTesting(nameHashing: Boolean = false) { analysisCallback.apis(tempSrcFile) } + def extractUsedNamesFromSrc(src: String): Set[String] = { + val (Seq(tempSrcFile), analysisCallback) = compileSrcs(src) + analysisCallback.usedNames(tempSrcFile).toSet + } + + /** + * Extract used names from src provided as the second argument. + * + * The purpose of the first argument is to define names that the second + * source is going to refer to. Both files are compiled in the same compiler + * Run but only names used in the second src file are returned. + */ + def extractUsedNamesFromSrc(definitionSrc: String, actualSrc: String): Set[String] = { + // we drop temp src file corresponding to the definition src file + val (Seq(_, tempSrcFile), analysisCallback) = compileSrcs(definitionSrc, actualSrc) + analysisCallback.usedNames(tempSrcFile).toSet + } + /** * 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 diff --git a/compile/persist/src/main/scala/sbt/inc/AnalysisFormats.scala b/compile/persist/src/main/scala/sbt/inc/AnalysisFormats.scala index b5ddd84f0..5f2c7b9c6 100644 --- a/compile/persist/src/main/scala/sbt/inc/AnalysisFormats.scala +++ b/compile/persist/src/main/scala/sbt/inc/AnalysisFormats.scala @@ -99,16 +99,16 @@ 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], memberRefF: Format[SourceDependencies], inheritanceF: Format[SourceDependencies], csF: Format[RFS]): Format[Relations] = + 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], namesF: Format[RFS]): Format[Relations] = { def makeRelation(srcProd: RFF, binaryDep: RFF, direct: RSource, publicInherited: RSource, memberRef: SourceDependencies, inheritance: SourceDependencies, classes: RFS, - nameHashing: Boolean): Relations = if (nameHashing) { + nameHashing: Boolean, names: RFS): Relations = if (nameHashing) { 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 `nameHashing` flag is enabled.") - Relations.make(srcProd, binaryDep, memberRef, inheritance, classes) + Relations.make(srcProd, binaryDep, memberRef, inheritance, classes, names) } else { def isEmpty(sourceDependencies: SourceDependencies): Boolean = sourceDependencies.internal.all.isEmpty && sourceDependencies.external.all.isEmpty @@ -116,9 +116,9 @@ object AnalysisFormats assert(isEmpty(memberRef), "Direct dependencies are not empty but `nameHashing` 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.nameHashing) )( - prodF, binF, directF, inheritedF, memberRefF, inheritanceF, csF, implicitly[Format[Boolean]]) + asProduct9[Relations, RFF, RFF, RSource, RSource, SourceDependencies, SourceDependencies, RFS, Boolean, RFS]( (a,b,c,d,e,f,g,h,i) =>makeRelation(a,b,c,d,e,f,g,h,i) )( + rs => (rs.srcProd, rs.binaryDep, rs.direct, rs.publicInherited, rs.memberRef, rs.inheritance, rs.classes, rs.nameHashing, rs.names) )( + prodF, binF, directF, inheritedF, memberRefF, inheritanceF, csF, implicitly[Format[Boolean]], namesF) } implicit def relationsSourceFormat(implicit internalFormat: Format[Relation[File, File]], externalFormat: Format[Relation[File,String]]): Format[RSource] = diff --git a/compile/persist/src/main/scala/sbt/inc/TextAnalysisFormat.scala b/compile/persist/src/main/scala/sbt/inc/TextAnalysisFormat.scala index 849c57a7f..e4dea8a17 100644 --- a/compile/persist/src/main/scala/sbt/inc/TextAnalysisFormat.scala +++ b/compile/persist/src/main/scala/sbt/inc/TextAnalysisFormat.scala @@ -117,6 +117,8 @@ object TextAnalysisFormat { val memberRefExternalDep = "member reference external dependencies" val inheritanceInternalDep = "inheritance internal dependencies" val inheritanceExternalDep = "inheritance external dependencies" + + val usedNames = "used names" } def write(out: Writer, relations: Relations) { @@ -158,6 +160,7 @@ object TextAnalysisFormat { writeRelation(Headers.inheritanceExternalDep, inheritance.external) writeRelation(Headers.classes, relations.classes) + writeRelation(Headers.usedNames, relations.names) } def read(in: BufferedReader): Relations = { @@ -215,11 +218,15 @@ object TextAnalysisFormat { "One mechanism is supported for tracking source dependencies at the time") val nameHashing = memberRefSrcDeps != emptySourceDependencies val classes = readStringRelation(Headers.classes) + val names = readStringRelation(Headers.usedNames) if (nameHashing) - Relations.make(srcProd, binaryDep, memberRefSrcDeps, inheritanceSrcDeps, classes) - else + Relations.make(srcProd, binaryDep, memberRefSrcDeps, inheritanceSrcDeps, classes, names) + else { + assert(names.all.isEmpty, s"When `nameHashing` is disabled `names` relation " + + "should be empty: $names") 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 790db124a..0e083d4eb 100644 --- a/interface/src/main/java/xsbti/AnalysisCallback.java +++ b/interface/src/main/java/xsbti/AnalysisCallback.java @@ -24,6 +24,7 @@ public interface AnalysisCallback public void generatedClass(File source, File module, String name); /** Called when the public API of a source file is extracted. */ public void api(File sourceFile, xsbti.api.SourceAPI source); + public void usedName(File sourceFile, String names); /** 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); diff --git a/interface/src/test/scala/xsbti/TestCallback.scala b/interface/src/test/scala/xsbti/TestCallback.scala index e620f6be2..3ea7e32e1 100644 --- a/interface/src/test/scala/xsbti/TestCallback.scala +++ b/interface/src/test/scala/xsbti/TestCallback.scala @@ -9,12 +9,14 @@ class TestCallback(override val nameHashing: Boolean = false) extends AnalysisCa val sourceDependencies = new ArrayBuffer[(File, File, Boolean)] val binaryDependencies = new ArrayBuffer[(File, String, File, Boolean)] val products = new ArrayBuffer[(File, File, String)] + val usedNames = scala.collection.mutable.Map.empty[File, Set[String]].withDefaultValue(Set.empty) val apis: scala.collection.mutable.Map[File, SourceAPI] = scala.collection.mutable.Map.empty def sourceDependency(dependsOn: File, source: File, inherited: Boolean) { sourceDependencies += ((dependsOn, source, inherited)) } def binaryDependency(binary: File, name: String, source: File, inherited: Boolean) { binaryDependencies += ((binary, name, source, inherited)) } def generatedClass(source: File, module: File, name: String) { products += ((source, module, name)) } + def usedName(source: File, name: String) { usedNames(source) += name } def api(source: File, sourceAPI: SourceAPI): Unit = { assert(!apis.contains(source), s"The `api` method should be called once per source file: $source") apis(source) = sourceAPI diff --git a/project/Sbt.scala b/project/Sbt.scala index 39565833f..288eb25ba 100644 --- a/project/Sbt.scala +++ b/project/Sbt.scala @@ -270,6 +270,9 @@ object Sbt extends Build // we are expecting all of our dependencies to be on classpath so Scala compiler // can use them while constructing its own classpath for compilation fork in Test := true, + // needed because we fork tests and tests are ran in parallel so we have multiple Scala + // compiler instances that are memory hungry + javaOptions in Test += "-Xmx1G", artifact in (Compile, packageSrc) := Artifact(srcID).copy(configurations = Compile :: Nil).extra("e:component" -> srcID) ) def compilerSettings = Seq(