diff --git a/compile/api/src/main/scala/xsbt/api/HashAPI.scala b/compile/api/src/main/scala/xsbt/api/HashAPI.scala index 451f046de..dae3a5a00 100644 --- a/compile/api/src/main/scala/xsbt/api/HashAPI.scala +++ b/compile/api/src/main/scala/xsbt/api/HashAPI.scala @@ -12,13 +12,41 @@ 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) { + // 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} + import MurmurHash.{extendHash, nextMagicA, nextMagicB, startHash, startMagicA, startMagicB, stringHash, symmetricHash} private[this] val visitedStructures = visitedMap[Structure] private[this] val visitedClassLike = visitedMap[ClassLike] @@ -44,7 +72,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 +103,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 +121,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 +135,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 +145,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 +155,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 +194,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 +251,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 +269,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 +292,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 +309,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 +372,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 +389,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*