mirror of https://github.com/sbt/sbt.git
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
This commit is contained in:
parent
ec40eab92d
commit
a9a709ccc0
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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*
|
||||
|
|
|
|||
Loading…
Reference in New Issue