From a9a709ccc0621f5183e241b6c5f382427fa5a4c8 Mon Sep 17 00:00:00 2001 From: Grzegorz Kossakowski Date: Wed, 4 Dec 2013 01:34:18 +0100 Subject: [PATCH 1/3] Add hashing of public names defined in a source file. A hash for given name in a source file is computed by combining hashes of all definitions with given name. When hashing a single definition we take into account all information about it except nested definitions. For example, if we have following definition class Foo[T] { def bar(x: Int): Int = ??? } hash sum for `Foo` will include the fact that we have a class with a single type parameter but it won't include hash sum of `bar` method. Computed hash sums are location-sensitive. Each definition is hashed along with its location so we properly detect cases when definition's signature stays the same but it's moved around in the same compilation unit. The location is defined as sequence of selections. Each selection consists of a name and name type. The name type is either term name or type name. Scala specification (9.2) guarantees that each publicly visible definition is uniquely identified by a sequence of such selectors. For example, if we have: object Foo { class Bar { def abc: Int } } then location of `abc` is Seq((TermName, Foo), (TypeName, Bar)) It's worth mentioning that we track name-hash pairs separately for regular (non implicit) and implicit members. That's required for name hashing algorithm because it does not apply its heuristic when implicit members are being modified. Another important characteristic is that we include all inherited members when computing name hashes. Here comes the detailed list of changes made in this commit: * HashAPI has new parameter `includeDefinitions` that allows shallow hashing of Structures (where we do not compute hashes recursively) * HashAPI exposes `finalizeHash` method that allow one to capture current hash at any time. This is useful if you want to hash a list of definitions and not just whole `SourceAPI`. * NameHashing implements actual extraction of public definitions, grouping them by simple name and computing hash sums for each group using HashAPI * `Source` class (defined in interface/other file) has been extended to include `_internalOnly_nameHashes` field. This field stores NameHashes data structure for given source file. The NameHashes stores two separate collections of name-hash pairs for regular and implicit members. The prefix `_internalOnly_` is used to indicate that this is not an official incremental compiler's or sbt's API and it's for use by incremental compiler internals only. We had to use such a prefix because the `datatype` code generator doesn't support emitting access modifiers * `AnalysisCallback` implementation has been modified to gather all name hashes and store them in the Source object * TestCaseGenerators has been modified to implement generation of NameHashes * The NameHashingSpecification contains a few unit tests that make sure that the basic functionality works properly --- .../api/src/main/scala/xsbt/api/HashAPI.scala | 86 +++++-- compile/inc/src/main/scala/sbt/inc/APIs.scala | 18 +- .../inc/src/main/scala/sbt/inc/Compile.scala | 7 +- .../src/main/scala/sbt/inc/NameHashing.scala | 143 ++++++++++++ .../sbt/inc/NameHashingSpecification.scala | 214 ++++++++++++++++++ .../scala/sbt/inc/TestCaseGenerators.scala | 27 ++- interface/other | 9 + 7 files changed, 473 insertions(+), 31 deletions(-) create mode 100644 compile/inc/src/main/scala/sbt/inc/NameHashing.scala create mode 100644 compile/inc/src/test/scala/sbt/inc/NameHashingSpecification.scala diff --git a/compile/api/src/main/scala/xsbt/api/HashAPI.scala b/compile/api/src/main/scala/xsbt/api/HashAPI.scala index 451f046de..b48394910 100644 --- a/compile/api/src/main/scala/xsbt/api/HashAPI.scala +++ b/compile/api/src/main/scala/xsbt/api/HashAPI.scala @@ -12,10 +12,31 @@ object HashAPI { type Hash = Int def apply(a: SourceAPI): Hash = - (new HashAPI(false, true)).hashAPI(a) + (new HashAPI(false, true, true)).hashAPI(a) + + def apply(x: Def): Hash = { + val hashApi = new HashAPI(false, true, true) + hashApi.hashDefinition(x) + hashApi.finalizeHash + } + + def hashDefinitionsWithExtraHashes(ds: Seq[(Definition, Hash)]): Hash = { + val hashAPI = new HashAPI(false, true, false) + hashAPI.hashDefinitionsWithExtraHashes(ds) + hashAPI.finalizeHash + } } -final class HashAPI(includePrivate: Boolean, includeParamNames: Boolean) +/** + * Implements hashing of public API. + * + * @param includePrivate should private definitions be included in a hash sum + * @param includeParamNames should parameter names for methods be included in a hash sum + * @param includeDefinitions when hashing a structure (e.g. of a class) should hashes of definitions (members) + * be included in a hash sum. Structure can appear as a type (in structural type) and in that case we + * always include definitions in a hash sum. + */ +final class HashAPI(includePrivate: Boolean, includeParamNames: Boolean, includeDefinitions: Boolean) { import scala.collection.mutable import MurmurHash.{extendHash, finalizeHash, nextMagicA, nextMagicB, startHash, startMagicA, startMagicB, stringHash, symmetricHash} @@ -44,7 +65,7 @@ final class HashAPI(includePrivate: Boolean, includeParamNames: Boolean) private[this] final val PublicHash = 30 private[this] final val ProtectedHash = 31 - private[this] final val PrivateHash = 32 + private[this] final val PrivateHash = 32 private[this] final val UnqualifiedHash = 33 private[this] final val ThisQualifierHash = 34 private[this] final val IdQualifierHash = 35 @@ -75,7 +96,7 @@ final class HashAPI(includePrivate: Boolean, includeParamNames: Boolean) private[this] var hash: Hash = startHash(0) private[this] var magicA: Hash = startMagicA private[this] var magicB: Hash = startMagicB - + @inline final def hashString(s: String): Unit = extend(stringHash(s)) @inline final def hashBoolean(b: Boolean): Unit = extend(if(b) TrueHash else FalseHash) @inline final def hashSeq[T](s: Seq[T], hashF: T => Unit) @@ -93,7 +114,7 @@ final class HashAPI(includePrivate: Boolean, includeParamNames: Boolean) magicA = startMagicA magicB = startMagicB hashF(t) - (finalizeHash(hash), magicA, magicB) + (finalizeHash, magicA, magicB) } unzip3; hash = current magicA = mA @@ -107,6 +128,9 @@ final class HashAPI(includePrivate: Boolean, includeParamNames: Boolean) magicA = nextMagicA(magicA) magicB = nextMagicB(magicB) } + + def finalizeHash: Hash = MurmurHash.finalizeHash(hash) + def hashModifiers(m: Modifiers) = extend(m.raw) def hashAPI(s: SourceAPI): Hash = @@ -114,7 +138,7 @@ final class HashAPI(includePrivate: Boolean, includeParamNames: Boolean) hash = startHash(0) hashSymmetric(s.packages, hashPackage) hashDefinitions(s.definitions, true) - finalizeHash(hash) + finalizeHash } def hashPackage(p: Package) = hashString(p.name) @@ -124,6 +148,24 @@ final class HashAPI(includePrivate: Boolean, includeParamNames: Boolean) val defs = SameAPI.filterDefinitions(ds, topLevel, includePrivate) hashSymmetric(defs, hashDefinition) } + + /** + * Hashes a sequence of definitions by combining each definition's own + * hash with extra one supplied as first element of a pair. + * + * It's useful when one wants to influence hash of a definition by some + * external (to definition) factor (e.g. location of definition). + * + * NOTE: This method doesn't perform any filtering of passed definitions. + */ + def hashDefinitionsWithExtraHashes(ds: Seq[(Definition, Hash)]): Unit = + { + def hashDefinitionCombined(d: Definition, extraHash: Hash): Unit = { + hashDefinition(d) + extend(extraHash) + } + hashSymmetric(ds, (hashDefinitionCombined _).tupled) + } def hashDefinition(d: Definition) { hashString(d.name) @@ -145,7 +187,7 @@ final class HashAPI(includePrivate: Boolean, includeParamNames: Boolean) extend(ClassHash) hashParameterizedDefinition(c) hashType(c.selfType) - hashStructure(c.structure) + hashStructure(c.structure, includeDefinitions) } def hashField(f: FieldLike) { @@ -202,7 +244,7 @@ final class HashAPI(includePrivate: Boolean, includeParamNames: Boolean) extend(parameter.modifier.ordinal) hashBoolean(parameter.hasDefault) } - + def hashParameterizedDefinition[T <: ParameterizedDefinition](d: T) { hashTypeParameters(d.typeParameters) @@ -220,7 +262,7 @@ final class HashAPI(includePrivate: Boolean, includeParamNames: Boolean) hashParameterizedDefinition(d) hashType(d.tpe) } - + def hashTypeParameters(parameters: Seq[TypeParameter]) = hashSeq(parameters, hashTypeParameter) def hashTypeParameter(parameter: TypeParameter) { @@ -243,12 +285,13 @@ final class HashAPI(includePrivate: Boolean, includeParamNames: Boolean) hashString(arg.name) hashString(arg.value) } - - def hashTypes(ts: Seq[Type]) = hashSeq(ts, hashType) - def hashType(t: Type): Unit = + + def hashTypes(ts: Seq[Type], includeDefinitions: Boolean = true) = + hashSeq(ts, (t: Type) => hashType(t, includeDefinitions)) + def hashType(t: Type, includeDefinitions: Boolean = true): Unit = t match { - case s: Structure => hashStructure(s) + case s: Structure => hashStructure(s, includeDefinitions) case e: Existential => hashExistential(e) case c: Constant => hashConstant(c) case p: Polymorphic => hashPolymorphic(p) @@ -259,7 +302,7 @@ final class HashAPI(includePrivate: Boolean, includeParamNames: Boolean) case s: Singleton => hashSingleton(s) case pr: ParameterRef => hashParameterRef(pr) } - + def hashParameterRef(p: ParameterRef) { extend(ParameterRefHash) @@ -322,13 +365,16 @@ final class HashAPI(includePrivate: Boolean, includeParamNames: Boolean) hashType(a.baseType) hashAnnotations(a.annotations) } - final def hashStructure(structure: Structure) = visit(visitedStructures, structure)(hashStructure0) - def hashStructure0(structure: Structure) + final def hashStructure(structure: Structure, includeDefinitions: Boolean) = + visit(visitedStructures, structure)(structure => hashStructure0(structure, includeDefinitions)) + def hashStructure0(structure: Structure, includeDefinitions: Boolean) { extend(StructureHash) - hashTypes(structure.parents) - hashDefinitions(structure.declared, false) - hashDefinitions(structure.inherited, false) + hashTypes(structure.parents, includeDefinitions) + if (includeDefinitions) { + hashDefinitions(structure.declared, false) + hashDefinitions(structure.inherited, false) + } } def hashParameters(parameters: Seq[TypeParameter], base: Type): Unit = { @@ -336,4 +382,4 @@ final class HashAPI(includePrivate: Boolean, includeParamNames: Boolean) hashType(base) } } - + diff --git a/compile/inc/src/main/scala/sbt/inc/APIs.scala b/compile/inc/src/main/scala/sbt/inc/APIs.scala index cc28c20e8..a09df5ef7 100644 --- a/compile/inc/src/main/scala/sbt/inc/APIs.scala +++ b/compile/inc/src/main/scala/sbt/inc/APIs.scala @@ -7,6 +7,7 @@ package inc import xsbti.api.Source import java.io.File import APIs.getAPI +import xsbti.api._internalOnly_NameHashes import scala.util.Sorting import xsbt.api.SameAPI @@ -18,12 +19,12 @@ trait APIs /** The API for the external class `ext` at the time represented by this instance. * This method returns an empty API if the file had no API or is not known to this instance. */ def externalAPI(ext: String): Source - + def allExternals: collection.Set[String] def allInternalSources: collection.Set[File] - + def ++ (o: APIs): APIs - + def markInternalSource(src: File, api: Source): APIs def markExternalAPI(ext: String, api: Source): APIs @@ -39,10 +40,11 @@ object APIs { def apply(internal: Map[File, Source], external: Map[String, Source]): APIs = new MAPIs(internal, external) def empty: APIs = apply(Map.empty, Map.empty) - + val emptyAPI = new xsbti.api.SourceAPI(Array(), Array()) val emptyCompilation = new xsbti.api.Compilation(-1, Array()) - val emptySource = new xsbti.api.Source(emptyCompilation, Array(), emptyAPI, 0, false) + val emptyNameHashes = new xsbti.api._internalOnly_NameHashes(Array.empty, Array.empty) + val emptySource = new xsbti.api.Source(emptyCompilation, Array(), emptyAPI, 0, emptyNameHashes, false) def getAPI[T](map: Map[T, Source], src: T): Source = map.getOrElse(src, emptySource) } @@ -50,15 +52,15 @@ private class MAPIs(val internal: Map[File, Source], val external: Map[String, S { def allInternalSources: collection.Set[File] = internal.keySet def allExternals: collection.Set[String] = external.keySet - + def ++ (o: APIs): APIs = new MAPIs(internal ++ o.internal, external ++ o.external) - + def markInternalSource(src: File, api: Source): APIs = new MAPIs(internal.updated(src, api), external) def markExternalAPI(ext: String, api: Source): APIs = new MAPIs(internal, external.updated(ext, api)) - + def removeInternal(remove: Iterable[File]): APIs = new MAPIs(internal -- remove, external) def filterExt(keep: String => Boolean): APIs = new MAPIs(internal, external.filterKeys(keep)) @deprecated("Broken implementation. OK to remove in 0.14", "0.13.1") diff --git a/compile/inc/src/main/scala/sbt/inc/Compile.scala b/compile/inc/src/main/scala/sbt/inc/Compile.scala index 2d325a4d9..a5b56a5c5 100644 --- a/compile/inc/src/main/scala/sbt/inc/Compile.scala +++ b/compile/inc/src/main/scala/sbt/inc/Compile.scala @@ -4,11 +4,12 @@ package sbt package inc -import xsbti.api.{Source, SourceAPI, Compilation, OutputSetting} +import xsbti.api.{Source, SourceAPI, Compilation, OutputSetting, _internalOnly_NameHashes} import xsbti.compile.{DependencyChanges, Output, SingleOutput, MultipleOutput} import xsbti.{Position,Problem,Severity} import Logger.{m2o, problem} import java.io.File +import xsbti.api.Definition object IncrementalCompile { @@ -68,6 +69,7 @@ private final class AnalysisCallback(internalMap: File => Option[File], external private[this] val apis = new HashMap[File, (Int, SourceAPI)] private[this] val usedNames = new HashMap[File, Set[String]] + private[this] val publicNameHashes = new HashMap[File, _internalOnly_NameHashes] 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]] @@ -147,6 +149,7 @@ private final class AnalysisCallback(internalMap: File => Option[File], external def api(sourceFile: File, source: SourceAPI) { import xsbt.api.{APIUtil, HashAPI} if (APIUtil.isScalaSourceName(sourceFile.getName) && APIUtil.hasMacro(source)) macroSources += sourceFile + publicNameHashes(sourceFile) = (new NameHashing).nameHashes(source) val shouldMinimize = !Incremental.apiDebug(options) val savedSource = if (shouldMinimize) APIUtil.minimize(source) else source apis(sourceFile) = (HashAPI(source), savedSource) @@ -165,7 +168,7 @@ private final class AnalysisCallback(internalMap: File => Option[File], external val hash = stamp match { case h: Hash => h.value; case _ => new Array[Byte](0) } // TODO store this in Relations, rather than Source. val hasMacro: Boolean = macroSources.contains(src) - val s = new xsbti.api.Source(compilation, hash, api._2, api._1, hasMacro) + val s = new xsbti.api.Source(compilation, hash, api._2, api._1, publicNameHashes(src), hasMacro) val info = SourceInfos.makeInfo(getOrNil(reporteds, src), getOrNil(unreporteds, src)) val direct = sourceDeps.getOrElse(src, Nil: Iterable[File]) val publicInherited = inheritedSourceDeps.getOrElse(src, Nil: Iterable[File]) diff --git a/compile/inc/src/main/scala/sbt/inc/NameHashing.scala b/compile/inc/src/main/scala/sbt/inc/NameHashing.scala new file mode 100644 index 000000000..da998af2b --- /dev/null +++ b/compile/inc/src/main/scala/sbt/inc/NameHashing.scala @@ -0,0 +1,143 @@ +package sbt.inc + +import xsbti.api.SourceAPI +import xsbti.api.Definition +import xsbti.api.DefinitionType +import xsbti.api.ClassLike +import xsbti.api._internalOnly_NameHash +import xsbti.api._internalOnly_NameHashes +import xsbt.api.Visit + +/** + * A class that computes hashes for each group of definitions grouped by a simple name. + * + * See `nameHashes` method for details. + */ +class NameHashing { + + import NameHashing._ + + /** + * This method takes an API representation and extracts a flat collection of all + * definitions contained in that API representation. Then it groups definition + * by a simple name. Lastly, it computes a hash sum of all definitions in a single + * group. + * + * NOTE: The hashing sum used for hashing a group of definition is insensitive + * to order of definitions. + */ + def nameHashes(source: SourceAPI): _internalOnly_NameHashes = { + val apiPublicDefs = publicDefs(source) + val (regularDefs, implicitDefs) = apiPublicDefs.partition(locDef => !locDef.definition.modifiers.isImplicit) + val regularNameHashes = nameHashesForLocatedDefinitions(regularDefs) + val implicitNameHashes = nameHashesForLocatedDefinitions(implicitDefs) + new _internalOnly_NameHashes(regularNameHashes.toArray, implicitNameHashes.toArray) + } + + private def nameHashesForLocatedDefinitions(locatedDefs: Iterable[LocatedDefinition]): Iterable[_internalOnly_NameHash] = { + val groupedBySimpleName = locatedDefs.groupBy(locatedDef => localName(locatedDef.definition.name)) + val hashes = groupedBySimpleName.mapValues(hashLocatedDefinitions) + hashes.toIterable.map({ case (name: String, hash: Int) => new _internalOnly_NameHash(name, hash) }) + } + + private def hashLocatedDefinitions(locatedDefs: Iterable[LocatedDefinition]): Int = { + val defsWithExtraHashes = locatedDefs.toSeq.map(ld => ld.definition -> ld.location.hashCode) + xsbt.api.HashAPI.hashDefinitionsWithExtraHashes(defsWithExtraHashes) + } + + /** + * A visitor that visits given API object and extracts all nested public + * definitions it finds. The extracted definitions have Location attached + * to them which identifies API object's location. + * + * The returned location is basically a path to a definition that contains + * the located definition. For example, if we have: + * + * object Foo { + * class Bar { def abc: Int } + * } + * + * then location of `abc` is Seq((TermName, Foo), (TypeName, Bar)) + */ + private class ExtractPublicDefinitions extends Visit { + val locatedDefs = scala.collection.mutable.Buffer[LocatedDefinition]() + private var currentLocation: Location = Location() + override def visitAPI(s: SourceAPI): Unit = { + s.packages foreach visitPackage + s.definitions foreach { case topLevelDef: ClassLike => + val packageName = { + val fullName = topLevelDef.name() + val lastDotIndex = fullName.lastIndexOf('.') + if (lastDotIndex <= 0) "" else fullName.substring(0, lastDotIndex-1) + } + currentLocation = packageAsLocation(packageName) + visitDefinition(topLevelDef) + } + } + override def visitDefinition(d: Definition): Unit = { + val locatedDef = LocatedDefinition(currentLocation, d) + locatedDefs += locatedDef + d match { + case cl: xsbti.api.ClassLike => + val savedLocation = currentLocation + currentLocation = classLikeAsLocation(currentLocation, cl) + super.visitDefinition(d) + currentLocation = savedLocation + case _ => + super.visitDefinition(d) + } + } + } + + private def publicDefs(source: SourceAPI): Iterable[LocatedDefinition] = { + val visitor = new ExtractPublicDefinitions + visitor.visitAPI(source) + visitor.locatedDefs + } + + private def localName(name: String): String = { + // when there's no dot in name `lastIndexOf` returns -1 so we handle + // that case properly + val index = name.lastIndexOf('.') + 1 + name.substring(index) + } + + private def packageAsLocation(pkg: String): Location = if (pkg != "") { + val selectors = pkg.split('.').map(name => Selector(name, TermName)).toSeq + Location(selectors: _*) + } else Location.Empty + + private def classLikeAsLocation(prefix: Location, cl: ClassLike): Location = { + val selector = { + val clNameType = NameType(cl.definitionType) + Selector(localName(cl.name), clNameType) + } + Location((prefix.selectors :+ selector): _*) + } +} + +object NameHashing { + private case class LocatedDefinition(location: Location, definition: Definition) + /** + * Location is expressed as sequence of annotated names. The annotation denotes + * a type of a name, i.e. whether it's a term name or type name. + * + * Using Scala compiler terminology, location is defined as a sequence of member + * selections that uniquely identify a given Symbol. + */ + private case class Location(selectors: Selector*) + private object Location { + val Empty = Location(Seq.empty: _*) + } + private case class Selector(name: String, nameType: NameType) + private sealed trait NameType + private object NameType { + import DefinitionType._ + def apply(dt: DefinitionType): NameType = dt match { + case Trait | ClassDef => TypeName + case Module | PackageModule => TermName + } + } + private case object TermName extends NameType + private case object TypeName extends NameType +} diff --git a/compile/inc/src/test/scala/sbt/inc/NameHashingSpecification.scala b/compile/inc/src/test/scala/sbt/inc/NameHashingSpecification.scala new file mode 100644 index 000000000..3a6e93827 --- /dev/null +++ b/compile/inc/src/test/scala/sbt/inc/NameHashingSpecification.scala @@ -0,0 +1,214 @@ +package sbt.inc + +import org.junit.runner.RunWith +import xsbti.api._ +import xsbt.api.HashAPI +import org.specs2.mutable.Specification +import org.specs2.runner.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class NameHashingSpecification extends Specification { + + /** + * Very basic test which checks whether a name hash is insensitive to + * definition order (across the whole compilation unit). + */ + "new member" in { + val nameHashing = new NameHashing + val def1 = new Def(Array.empty, strTpe, Array.empty, "foo", publicAccess, defaultModifiers, Array.empty) + val def2 = new Def(Array.empty, intTpe, Array.empty, "bar", publicAccess, defaultModifiers, Array.empty) + val classBar1 = simpleClass("Bar", def1) + val classBar2 = simpleClass("Bar", def1, def2) + val api1 = new SourceAPI(Array.empty, Array(classBar1)) + val api2 = new SourceAPI(Array.empty, Array(classBar2)) + val nameHashes1 = nameHashing.nameHashes(api1) + val nameHashes2 = nameHashing.nameHashes(api2) + assertNameHashEqualForRegularName("Bar", nameHashes1, nameHashes2) + assertNameHashEqualForRegularName("foo", nameHashes1, nameHashes2) + nameHashes1.regularMembers.map(_.name).toSeq must not contain("bar") + nameHashes2.regularMembers.map(_.name).toSeq must contain("bar") + } + + /** + * Very basic test which checks whether a name hash is insensitive to + * definition order (across the whole compilation unit). + */ + "definition order" in { + val nameHashing = new NameHashing + val def1 = new Def(Array.empty, intTpe, Array.empty, "bar", publicAccess, defaultModifiers, Array.empty) + val def2 = new Def(Array.empty, strTpe, Array.empty, "bar", publicAccess, defaultModifiers, Array.empty) + val nestedBar1 = simpleClass("Bar1", def1) + val nestedBar2 = simpleClass("Bar2", def2) + val classA = simpleClass("Foo", nestedBar1, nestedBar2) + val classB = simpleClass("Foo", nestedBar2, nestedBar1) + val api1 = new SourceAPI(Array.empty, Array(classA)) + val api2 = new SourceAPI(Array.empty, Array(classB)) + val nameHashes1 = nameHashing.nameHashes(api1) + val nameHashes2 = nameHashing.nameHashes(api2) + val def1Hash = HashAPI(def1) + val def2Hash = HashAPI(def2) + def1Hash !=== def2Hash + nameHashes1 === nameHashes2 + } + + /** + * Very basic test which asserts that a name hash is sensitive to definition location. + * + * For example, if we have: + * // Foo1.scala + * class Foo { def xyz: Int = ... } + * object Foo + * + * and: + * // Foo2.scala + * class Foo + * object Foo { def xyz: Int = ... } + * + * then hash for `xyz` name should differ in those two cases + * because method `xyz` was moved from class to an object. + */ + "definition location" in { + val nameHashing = new NameHashing + val deff = new Def(Array.empty, intTpe, Array.empty, "bar", publicAccess, defaultModifiers, Array.empty) + val classA = { + val nestedBar1 = simpleClass("Bar1", deff) + val nestedBar2 = simpleClass("Bar2") + simpleClass("Foo", nestedBar1, nestedBar2) + } + val classB = { + val nestedBar1 = simpleClass("Bar1") + val nestedBar2 = simpleClass("Bar2", deff) + simpleClass("Foo", nestedBar1, nestedBar2) + } + val api1 = new SourceAPI(Array.empty, Array(classA)) + val api2 = new SourceAPI(Array.empty, Array(classB)) + val nameHashes1 = nameHashing.nameHashes(api1) + val nameHashes2 = nameHashing.nameHashes(api2) + nameHashes1 !=== nameHashes2 + } + + /** + * Test if members introduced in parent class affect hash of a name + * of a child class. + * + * For example, if we have: + * // Test1.scala + * class Parent + * class Child extends Parent + * + * and: + * // Test2.scala + * class Parent { def bar: Int = ... } + * class Child extends Parent + * + * then hash for `Child` name should be the same in both + * cases. + */ + "definition in parent class" in { + val parentA = simpleClass("Parent") + val barMethod = new Def(Array.empty, intTpe, Array.empty, "bar", publicAccess, defaultModifiers, Array.empty) + val parentB = simpleClass("Parent", barMethod) + val childA = { + val structure = new Structure(lzy(Array[Type](parentA.structure)), lzy(Array.empty[Definition]), lzy(Array.empty[Definition])) + simpleClass("Child", structure) + } + val childB = { + val structure = new Structure(lzy(Array[Type](parentB.structure)), lzy(Array.empty[Definition]), lzy(Array[Definition](barMethod))) + simpleClass("Child", structure) + } + val parentANameHashes = nameHashesForClass(parentA) + val parentBNameHashes = nameHashesForClass(parentB) + Seq("Parent") === parentANameHashes.regularMembers.map(_.name).toSeq + Seq("Parent", "bar") === parentBNameHashes.regularMembers.map(_.name).toSeq + parentANameHashes !=== parentBNameHashes + val childANameHashes = nameHashesForClass(childA) + val childBNameHashes = nameHashesForClass(childB) + assertNameHashEqualForRegularName("Child", childANameHashes, childBNameHashes) + } + + /** + * Checks if changes to structural types that appear in method signature + * affect name hash of the method. For example, if we have: + * + * // Test1.scala + * class A { + * def foo: { bar: Int } + * } + * + * // Test2.scala + * class A { + * def foo: { bar: String } + * } + * + * then name hash for "foo" should be different in those two cases. + */ + "structural type in definition" in { + /** def foo: { bar: Int } */ + val fooMethod1 = { + val barMethod1 = new Def(Array.empty, intTpe, Array.empty, "bar", publicAccess, defaultModifiers, Array.empty) + new Def(Array.empty, simpleStructure(barMethod1), Array.empty, "foo", publicAccess, defaultModifiers, Array.empty) + } + /** def foo: { bar: String } */ + val fooMethod2 = { + val barMethod2 = new Def(Array.empty, strTpe, Array.empty, "bar", publicAccess, defaultModifiers, Array.empty) + new Def(Array.empty, simpleStructure(barMethod2), Array.empty, "foo", publicAccess, defaultModifiers, Array.empty) + } + val aClass1 = simpleClass("A", fooMethod1) + val aClass2 = simpleClass("A", fooMethod2) + val nameHashes1 = nameHashesForClass(aClass1) + val nameHashes2 = nameHashesForClass(aClass2) + // note that `bar` does appear here + Seq("A", "foo", "bar") === nameHashes1.regularMembers.map(_.name).toSeq + Seq("A", "foo", "bar") === nameHashes2.regularMembers.map(_.name).toSeq + assertNameHashEqualForRegularName("A", nameHashes1, nameHashes2) + assertNameHashNotEqualForRegularName("foo", nameHashes1, nameHashes2) + assertNameHashNotEqualForRegularName("bar", nameHashes1, nameHashes2) + } + + private def assertNameHashEqualForRegularName(name: String, nameHashes1: _internalOnly_NameHashes, + nameHashes2: _internalOnly_NameHashes): Unit = { + val nameHash1 = nameHashForRegularName(nameHashes1, name) + val nameHash2 = nameHashForRegularName(nameHashes1, name) + nameHash1 === nameHash2 + } + + private def assertNameHashNotEqualForRegularName(name: String, nameHashes1: _internalOnly_NameHashes, + nameHashes2: _internalOnly_NameHashes): Unit = { + val nameHash1 = nameHashForRegularName(nameHashes1, name) + val nameHash2 = nameHashForRegularName(nameHashes2, name) + nameHash1 !=== nameHash2 + } + + private def nameHashForRegularName(nameHashes: _internalOnly_NameHashes, name: String): _internalOnly_NameHash = + try { + nameHashes.regularMembers.find(_.name == name).get + } catch { + case e: NoSuchElementException => throw new RuntimeException(s"Couldn't find $name in $nameHashes", e) + } + + private def nameHashesForClass(cl: ClassLike): _internalOnly_NameHashes = { + val sourceAPI = new SourceAPI(Array.empty, Array(cl)) + val nameHashing = new NameHashing + nameHashing.nameHashes(sourceAPI) + } + + private def lzy[T](x: T): Lazy[T] = new Lazy[T] { def get: T = x } + + private def simpleStructure(defs: Definition*) = new Structure(lzy(Array.empty[Type]), lzy(defs.toArray), lzy(Array.empty[Definition])) + + private def simpleClass(name: String, defs: Definition*): ClassLike = { + val structure = simpleStructure(defs: _*) + simpleClass(name, structure) + } + + private def simpleClass(name: String, structure: Structure): ClassLike = { + new ClassLike(DefinitionType.ClassDef, lzy(emptyType), lzy(structure), Array.empty, Array.empty, name, publicAccess, defaultModifiers, Array.empty) + } + + private val emptyType = new EmptyType + private val intTpe = new Projection(emptyType, "Int") + private val strTpe = new Projection(emptyType, "String") + private val publicAccess = new Public + private val defaultModifiers = new Modifiers(false, false, false, false, false, false, false) + +} diff --git a/compile/inc/src/test/scala/sbt/inc/TestCaseGenerators.scala b/compile/inc/src/test/scala/sbt/inc/TestCaseGenerators.scala index f00ff8303..ea818871e 100644 --- a/compile/inc/src/test/scala/sbt/inc/TestCaseGenerators.scala +++ b/compile/inc/src/test/scala/sbt/inc/TestCaseGenerators.scala @@ -71,13 +71,38 @@ object TestCaseGenerators { private [this] def lzy[T <: AnyRef](x: T) = SafeLazy.strict(x) + def genNameHash(defn: String): Gen[xsbti.api._internalOnly_NameHash] = + value(new xsbti.api._internalOnly_NameHash(defn, defn.hashCode())) + + def genNameHashes(defns: Seq[String]): Gen[xsbti.api._internalOnly_NameHashes] = { + def partitionAccordingToMask[T](mask: List[Boolean], xs: List[T]): (List[T], List[T]) = { + val (p1, p2) = (mask zip xs).partition(_._1) + (p1.map(_._2), p2.map(_._2)) + } + val pairsOfGenerators = for (defn <- defns) yield { + for { + isRegularMember <- arbitrary[Boolean] + nameHash <- genNameHash(defn) + } yield (isRegularMember, nameHash) + } + val genNameHashesList = Gen.sequence[List, xsbti.api._internalOnly_NameHash](defns.map(genNameHash)) + val genTwoListOfNameHashes = for { + nameHashesList <- genNameHashesList + isRegularMemberList <- listOfN(nameHashesList.length, arbitrary[Boolean]) + } yield partitionAccordingToMask(isRegularMemberList, nameHashesList) + for { + (regularMemberNameHashes, implicitMemberNameHashes) <- genTwoListOfNameHashes + } yield new xsbti.api._internalOnly_NameHashes(regularMemberNameHashes.toArray, implicitMemberNameHashes.toArray) + } + def genSource(defns: Seq[String]): Gen[Source] = for { startTime <- arbitrary[Long] hashLen <- choose(10, 20) // Requred by SameAPI to be > 0. hash <- Gen.containerOfN[Array,Byte](hashLen, arbitrary[Byte]) apiHash <- arbitrary[Int] hasMacro <- arbitrary[Boolean] - } yield new Source(new Compilation(startTime, Array()), hash, new SourceAPI(Array(), Array(defns map makeDefinition:_*)), apiHash, hasMacro) + nameHashes <- genNameHashes(defns) + } yield new Source(new Compilation(startTime, Array()), hash, new SourceAPI(Array(), Array(defns map makeDefinition:_*)), apiHash, nameHashes, hasMacro) def genSources(all_defns: Seq[Seq[String]]): Gen[Seq[Source]] = Gen.sequence[List, Source](all_defns.map(genSource)) diff --git a/interface/other b/interface/other index 111896f0b..68e4c3a50 100644 --- a/interface/other +++ b/interface/other @@ -3,8 +3,17 @@ Source hash: Byte* api: SourceAPI apiHash: Int + _internalOnly_nameHashes: _internalOnly_NameHashes hasMacro: Boolean +_internalOnly_NameHashes + regularMembers: _internalOnly_NameHash* + implicitMembers: _internalOnly_NameHash* + +_internalOnly_NameHash + name: String + hash: Int + SourceAPI packages : Package* definitions: Definition* From fa220f372a7c85d0bb1d32b39a40211d016ea587 Mon Sep 17 00:00:00 2001 From: Grzegorz Kossakowski Date: Wed, 4 Dec 2013 12:49:15 +0100 Subject: [PATCH 2/3] Add overloaded constructor to HashAPI for backwards compatibility. Add a variant of constructor to `HashAPI` that is binary and source backwards compatible with sbt 0.13.0. --- compile/api/src/main/scala/xsbt/api/HashAPI.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/compile/api/src/main/scala/xsbt/api/HashAPI.scala b/compile/api/src/main/scala/xsbt/api/HashAPI.scala index b48394910..7927ace69 100644 --- a/compile/api/src/main/scala/xsbt/api/HashAPI.scala +++ b/compile/api/src/main/scala/xsbt/api/HashAPI.scala @@ -38,6 +38,13 @@ object HashAPI */ final class HashAPI(includePrivate: Boolean, includeParamNames: Boolean, includeDefinitions: Boolean) { + // this constructor variant is for source and binary backwards compatibility with sbt 0.13.0 + def this(includePrivate: Boolean, includeParamNames: Boolean) { + // in the old logic we used to always include definitions hence + // includeDefinitions=true + this(includePrivate, includeParamNames, includeDefinitions=true) + } + import scala.collection.mutable import MurmurHash.{extendHash, finalizeHash, nextMagicA, nextMagicB, startHash, startMagicA, startMagicB, stringHash, symmetricHash} From 1bbbbb38c9dbaee49a7ba02dc45f70d361925aea Mon Sep 17 00:00:00 2001 From: Grzegorz Kossakowski Date: Wed, 4 Dec 2013 12:50:29 +0100 Subject: [PATCH 3/3] Get rid of MurmurHash.finalizeHash import in HashAPI. That import is shadowed by the local definition of `finalizeHash` introduced to HashAPI class. --- compile/api/src/main/scala/xsbt/api/HashAPI.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compile/api/src/main/scala/xsbt/api/HashAPI.scala b/compile/api/src/main/scala/xsbt/api/HashAPI.scala index 7927ace69..dae3a5a00 100644 --- a/compile/api/src/main/scala/xsbt/api/HashAPI.scala +++ b/compile/api/src/main/scala/xsbt/api/HashAPI.scala @@ -46,7 +46,7 @@ final class HashAPI(includePrivate: Boolean, includeParamNames: Boolean, include } import scala.collection.mutable - import MurmurHash.{extendHash, finalizeHash, nextMagicA, nextMagicB, startHash, startMagicA, startMagicB, stringHash, symmetricHash} + import MurmurHash.{extendHash, nextMagicA, nextMagicB, startHash, startMagicA, startMagicB, stringHash, symmetricHash} private[this] val visitedStructures = visitedMap[Structure] private[this] val visitedClassLike = visitedMap[ClassLike]