Represent api changes as values and cleanup APIChanges class.

The main motivation behind this commit is to reify information about
api changes that incremental compiler considers. We introduce a new
sealed class `APIChange` that has (at the moment) two subtypes:

  * APIChangeDueToMacroDefinition - as the name explains, this represents
    the case where incremental compiler considers an api to be changed
    just because given source file contains a macro definition
  * SourceAPIChange - this represents the case of regular api change;
    at the moment it's just a simple wrapper around value representing
    source file but in the future it will get expanded to contain more
    detailed information about API changes (e.g. collection of changed
    name hashes)

The APIChanges becomes just a collection of APIChange instances.
In particular, I removed `names` field that seems to be a dead code in
incremental compiler. The `NameChanges` class and methods that refer to
it in `SameAPI` has been deprecated.

The Incremental.scala has been adapted to changed signature of APIChanges
class. The `sameSource` method returns representation of APIChange
(if there's one) instead of just simple boolean. One notable change is
that information about APIChanges is pushed deeper into invalidation logic.
This will allow us to treat the APIChangeDueToMacroDefinition case properly
once name hashing scheme arrives.

This commit shouldn't change any behavior and is purely a refactoring.
This commit is contained in:
Grzegorz Kossakowski 2013-10-29 13:20:12 +01:00
parent 4ed8abd4fb
commit 4b43110a2c
3 changed files with 58 additions and 31 deletions

View File

@ -8,6 +8,7 @@ import xsbti.api._
import Function.tupled
import scala.collection.{immutable, mutable}
@deprecated("This class is not used in incremental compiler and will be removed in next major version.", "0.13.2")
class NameChanges(val newTypes: Set[String], val removedTypes: Set[String], val newTerms: Set[String], val removedTerms: Set[String])
{
override def toString =
@ -19,11 +20,13 @@ class NameChanges(val newTypes: Set[String], val removedTypes: Set[String], val
object TopLevel
{
@deprecated("The NameChanges class is deprecated and will be removed in next major version.", "0.13.2")
def nameChanges(a: Iterable[Source], b: Iterable[Source]): NameChanges = {
val api = (_: Source).api
apiNameChanges(a map api, b map api)
}
/** Identifies removed and new top-level definitions by name. */
@deprecated("The NameChanges class is deprecated and will be removed in next major version.", "0.13.2")
def apiNameChanges(a: Iterable[SourceAPI], b: Iterable[SourceAPI]): NameChanges =
{
def changes(s: Set[String], t: Set[String]) = (s -- t, t -- s)

View File

@ -8,11 +8,21 @@ import xsbt.api.NameChanges
import java.io.File
final case class InitialChanges(internalSrc: Changes[File], removedProducts: Set[File], binaryDeps: Set[File], external: APIChanges[String])
final class APIChanges[T](val modified: Set[T], val names: NameChanges)
final class APIChanges[T](val apiChanges: Iterable[APIChange[T]])
{
override def toString = "API Changes: " + modified + "\n" + names
override def toString = "API Changes: " + apiChanges
def allModified: Iterable[T] = apiChanges.map(_.modified)
}
sealed abstract class APIChange[T](val modified: T)
/**
* If we recompile a source file that contains a macro definition then we always assume that it's
* api has changed. The reason is that there's no way to determine if changes to macros implementation
* are affecting its users or not. Therefore we err on the side of caution.
*/
case class APIChangeDueToMacroDefinition[T](modified0: T) extends APIChange(modified0)
case class SourceAPIChange[T](modified0: T) extends APIChange(modified0)
trait Changes[A]
{
def added: Set[A]
@ -24,4 +34,4 @@ trait Changes[A]
sealed abstract class Change(val file: File)
final class Removed(f: File) extends Change(f)
final class Added(f: File, newStamp: Stamp) extends Change(f)
final class Modified(f: File, oldStamp: Stamp, newStamp: Stamp) extends Change(f)
final class Modified(f: File, oldStamp: Stamp, newStamp: Stamp) extends Change(f)

View File

@ -24,7 +24,7 @@ object Incremental
val initialChanges = changedInitial(entry, sources, previous, current, forEntry, options, log)
val binaryChanges = new DependencyChanges {
val modifiedBinaries = initialChanges.binaryDeps.toArray
val modifiedClasses = initialChanges.external.modified.toArray
val modifiedClasses = initialChanges.external.allModified.toArray
def isEmpty = modifiedBinaries.isEmpty && modifiedClasses.isEmpty
}
val initialInv = invalidateInitial(previous.relations, initialChanges, log)
@ -79,7 +79,7 @@ object Incremental
val incChanges = changedIncremental(invalidated, previous.apis.internalAPI _, merged.apis.internalAPI _, log, options)
debug("\nChanges:\n" + incChanges)
val transitiveStep = options.transitiveStep
val incInv = invalidateIncremental(merged.relations, incChanges, invalidated, cycleNum >= transitiveStep, log)
val incInv = invalidateIncremental(merged.relations, merged.apis, incChanges, invalidated, cycleNum >= transitiveStep, log)
cycle(incInv, allSources, emptyChanges, merged, doCompile, classfileManager, cycleNum+1, log, options)
}
private[this] def emptyChanges: DependencyChanges = new DependencyChanges {
@ -106,15 +106,20 @@ object Incremental
*
* NOTE: This method creates a new APIDiff instance on every invocation.
*/
private def logApiChanges[T](changes: (collection.Set[T], Seq[Source], Seq[Source]), log: Logger,
options: IncOptions): Unit = {
private def logApiChanges[T](apiChanges: Iterable[APIChange[T]], oldAPIMapping: T => Source,
newAPIMapping: T => Source, log: Logger, options: IncOptions): Unit = {
val contextSize = options.apiDiffContextSize
try {
val apiDiff = new APIDiff
changes.zipped foreach {
case (src, oldApi, newApi) =>
apiChanges foreach {
case APIChangeDueToMacroDefinition(src) =>
log.debug(s"Public API is considered to be changed because $src contains a macro definition.")
case SourceAPIChange(src) =>
val oldApi = oldAPIMapping(src)
val newApi = newAPIMapping(src)
val apiUnifiedPatch = apiDiff.generateApiDiff(src.toString, oldApi.api, newApi.api, contextSize)
log.debug("Detected a change in a public API:\n" + apiUnifiedPatch)
log.debug(s"Detected a change in a public API (${src.toString}):\n"
+ apiUnifiedPatch)
}
} catch {
case e: ClassNotFoundException =>
@ -137,26 +142,32 @@ object Incremental
{
val oldApis = lastSources.toSeq map oldAPI
val newApis = lastSources.toSeq map newAPI
val changes = (lastSources, oldApis, newApis).zipped.filter { (src, oldApi, newApi) => !sameSource(src, oldApi, newApi, log) }
val apiChanges = (lastSources, oldApis, newApis).zipped.flatMap { (src, oldApi, newApi) => sameSource(src, oldApi, newApi, log) }
if (apiDebug(options) && changes.zipped.nonEmpty) {
logApiChanges(changes, log, options)
if (apiDebug(options) && apiChanges.nonEmpty) {
logApiChanges(apiChanges, oldAPI, newAPI, log, options)
}
val changedNames = TopLevel.nameChanges(changes._3, changes._2 )
val modifiedAPIs = changes._1.toSet
new APIChanges(modifiedAPIs, changedNames)
new APIChanges(apiChanges)
}
def sameSource[T](src: T, a: Source, b: Source, log: Logger): Boolean = {
def sameSource[T](src: T, a: Source, b: Source, log: Logger): Option[APIChange[T]] = {
// Clients of a modified source file (ie, one that doesn't satisfy `shortcutSameSource`) containing macros must be recompiled.
val hasMacro = a.hasMacro || b.hasMacro
shortcutSameSource(a, b) || {
if (shortcutSameSource(a, b)) {
None
} else {
if (hasMacro) {
log.debug("API is considered to be modified because the following source file contains a macro: " + src)
false
} else SameAPI(a,b)
Some(APIChangeDueToMacroDefinition(src))
} else sameAPI(src, a, b, log)
}
}
def sameAPI[T](src: T, a: Source, b: Source, log: Logger): Option[SourceAPIChange[T]] = {
if (SameAPI(a,b))
None
else {
val sourceApiChange = SourceAPIChange(src)
Some(sourceApiChange)
}
}
@ -188,14 +199,14 @@ object Incremental
val (changed, unmodified) = inBoth.partition(existingModified)
}
def invalidateIncremental(previous: Relations, changes: APIChanges[File], recompiledSources: Set[File], transitive: Boolean, log: Logger): Set[File] =
def invalidateIncremental(previous: Relations, apis: APIs, changes: APIChanges[File], recompiledSources: Set[File], transitive: Boolean, log: Logger): Set[File] =
{
val dependsOnSrc = previous.usesInternalSrc _
val propagated =
if(transitive)
transitiveDependencies(dependsOnSrc, changes.modified, log)
transitiveDependencies(dependsOnSrc, changes.allModified.toSet, log)
else
invalidateIntermediate(previous, changes.modified, log)
invalidateIntermediate(previous, changes, log)
val dups = invalidateDuplicates(previous)
if(dups.nonEmpty)
@ -231,12 +242,13 @@ object Incremental
val srcDirect = srcChanges.removed ++ srcChanges.removed.flatMap(previous.usesInternalSrc) ++ srcChanges.added ++ srcChanges.changed
val byProduct = changes.removedProducts.flatMap(previous.produced)
val byBinaryDep = changes.binaryDeps.flatMap(previous.usesBinary)
val byExtSrcDep = invalidateByExternal(previous, changes.external.modified, log) //changes.external.modified.flatMap(previous.usesExternal) // ++ scopeInvalidations
val externalModifiedSources = changes.external.allModified.toSet
val byExtSrcDep = invalidateByExternal(previous, externalModifiedSources, log) //changes.external.modified.flatMap(previous.usesExternal) // ++ scopeInvalidations
checkAbsolute(srcChanges.added.toList)
log.debug(
"\nInitial source changes: \n\tremoved:" + srcChanges.removed + "\n\tadded: " + srcChanges.added + "\n\tmodified: " + srcChanges.changed +
"\nRemoved products: " + changes.removedProducts +
"\nModified external sources: " + changes.external.modified +
"\nModified external sources: " + externalModifiedSources +
"\nModified binary dependencies: " + changes.binaryDeps +
"\nInitial directly invalidated sources: " + srcDirect +
"\n\nSources indirectly invalidated by:" +
@ -278,15 +290,16 @@ object Incremental
transitiveInherited ++ directA ++ directB
}
/** Intermediate invalidation step: steps after the initial invalidation, but before the final transitive invalidation. */
def invalidateIntermediate(relations: Relations, modified: Set[File], log: Logger): Set[File] =
def invalidateIntermediate(relations: Relations, changes: APIChanges[File], log: Logger): Set[File] =
{
def reverse(r: Relations.Source) = r.internal.reverse _
invalidateSources(reverse(relations.direct), reverse(relations.publicInherited), modified, log)
invalidateSources(reverse(relations.direct), reverse(relations.publicInherited), changes, log)
}
/** Invalidates inheritance dependencies, transitively. Then, invalidates direct dependencies. Finally, excludes initial dependencies not
* included in a cycle with newly invalidated sources. */
private[this] def invalidateSources(directDeps: File => Set[File], publicInherited: File => Set[File], initial: Set[File], log: Logger): Set[File] =
private[this] def invalidateSources(directDeps: File => Set[File], publicInherited: File => Set[File], changes: APIChanges[File], log: Logger): Set[File] =
{
val initial = changes.allModified.toSet
log.debug("Invalidating by inheritance (transitively)...")
val transitiveInherited = transitiveDeps(initial, log)(publicInherited)
log.debug("Invalidated by transitive public inheritance: " + transitiveInherited)
@ -295,6 +308,7 @@ object Incremental
val all = transitiveInherited ++ direct
includeInitialCond(initial, all, f => directDeps(f) ++ publicInherited(f), log)
}
/** Conditionally include initial sources that are dependencies of newly invalidated sources.
** Initial sources included in this step can be because of a cycle, but not always. */
private[this] def includeInitialCond(initial: Set[File], currentInvalidations: Set[File], allDeps: File => Set[File], log: Logger): Set[File] =