KT-45777: Allow 2 levels of granularity when tracking changes

1. CLASS_LEVEL: allows tracking whether a .class file has changed
     without tracking what specific parts of the .class file (e.g.,
     fields or methods) have changed.

  2. CLASS_MEMBER_LEVEL: allows tracking not only whether a .class file
     has changed but also what specific parts of the .class file (e.g.,
     fields or methods) have changed.

 The idea is that for better performance we will use CLASS_LEVEL for
 classpath entries that are usually unchanged, and CLASS_MEMBER_LEVEL
 for classpath entries that are frequently changed. We'll work out the
 specifics in a following commit after some measurements.

Support running kotlinc on Windows in ClasspathSnapshotTestCommon
Also add tests for different Kotlin class kinds.
Add unit tests for CLASS_LEVEL snapshotting and diffing

Test: Updated ClasspathSnapshotterTest + ClasspathChangesComputerTest
Add ClasspathChangesComputerTest.testMixedClassSnapshotGranularities
This commit is contained in:
Hung Nguyen
2022-01-17 19:06:04 +00:00
committed by nataliya.valtman
parent 05457c291d
commit d2193f3873
76 changed files with 1170 additions and 566 deletions
+2
View File
@@ -1,6 +1,8 @@
<component name="ProjectDictionaryState">
<dictionary name="hungnv">
<words>
<w>externalizers</w>
<w>granularities</w>
<w>multifile</w>
<w>shrinker</w>
<w>snapshotter</w>
@@ -35,33 +35,33 @@ fun File.isClassFile(): Boolean =
*/
fun File.cleanDirectoryContents() {
when {
isDirectory -> listFiles()!!.forEach { it.forceDeleteRecursively() }
isDirectory -> listFiles()!!.forEach { it.deleteRecursivelyOrThrow() }
isFile -> error("File.cleanDirectoryContents does not accept a regular file: $path")
else -> forceMkdirs()
else -> mkdirsOrThrow()
}
}
/** Deletes this file or directory recursively (if it exists). */
fun File.forceDeleteRecursively() {
/** Deletes this file or directory recursively (if it exists), throwing an exception if the deletion failed. */
fun File.deleteRecursivelyOrThrow() {
if (!deleteRecursively()) {
throw IOException("Could not delete '$path'")
}
}
/**
* Creates this directory (if it does not yet exist).
*
* If this is a regular file, this method will throw an exception.
* Creates this directory (if it does not yet exist), throwing an exception if the directiory creation failed or if a regular file already
* exists at this path.
*/
@Suppress("SpellCheckingInspection")
fun File.forceMkdirs() {
fun File.mkdirsOrThrow() {
when {
this.isDirectory -> { /* Do nothing */ }
this.isFile -> error("File.forceMkdirs does not accept a regular file: $path")
isDirectory -> Unit
isFile -> error("A regular file already exists at this path: $path")
else -> {
// Note that if the directory already exists, mkdirs() will return `false`, but here we ensure that the directory does not exist
// before calling mkdirs(), so it's safe to check the returned result of mkdirs() below.
if (!mkdirs()) {
// Note that `mkdirs()` returns `false` if the directory already exists (even though we have checked that the directory did not
// exist earlier, it might just have been created by some other thread). Therefore, we need to check if the directory exists
// again when `mkdirs()` returns `false`.
if (!mkdirs() && !isDirectory) {
throw IOException("Could not create directory '$path'")
}
}
@@ -346,7 +346,7 @@ object ByteArrayExternalizer : DataExternalizer<ByteArray> {
}
}
open class GenericCollectionExternalizer<T, C : Collection<T>>(
abstract class GenericCollectionExternalizer<T, C : Collection<T>>(
private val elementExternalizer: DataExternalizer<T>,
private val newCollection: (size: Int) -> MutableCollection<T>
) : DataExternalizer<C> {
@@ -364,6 +364,9 @@ open class GenericCollectionExternalizer<T, C : Collection<T>>(
repeat(size) {
collection.add(elementExternalizer.read(input))
}
// We want `collection` to be both a mutable collection (so we can add elements to it as done above) and a type that can be safely
// converted to type `C` (to be used as the returned value of this method). However, there is no type-safe way to express that, so
// we have to use this unsafe cast.
@Suppress("UNCHECKED_CAST")
return collection as C
}
@@ -375,12 +378,13 @@ class ListExternalizer<T>(elementExternalizer: DataExternalizer<T>) :
class SetExternalizer<T>(elementExternalizer: DataExternalizer<T>) :
GenericCollectionExternalizer<T, Set<T>>(elementExternalizer, { size -> LinkedHashSet(size) })
class LinkedHashMapExternalizer<K, V>(
open class MapExternalizer<K, V, M : Map<K, V>>(
private val keyExternalizer: DataExternalizer<K>,
private val valueExternalizer: DataExternalizer<V>
) : DataExternalizer<LinkedHashMap<K, V>> {
private val valueExternalizer: DataExternalizer<V>,
private val newMap: (size: Int) -> MutableMap<K, V> = { size -> LinkedHashMap(size) }
) : DataExternalizer<M> {
override fun save(output: DataOutput, map: LinkedHashMap<K, V>) {
override fun save(output: DataOutput, map: M) {
output.writeInt(map.size)
for ((key, value) in map) {
keyExternalizer.save(output, key)
@@ -388,14 +392,20 @@ class LinkedHashMapExternalizer<K, V>(
}
}
override fun read(input: DataInput): LinkedHashMap<K, V> {
override fun read(input: DataInput): M {
val size = input.readInt()
val map = LinkedHashMap<K, V>(size)
val map = newMap(size)
repeat(size) {
val key = keyExternalizer.read(input)
val value = valueExternalizer.read(input)
map[key] = value
}
return map
@Suppress("UNCHECKED_CAST")
return map as M
}
}
class LinkedHashMapExternalizer<K, V>(
keyExternalizer: DataExternalizer<K>,
valueExternalizer: DataExternalizer<V>
) : MapExternalizer<K, V, LinkedHashMap<K, V>>(keyExternalizer, valueExternalizer, { size -> LinkedHashMap(size) })
@@ -216,7 +216,7 @@ abstract class IncrementalCompilerRunner<
}
it.isFile -> {
reporter.reportVerbose { " Deleting file '${it.path}'" }
it.forceDeleteRecursively()
it.deleteRecursivelyOrThrow()
}
}
}
@@ -52,6 +52,8 @@ import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotEnable
import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotEnabled.NotAvailableForNonIncrementalRun
import org.jetbrains.kotlin.incremental.ClasspathChanges.NotAvailableForJSCompiler
import org.jetbrains.kotlin.incremental.classpathDiff.*
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathChangesComputer.computeChangedAndImpactedSet
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinker.shrinkClasspath
import org.jetbrains.kotlin.incremental.components.ExpectActualTracker
import org.jetbrains.kotlin.incremental.components.LookupTracker
import org.jetbrains.kotlin.incremental.multiproject.EmptyModulesApiHistory
@@ -219,8 +221,8 @@ class IncrementalJvmCompilerRunner(
// Used by `calculateSourcesToCompileImpl` and `performWorkAfterSuccessfulCompilation` methods below.
// Thread safety: There is no concurrent access to these variables.
private var currentClasspathSnapshot: List<ClassSnapshotWithHash>? = null
private var shrunkCurrentClasspathAgainstPreviousLookups: List<ClassSnapshotWithHash>? = null
private var currentClasspathSnapshot: List<AccessibleClassSnapshot>? = null
private var shrunkCurrentClasspathAgainstPreviousLookups: List<AccessibleClassSnapshot>? = null
private fun calculateSourcesToCompileImpl(
caches: IncrementalJvmCachesManager,
@@ -245,9 +247,15 @@ class IncrementalJvmCompilerRunner(
}
check(shrunkCurrentClasspathAgainstPreviousLookups == null)
shrunkCurrentClasspathAgainstPreviousLookups = reporter.measure(BuildTime.SHRINK_CURRENT_CLASSPATH_SNAPSHOT) {
ClasspathSnapshotShrinker.shrink(currentClasspathSnapshot!!, caches.lookupCache, reporter)
shrinkClasspath(
currentClasspathSnapshot!!, caches.lookupCache,
ClasspathSnapshotShrinker.MetricsReporter(
reporter,
BuildTime.GET_LOOKUP_SYMBOLS, BuildTime.FIND_REFERENCED_CLASSES, BuildTime.FIND_TRANSITIVELY_REFERENCED_CLASSES
)
)
}
ClasspathChangesComputer.computeChangedAndImpactedSet(
computeChangedAndImpactedSet(
shrunkCurrentClasspathAgainstPreviousLookups!!,
classpathChanges.classpathSnapshotFiles.shrunkPreviousClasspathSnapshotFile,
reporter
@@ -49,6 +49,12 @@ class ChangeSet(
fun addChangedClassMember(className: ClassId, memberName: String) = addChangedClassMembers(className, listOf(memberName))
fun addChangedTopLevelMembers(packageMemberSet: PackageMemberSet) {
packageMemberSet.packageToMembersMap.forEach { (packageName, memberNames) ->
changedTopLevelMembers.computeIfAbsent(packageName) { mutableSetOf() }.addAll(memberNames)
}
}
fun addChangedTopLevelMembers(packageName: FqName, topLevelMembers: Collection<String>) {
if (topLevelMembers.isNotEmpty()) {
changedTopLevelMembers.computeIfAbsent(packageName) { mutableSetOf() }.addAll(topLevelMembers)
@@ -30,12 +30,12 @@ object ClasspathChangesComputer {
* NOTE: The original classpath may contain duplicate classes, but the shrunk classpath must not contain duplicate classes.
*/
fun computeChangedAndImpactedSet(
shrunkCurrentClasspathSnapshot: List<ClassSnapshotWithHash>,
shrunkCurrentClasspathSnapshot: List<AccessibleClassSnapshot>,
shrunkPreviousClasspathSnapshotFile: File,
metrics: BuildMetricsReporter
): ChangeSet {
val shrunkPreviousClasspathSnapshot = metrics.measure(BuildTime.LOAD_SHRUNK_PREVIOUS_CLASSPATH_SNAPSHOT) {
ListExternalizer(ClassSnapshotWithHashExternalizer).loadFromFile(shrunkPreviousClasspathSnapshotFile)
ListExternalizer(AccessibleClassSnapshotExternalizer).loadFromFile(shrunkPreviousClasspathSnapshotFile)
}
return metrics.measure(BuildTime.COMPUTE_CHANGED_AND_IMPACTED_SET) {
computeChangedAndImpactedSet(shrunkCurrentClasspathSnapshot, shrunkPreviousClasspathSnapshot, metrics)
@@ -48,22 +48,26 @@ object ClasspathChangesComputer {
* NOTE: Each list of classes must not contain duplicates.
*/
fun computeChangedAndImpactedSet(
currentClassSnapshots: List<ClassSnapshotWithHash>,
previousClassSnapshots: List<ClassSnapshotWithHash>,
currentClassSnapshots: List<AccessibleClassSnapshot>,
previousClassSnapshots: List<AccessibleClassSnapshot>,
metrics: BuildMetricsReporter
): ChangeSet {
val currentClasses: Map<ClassId, ClassSnapshotWithHash> = currentClassSnapshots.associateBy { it.classSnapshot.getClassId() }
val previousClasses: Map<ClassId, ClassSnapshotWithHash> = previousClassSnapshots.associateBy { it.classSnapshot.getClassId() }
val currentClasses: Map<ClassId, AccessibleClassSnapshot> = currentClassSnapshots.associateBy { it.classId }
val previousClasses: Map<ClassId, AccessibleClassSnapshot> = previousClassSnapshots.associateBy { it.classId }
val changedCurrentClasses: List<ClassSnapshot> = currentClasses.filter { (classId, currentClass) ->
val changedCurrentClasses: List<AccessibleClassSnapshot> = currentClasses.mapNotNull { (classId, currentClass) ->
val previousClass = previousClasses[classId]
previousClass == null || currentClass.hash != previousClass.hash
}.map { it.value.classSnapshot }
if (previousClass == null || currentClass.classAbiHash != previousClass.classAbiHash) {
currentClass
} else null
}
val changedPreviousClasses: List<ClassSnapshot> = previousClasses.filter { (classId, previousClass) ->
val changedPreviousClasses: List<AccessibleClassSnapshot> = previousClasses.mapNotNull { (classId, previousClass) ->
val currentClass = currentClasses[classId]
currentClass == null || currentClass.hash != previousClass.hash
}.map { it.value.classSnapshot }
if (currentClass == null || currentClass.classAbiHash != previousClass.classAbiHash) {
previousClass
} else null
}
val classChanges = metrics.measure(BuildTime.COMPUTE_CLASS_CHANGES) {
computeClassChanges(changedCurrentClasses, changedPreviousClasses, metrics)
@@ -74,7 +78,7 @@ object ClasspathChangesComputer {
}
return metrics.measure(BuildTime.COMPUTE_IMPACTED_SET) {
computeImpactedSet(classChanges, previousClasses.map { it.value.classSnapshot })
computeImpactedSet(classChanges, previousClassSnapshots)
}
}
@@ -84,20 +88,13 @@ object ClasspathChangesComputer {
*
* NOTE: Each list of classes must not contain duplicates.
*/
fun computeClassChanges(
currentClassSnapshots: List<ClassSnapshot>,
previousClassSnapshots: List<ClassSnapshot>,
private fun computeClassChanges(
currentClassSnapshots: List<AccessibleClassSnapshot>,
previousClassSnapshots: List<AccessibleClassSnapshot>,
metrics: BuildMetricsReporter
): ChangeSet {
val isKotlinClass: (ClassSnapshot) -> Boolean = {
when (it) {
is KotlinClassSnapshot -> true
is JavaClassSnapshot -> false
is InaccessibleClassSnapshot -> error("Unexpected type (it should have been handled earlier): ${it.javaClass.name}")
}
}
val (currentKotlinClassSnapshots, currentJavaClassSnapshots) = currentClassSnapshots.partition(isKotlinClass)
val (previousKotlinClassSnapshots, previousJavaClassSnapshots) = previousClassSnapshots.partition(isKotlinClass)
val (currentKotlinClassSnapshots, currentJavaClassSnapshots) = currentClassSnapshots.partition { it is KotlinClassSnapshot }
val (previousKotlinClassSnapshots, previousJavaClassSnapshots) = previousClassSnapshots.partition { it is KotlinClassSnapshot }
@Suppress("UNCHECKED_CAST")
val kotlinClassChanges = metrics.measure(BuildTime.COMPUTE_KOTLIN_CLASS_CHANGES) {
@@ -121,6 +118,36 @@ object ClasspathChangesComputer {
private fun computeKotlinClassChanges(
currentClassSnapshots: List<KotlinClassSnapshot>,
previousClassSnapshots: List<KotlinClassSnapshot>
): ChangeSet {
val (coarseGrainedCurrentClassSnapshots, fineGrainedCurrentClassSnapshots) =
currentClassSnapshots.partition { it.classMemberLevelSnapshot == null }
val (coarseGrainedPreviousClassSnapshots, fineGrainedPreviousClassSnapshots) =
previousClassSnapshots.partition { it.classMemberLevelSnapshot == null }
return computeCoarseGrainedKotlinClassChanges(coarseGrainedCurrentClassSnapshots, coarseGrainedPreviousClassSnapshots) +
computeFineGrainedKotlinClassChanges(fineGrainedCurrentClassSnapshots, fineGrainedPreviousClassSnapshots)
}
private fun computeCoarseGrainedKotlinClassChanges(
currentClassSnapshots: List<KotlinClassSnapshot>,
previousClassSnapshots: List<KotlinClassSnapshot>
): ChangeSet {
// Note: We have removed unchanged classes earlier in computeChangedAndImpactedSet method, so here we only have changed classes.
return ChangeSet.Collector().run {
(currentClassSnapshots + previousClassSnapshots).forEach {
when (it) {
is RegularKotlinClassSnapshot -> addChangedClass(it.classId)
is PackageFacadeKotlinClassSnapshot -> addChangedTopLevelMembers(it.packageMembers)
is MultifileClassKotlinClassSnapshot -> addChangedTopLevelMembers(it.constants)
}
}
getChanges()
}
}
private fun computeFineGrainedKotlinClassChanges(
currentClassSnapshots: List<KotlinClassSnapshot>,
previousClassSnapshots: List<KotlinClassSnapshot>
): ChangeSet {
val workingDir =
FileUtil.createTempDirectory(this::class.java.simpleName, "_WorkingDir_${UUID.randomUUID()}", /* deleteOnExit */ true)
@@ -135,11 +162,11 @@ object ClasspathChangesComputer {
val unusedChangesCollector = ChangesCollector()
previousClassSnapshots.forEach {
incrementalJvmCache.saveClassToCache(
kotlinClassInfo = it.classInfo,
kotlinClassInfo = it.classMemberLevelSnapshot!!,
sourceFiles = null,
changesCollector = unusedChangesCollector
)
incrementalJvmCache.markDirty(it.classInfo.className)
incrementalJvmCache.markDirty(it.classMemberLevelSnapshot!!.className)
}
// Step 2:
@@ -152,7 +179,7 @@ object ClasspathChangesComputer {
val changesCollector = ChangesCollector()
currentClassSnapshots.forEach {
incrementalJvmCache.saveClassToCache(
kotlinClassInfo = it.classInfo,
kotlinClassInfo = it.classMemberLevelSnapshot!!,
sourceFiles = null,
changesCollector = changesCollector
)
@@ -172,7 +199,10 @@ object ClasspathChangesComputer {
return dirtyData.normalize(currentClassSnapshots, previousClassSnapshots)
}
private fun DirtyData.normalize(currentClassSnapshots: List<ClassSnapshot>, previousClassSnapshots: List<ClassSnapshot>): ChangeSet {
private fun DirtyData.normalize(
currentClassSnapshots: List<AccessibleClassSnapshot>,
previousClassSnapshots: List<AccessibleClassSnapshot>
): ChangeSet {
val changedLookupSymbols =
dirtyLookupSymbols.filterLookupSymbols(currentClassSnapshots).toSet() +
dirtyLookupSymbols.filterLookupSymbols(previousClassSnapshots)
@@ -216,7 +246,7 @@ internal object ImpactAnalysis {
*
* The returned set is also a [ChangeSet], which includes the given changes plus the impacted ones.
*/
fun computeImpactedSet(changes: ChangeSet, previousClassSnapshots: List<ClassSnapshot>): ChangeSet {
fun computeImpactedSet(changes: ChangeSet, previousClassSnapshots: List<AccessibleClassSnapshot>): ChangeSet {
val classIdToSubclasses = getClassIdToSubclassesMap(previousClassSnapshots)
val impactedClassesResolver = { classId: ClassId -> classIdToSubclasses[classId] ?: emptySet() }
@@ -234,14 +264,14 @@ internal object ImpactAnalysis {
}
}
private fun getClassIdToSubclassesMap(classSnapshots: List<ClassSnapshot>): Map<ClassId, Set<ClassId>> {
val classIds: Set<ClassId> = classSnapshots.map { it.getClassId() }.toSet() // Use Set for presence check
private fun getClassIdToSubclassesMap(classSnapshots: List<AccessibleClassSnapshot>): Map<ClassId, Set<ClassId>> {
val classIds: Set<ClassId> = classSnapshots.map { it.classId }.toSet() // Use Set for presence check
val classNameToClassId = classIds.associateBy { JvmClassName.byClassId(it) }
val classNameToClassIdResolver = { className: JvmClassName -> classNameToClassId[className] }
val classIdToSubclasses = mutableMapOf<ClassId, MutableSet<ClassId>>()
classSnapshots.forEach { classSnapshot ->
val classId = classSnapshot.getClassId()
val classId = classSnapshot.classId
classSnapshot.getSupertypes(classNameToClassIdResolver).forEach { supertype ->
// No need to collect supertypes outside the given set of classes (e.g., "java/lang/Object")
if (supertype in classIds) {
@@ -257,39 +287,36 @@ internal object ImpactAnalysis {
* impacted classes.
*/
fun findImpactedClassesInclusive(classIds: Set<ClassId>, impactedClassesResolver: (ClassId) -> Set<ClassId>): Set<ClassId> {
val visitedClasses = mutableSetOf<ClassId>()
val toVisitClasses = classIds.toMutableSet()
while (toVisitClasses.isNotEmpty()) {
val nextToVisit = mutableSetOf<ClassId>()
toVisitClasses.forEach {
nextToVisit.addAll(impactedClassesResolver.invoke(it))
}
visitedClasses.addAll(toVisitClasses)
toVisitClasses.clear()
toVisitClasses.addAll(nextToVisit - visitedClasses)
}
return visitedClasses
}
}
// Standard Breadth-First Search
val visitedAndToVisitClasses = classIds.toMutableSet()
val classesToVisit = ArrayDeque(classIds)
internal fun ClassSnapshot.getClassId(): ClassId {
return when (this) {
is KotlinClassSnapshot -> classInfo.classId
is JavaClassSnapshot -> classId
is InaccessibleClassSnapshot -> error("Unexpected type (it should have been handled earlier): ${javaClass.name}")
while (classesToVisit.isNotEmpty()) {
val classToVisit = classesToVisit.removeFirst()
val nextClassesToVisit = impactedClassesResolver.invoke(classToVisit) - visitedAndToVisitClasses
visitedAndToVisitClasses.addAll(nextClassesToVisit)
classesToVisit.addAll(nextClassesToVisit)
}
return visitedAndToVisitClasses
}
}
/**
* Returns the [ClassId]s of the supertypes of this class.
* Returns the [ClassId]s of the supertypes of this class (could be empty in some cases).
*
* @param classIdResolver Resolves the [ClassId] from the [JvmClassName] of a supertype. It may return null if the supertype is outside the
* considered set of classes (e.g., "java/lang/Object"). Those supertypes do not need to be included in the returned result.
*/
internal fun ClassSnapshot.getSupertypes(classIdResolver: (JvmClassName) -> ClassId?): List<ClassId> {
internal fun AccessibleClassSnapshot.getSupertypes(classIdResolver: (JvmClassName) -> ClassId?): List<ClassId> {
return when (this) {
is KotlinClassSnapshot -> supertypes.mapNotNull { classIdResolver.invoke(it) }
is RegularKotlinClassSnapshot -> supertypes.mapNotNull { classIdResolver.invoke(it) }
is PackageFacadeKotlinClassSnapshot, is MultifileClassKotlinClassSnapshot -> {
// These classes may have supertypes (e.g., kotlin/collections/ArraysKt (MULTIFILE_CLASS) extends
// kotlin/collections/ArraysKt___ArraysKt (MULTIFILE_CLASS_PART)), but we don't have to use that info during impact analysis
// because those inheritors and supertypes should have the same package names, and in package facades only the package names and
// member names matter.
emptyList()
}
is JavaClassSnapshot -> supertypes.mapNotNull { classIdResolver.invoke(it) }
is InaccessibleClassSnapshot -> error("Unexpected type (it should have been handled earlier): ${javaClass.name}")
}
}
@@ -6,8 +6,8 @@
package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.incremental.KotlinClassInfo
import org.jetbrains.kotlin.incremental.md5
import org.jetbrains.kotlin.incremental.storage.toByteArray
import org.jetbrains.kotlin.incremental.classpathDiff.ClassSnapshotGranularity.CLASS_MEMBER_LEVEL
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader.Kind.*
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
@@ -26,7 +26,7 @@ class ClasspathEntrySnapshot(
* Maps (Unix-style) relative paths of classes to their snapshots. The paths are relative to the containing classpath entry (directory
* or jar).
*/
val classSnapshots: LinkedHashMap<String, ClassSnapshotWithHash>
val classSnapshots: LinkedHashMap<String, ClassSnapshot>
)
/**
@@ -37,70 +37,83 @@ class ClasspathEntrySnapshot(
* `KotlinCompile` task and the task needs to support compile avoidance. For example, this class should contain public method signatures,
* and should not contain private method signatures, or method implementations.
*/
sealed class ClassSnapshot {
sealed class ClassSnapshot
/** Computes the hash of this [ClassSnapshot] and returns a [ClassSnapshotWithHash]. */
val withHash: ClassSnapshotWithHash by lazy {
ClassSnapshotWithHash(this, ClassSnapshotExternalizer.toByteArray(this).md5())
}
}
/** [ClassSnapshot] of an accessible class. See [InaccessibleClassSnapshot] for info on the accessibility of a class. */
sealed class AccessibleClassSnapshot : ClassSnapshot() {
abstract val classId: ClassId
/** Contains a [ClassSnapshot] and its hash. */
class ClassSnapshotWithHash(val classSnapshot: ClassSnapshot, val hash: Long) {
override fun toString() = classSnapshot.toString()
}
/** [ClassSnapshot] of a Kotlin class. */
class KotlinClassSnapshot(
val classInfo: KotlinClassInfo,
val supertypes: List<JvmClassName>,
/**
* Package-level members if this class is a package facade (classInfo.classKind != KotlinClassHeader.Kind.CLASS).
*
* Note that MULTIFILE_CLASS classes do not have proto data, so we can't extract their package-level members even though they are
* package facades.
*
* Therefore, the [packageMembers] property below is not null iff classInfo.classKind !in setOf(CLASS, MULTIFILE_CLASS)
*/
val packageMembers: List<PackageMember>?
) : ClassSnapshot() {
override fun toString() = classInfo.classId.toString()
}
/** [ClassSnapshot] of a Java class. */
class JavaClassSnapshot(
/** [ClassId] of the class. It is part of the class's ABI ([classAbiExcludingMembers]). */
val classId: ClassId,
/** The superclass and interfaces of the class. It is part of the class's ABI ([classAbiExcludingMembers]). */
val supertypes: List<JvmClassName>,
/** [AbiSnapshot] of the class excluding its fields and methods. */
val classAbiExcludingMembers: AbiSnapshot,
/** [AbiSnapshot]s of the class's fields. */
val fieldsAbi: List<AbiSnapshot>,
/** [AbiSnapshot]s of the class's methods. */
val methodsAbi: List<AbiSnapshot>
) : ClassSnapshot() {
val className by lazy {
JvmClassName.byClassId(classId).also {
check(it == JvmClassName.byInternalName(classAbiExcludingMembers.name))
}
}
/** The hash of the class's ABI. */
abstract val classAbiHash: Long
override fun toString() = classId.toString()
}
/** The ABI snapshot of a Java element (e.g., class, field, or method). */
open class AbiSnapshot(
/** [ClassSnapshot] of a Kotlin class. */
sealed class KotlinClassSnapshot : AccessibleClassSnapshot() {
/** Snapshot of this class when [ClassSnapshotGranularity] == [CLASS_MEMBER_LEVEL], null otherwise. */
abstract val classMemberLevelSnapshot: KotlinClassInfo?
}
/** [KotlinClassSnapshot] where class kind == [CLASS]. */
class RegularKotlinClassSnapshot(
override val classId: ClassId,
override val classAbiHash: Long,
override val classMemberLevelSnapshot: KotlinClassInfo?,
val supertypes: List<JvmClassName>
) : KotlinClassSnapshot()
/** [KotlinClassSnapshot] where class kind == [FILE_FACADE] or [MULTIFILE_CLASS_PART]. */
class PackageFacadeKotlinClassSnapshot(
override val classId: ClassId,
override val classAbiHash: Long,
override val classMemberLevelSnapshot: KotlinClassInfo?,
val packageMembers: PackageMemberSet
) : KotlinClassSnapshot()
/**
* [KotlinClassSnapshot] where class kind == [MULTIFILE_CLASS].
*
* NOTE: We have to handle [MULTIFILE_CLASS] differently from [FILE_FACADE] and [MULTIFILE_CLASS_PART] because [MULTIFILE_CLASS] classes
* don't contain proto data. Except for constants (see below), it is actually okay to ignore [MULTIFILE_CLASS] because any change in a
* [MULTIFILE_CLASS] will have an associated change in one of its [MULTIFILE_CLASS_PART]s, so the change will be detected when we analyze
* the [MULTIFILE_CLASS_PART]s.
*
* However, if there is a constant is defined in a [MULTIFILE_CLASS], that constant will have a declared value in the [MULTIFILE_CLASS] but
* not in its [MULTIFILE_CLASS_PART]s. Therefore, we'll need to track constants for [MULTIFILE_CLASS]. (We don't have to do this for inline
* functions or other package members as those are defined in [MULTIFILE_CLASS_PART]s.)
*/
class MultifileClassKotlinClassSnapshot(
override val classId: ClassId,
override val classAbiHash: Long,
override val classMemberLevelSnapshot: KotlinClassInfo?,
val constants: PackageMemberSet
) : KotlinClassSnapshot()
/** [ClassSnapshot] of a Java class. */
class JavaClassSnapshot(
override val classId: ClassId,
override val classAbiHash: Long,
/** Snapshot of this class when [ClassSnapshotGranularity] == [CLASS_MEMBER_LEVEL], null otherwise. */
val classMemberLevelSnapshot: JavaClassMemberLevelSnapshot?,
val supertypes: List<JvmClassName>
) : AccessibleClassSnapshot()
/** Snapshot of a Java class when [ClassSnapshotGranularity] == [CLASS_MEMBER_LEVEL]. */
class JavaClassMemberLevelSnapshot(
/** [JavaElementSnapshot] of the class excluding its fields and methods. */
val classAbiExcludingMembers: JavaElementSnapshot,
/** [JavaElementSnapshot]s of the class's fields. */
val fieldsAbi: List<JavaElementSnapshot>,
/** [JavaElementSnapshot]s of the class's methods. */
val methodsAbi: List<JavaElementSnapshot>
)
/** Snapshot of a Java class or a Java class member (field or method). */
open class JavaElementSnapshot(
/** The name of the Java element. It is part of the Java element's ABI. */
val name: String,
@@ -109,15 +122,16 @@ open class AbiSnapshot(
val abiHash: Long
)
/** TEST-ONLY: An [AbiSnapshot] that is used for testing only and must not be used in production code. */
class AbiSnapshotForTests(
/** TEST-ONLY: A [JavaElementSnapshot] that is used for testing only and must not be used in production code. */
class JavaElementSnapshotForTests(
name: String,
abiHash: Long,
/** The Java element's ABI, captured in a [String]. */
@Suppress("unused") val abiValue: String
@Suppress("unused") // Used by Gson reflection
val abiValue: String
) : AbiSnapshot(name, abiHash)
) : JavaElementSnapshot(name, abiHash)
/**
* [ClassSnapshot] of an inaccessible class.
@@ -126,3 +140,19 @@ class AbiSnapshotForTests(
* will not require recompilation of other source files.
*/
object InaccessibleClassSnapshot : ClassSnapshot()
/** The granularity of a [ClassSnapshot]. */
enum class ClassSnapshotGranularity {
/**
* Snapshotting level that allows tracking whether a .class file has changed without tracking what specific parts of the .class file
* (e.g., fields or methods) have changed.
*/
CLASS_LEVEL,
/**
* Snapshotting level that allows tracking not only whether a .class file has changed but also what specific parts of the .class file
* (e.g., fields or methods) have changed.
*/
CLASS_MEMBER_LEVEL
}
@@ -48,76 +48,119 @@ object CachedClasspathSnapshotSerializer {
}
}
open class DataExternalizerForSealedClass<T>(
val baseClass: Class<T>,
val inheritorClasses: List<Class<out T>>,
val inheritorExternalizers: List<DataExternalizer<*>>
) : DataExternalizer<T> {
override fun save(output: DataOutput, objectToExternalize: T) {
val inheritorClassIndex =
inheritorClasses.indexOfFirst { it.isAssignableFrom(objectToExternalize!!::class.java) }.also { check(it != -1) }
output.writeInt(inheritorClassIndex)
@Suppress("UNCHECKED_CAST")
(inheritorExternalizers[inheritorClassIndex] as DataExternalizer<T>).save(output, objectToExternalize)
}
override fun read(input: DataInput): T {
val inheritorClassIndex = input.readInt()
@Suppress("UNCHECKED_CAST")
return inheritorExternalizers[inheritorClassIndex].read(input) as T
}
}
object ClasspathEntrySnapshotExternalizer : DataExternalizer<ClasspathEntrySnapshot> {
override fun save(output: DataOutput, snapshot: ClasspathEntrySnapshot) {
LinkedHashMapExternalizer(StringExternalizer, ClassSnapshotWithHashExternalizer).save(output, snapshot.classSnapshots)
LinkedHashMapExternalizer(StringExternalizer, ClassSnapshotExternalizer).save(output, snapshot.classSnapshots)
}
override fun read(input: DataInput): ClasspathEntrySnapshot {
return ClasspathEntrySnapshot(
classSnapshots = LinkedHashMapExternalizer(StringExternalizer, ClassSnapshotWithHashExternalizer).read(input)
classSnapshots = LinkedHashMapExternalizer(StringExternalizer, ClassSnapshotExternalizer).read(input)
)
}
}
object ClassSnapshotExternalizer : DataExternalizer<ClassSnapshot> {
object ClassSnapshotExternalizer : DataExternalizerForSealedClass<ClassSnapshot>(
baseClass = ClassSnapshot::class.java,
inheritorClasses = listOf(AccessibleClassSnapshot::class.java, InaccessibleClassSnapshot::class.java),
inheritorExternalizers = listOf(AccessibleClassSnapshotExternalizer, InaccessibleClassSnapshotExternalizer)
)
override fun save(output: DataOutput, snapshot: ClassSnapshot) {
when (snapshot) {
is KotlinClassSnapshot -> {
output.writeString(KotlinClassSnapshot::class.java.name)
KotlinClassSnapshotExternalizer.save(output, snapshot)
}
is JavaClassSnapshot -> {
output.writeString(JavaClassSnapshot::class.java.name)
JavaClassSnapshotExternalizer.save(output, snapshot)
}
is InaccessibleClassSnapshot -> {
output.writeString(InaccessibleClassSnapshot::class.java.name)
InaccessibleClassSnapshotExternalizer.save(output, snapshot)
}
}
}
object AccessibleClassSnapshotExternalizer : DataExternalizerForSealedClass<AccessibleClassSnapshot>(
baseClass = AccessibleClassSnapshot::class.java,
inheritorClasses = listOf(KotlinClassSnapshot::class.java, JavaClassSnapshot::class.java),
inheritorExternalizers = listOf(KotlinClassSnapshotExternalizer, JavaClassSnapshotExternalizer)
)
override fun read(input: DataInput): ClassSnapshot {
return when (val className = input.readString()) {
KotlinClassSnapshot::class.java.name -> KotlinClassSnapshotExternalizer.read(input)
JavaClassSnapshot::class.java.name -> JavaClassSnapshotExternalizer.read(input)
InaccessibleClassSnapshot::class.java.name -> InaccessibleClassSnapshotExternalizer.read(input)
else -> error("Unrecognized class name: $className")
}
}
}
object KotlinClassSnapshotExternalizer : DataExternalizerForSealedClass<KotlinClassSnapshot>(
baseClass = KotlinClassSnapshot::class.java,
inheritorClasses = listOf(
RegularKotlinClassSnapshot::class.java,
PackageFacadeKotlinClassSnapshot::class.java,
MultifileClassKotlinClassSnapshot::class.java
),
inheritorExternalizers = listOf(
RegularKotlinClassSnapshotExternalizer,
PackageFacadeKotlinClassSnapshotExternalizer,
MultifileClassKotlinClassSnapshotExternalizer
)
)
object ClassSnapshotWithHashExternalizer : DataExternalizer<ClassSnapshotWithHash> {
object RegularKotlinClassSnapshotExternalizer : DataExternalizer<RegularKotlinClassSnapshot> {
override fun save(output: DataOutput, snapshot: ClassSnapshotWithHash) {
ClassSnapshotExternalizer.save(output, snapshot.classSnapshot)
LongExternalizer.save(output, snapshot.hash)
}
override fun read(input: DataInput): ClassSnapshotWithHash {
return ClassSnapshotWithHash(
classSnapshot = ClassSnapshotExternalizer.read(input),
hash = LongExternalizer.read(input)
)
}
}
object KotlinClassSnapshotExternalizer : DataExternalizer<KotlinClassSnapshot> {
override fun save(output: DataOutput, snapshot: KotlinClassSnapshot) {
KotlinClassInfoExternalizer.save(output, snapshot.classInfo)
override fun save(output: DataOutput, snapshot: RegularKotlinClassSnapshot) {
ClassIdExternalizer.save(output, snapshot.classId)
LongExternalizer.save(output, snapshot.classAbiHash)
NullableValueExternalizer(KotlinClassInfoExternalizer).save(output, snapshot.classMemberLevelSnapshot)
ListExternalizer(JvmClassNameExternalizer).save(output, snapshot.supertypes)
NullableValueExternalizer(ListExternalizer(PackageMemberExternalizer)).save(output, snapshot.packageMembers)
}
override fun read(input: DataInput): KotlinClassSnapshot {
return KotlinClassSnapshot(
classInfo = KotlinClassInfoExternalizer.read(input),
supertypes = ListExternalizer(JvmClassNameExternalizer).read(input),
packageMembers = NullableValueExternalizer(ListExternalizer(PackageMemberExternalizer)).read(input)
override fun read(input: DataInput): RegularKotlinClassSnapshot {
return RegularKotlinClassSnapshot(
classId = ClassIdExternalizer.read(input),
classAbiHash = LongExternalizer.read(input),
classMemberLevelSnapshot = NullableValueExternalizer(KotlinClassInfoExternalizer).read(input),
supertypes = ListExternalizer(JvmClassNameExternalizer).read(input)
)
}
}
object PackageFacadeKotlinClassSnapshotExternalizer : DataExternalizer<PackageFacadeKotlinClassSnapshot> {
override fun save(output: DataOutput, snapshot: PackageFacadeKotlinClassSnapshot) {
ClassIdExternalizer.save(output, snapshot.classId)
LongExternalizer.save(output, snapshot.classAbiHash)
NullableValueExternalizer(KotlinClassInfoExternalizer).save(output, snapshot.classMemberLevelSnapshot)
PackageMemberSetExternalizer.save(output, snapshot.packageMembers)
}
override fun read(input: DataInput): PackageFacadeKotlinClassSnapshot {
return PackageFacadeKotlinClassSnapshot(
classId = ClassIdExternalizer.read(input),
classAbiHash = LongExternalizer.read(input),
classMemberLevelSnapshot = NullableValueExternalizer(KotlinClassInfoExternalizer).read(input),
packageMembers = PackageMemberSetExternalizer.read(input)
)
}
}
object MultifileClassKotlinClassSnapshotExternalizer : DataExternalizer<MultifileClassKotlinClassSnapshot> {
override fun save(output: DataOutput, snapshot: MultifileClassKotlinClassSnapshot) {
ClassIdExternalizer.save(output, snapshot.classId)
LongExternalizer.save(output, snapshot.classAbiHash)
NullableValueExternalizer(KotlinClassInfoExternalizer).save(output, snapshot.classMemberLevelSnapshot)
PackageMemberSetExternalizer.save(output, snapshot.constants)
}
override fun read(input: DataInput): MultifileClassKotlinClassSnapshot {
return MultifileClassKotlinClassSnapshot(
classId = ClassIdExternalizer.read(input),
classAbiHash = LongExternalizer.read(input),
classMemberLevelSnapshot = NullableValueExternalizer(KotlinClassInfoExternalizer).read(input),
constants = PackageMemberSetExternalizer.read(input)
)
}
}
@@ -126,7 +169,7 @@ object KotlinClassInfoExternalizer : DataExternalizer<KotlinClassInfo> {
override fun save(output: DataOutput, info: KotlinClassInfo) {
ClassIdExternalizer.save(output, info.classId)
output.writeInt(info.classKind.id)
IntExternalizer.save(output, info.classKind.id)
ListExternalizer(StringExternalizer).save(output, info.classHeaderData.toList())
ListExternalizer(StringExternalizer).save(output, info.classHeaderStrings.toList())
NullableValueExternalizer(StringExternalizer).save(output, info.multifileClassName)
@@ -137,7 +180,7 @@ object KotlinClassInfoExternalizer : DataExternalizer<KotlinClassInfo> {
override fun read(input: DataInput): KotlinClassInfo {
return KotlinClassInfo(
classId = ClassIdExternalizer.read(input),
classKind = KotlinClassHeader.Kind.getById(input.readInt()),
classKind = KotlinClassHeader.Kind.getById(IntExternalizer.read(input)),
classHeaderData = ListExternalizer(StringExternalizer).read(input).toTypedArray(),
classHeaderStrings = ListExternalizer(StringExternalizer).read(input).toTypedArray(),
multifileClassName = NullableValueExternalizer(StringExternalizer).read(input),
@@ -147,17 +190,15 @@ object KotlinClassInfoExternalizer : DataExternalizer<KotlinClassInfo> {
}
}
object PackageMemberExternalizer : DataExternalizer<PackageMember> {
object PackageMemberSetExternalizer : DataExternalizer<PackageMemberSet> {
override fun save(output: DataOutput, packageMember: PackageMember) {
FqNameExternalizer.save(output, packageMember.packageFqName)
StringExternalizer.save(output, packageMember.memberName)
override fun save(output: DataOutput, set: PackageMemberSet) {
MapExternalizer(FqNameExternalizer, SetExternalizer(StringExternalizer)).save(output, set.packageToMembersMap)
}
override fun read(input: DataInput): PackageMember {
return PackageMember(
packageFqName = FqNameExternalizer.read(input),
memberName = StringExternalizer.read(input)
override fun read(input: DataInput): PackageMemberSet {
return PackageMemberSet(
packageToMembersMap = MapExternalizer(FqNameExternalizer, SetExternalizer(StringExternalizer)).read(input)
)
}
}
@@ -166,32 +207,50 @@ object JavaClassSnapshotExternalizer : DataExternalizer<JavaClassSnapshot> {
override fun save(output: DataOutput, snapshot: JavaClassSnapshot) {
ClassIdExternalizer.save(output, snapshot.classId)
LongExternalizer.save(output, snapshot.classAbiHash)
NullableValueExternalizer(JavaClassMemberLevelSnapshotExternalizer).save(output, snapshot.classMemberLevelSnapshot)
ListExternalizer(JvmClassNameExternalizer).save(output, snapshot.supertypes)
AbiSnapshotExternalizer.save(output, snapshot.classAbiExcludingMembers)
ListExternalizer(AbiSnapshotExternalizer).save(output, snapshot.fieldsAbi)
ListExternalizer(AbiSnapshotExternalizer).save(output, snapshot.methodsAbi)
}
override fun read(input: DataInput): JavaClassSnapshot {
return JavaClassSnapshot(
classId = ClassIdExternalizer.read(input),
supertypes = ListExternalizer(JvmClassNameExternalizer).read(input),
classAbiExcludingMembers = AbiSnapshotExternalizer.read(input),
fieldsAbi = ListExternalizer(AbiSnapshotExternalizer).read(input),
methodsAbi = ListExternalizer(AbiSnapshotExternalizer).read(input)
classAbiHash = LongExternalizer.read(input),
classMemberLevelSnapshot = NullableValueExternalizer(JavaClassMemberLevelSnapshotExternalizer).read(input),
supertypes = ListExternalizer(JvmClassNameExternalizer).read(input)
)
}
}
object AbiSnapshotExternalizer : DataExternalizer<AbiSnapshot> {
object JavaClassMemberLevelSnapshotExternalizer : DataExternalizer<JavaClassMemberLevelSnapshot> {
override fun save(output: DataOutput, value: AbiSnapshot) {
output.writeString(value.name)
override fun save(output: DataOutput, snapshot: JavaClassMemberLevelSnapshot) {
JavaElementSnapshotExternalizer.save(output, snapshot.classAbiExcludingMembers)
ListExternalizer(JavaElementSnapshotExternalizer).save(output, snapshot.fieldsAbi)
ListExternalizer(JavaElementSnapshotExternalizer).save(output, snapshot.methodsAbi)
}
override fun read(input: DataInput): JavaClassMemberLevelSnapshot {
return JavaClassMemberLevelSnapshot(
classAbiExcludingMembers = JavaElementSnapshotExternalizer.read(input),
fieldsAbi = ListExternalizer(JavaElementSnapshotExternalizer).read(input),
methodsAbi = ListExternalizer(JavaElementSnapshotExternalizer).read(input)
)
}
}
object JavaElementSnapshotExternalizer : DataExternalizer<JavaElementSnapshot> {
override fun save(output: DataOutput, value: JavaElementSnapshot) {
StringExternalizer.save(output, value.name)
LongExternalizer.save(output, value.abiHash)
}
override fun read(input: DataInput): AbiSnapshot {
return AbiSnapshot(name = input.readString(), abiHash = LongExternalizer.read(input))
override fun read(input: DataInput): JavaElementSnapshot {
return JavaElementSnapshot(
name = StringExternalizer.read(input),
abiHash = LongExternalizer.read(input)
)
}
}
@@ -5,7 +5,10 @@
package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.build.report.metrics.*
import org.jetbrains.kotlin.build.report.metrics.BuildMetricsReporter
import org.jetbrains.kotlin.build.report.metrics.BuildPerformanceMetric
import org.jetbrains.kotlin.build.report.metrics.BuildTime
import org.jetbrains.kotlin.build.report.metrics.measure
import org.jetbrains.kotlin.incremental.ClasspathChanges
import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotEnabled.IncrementalRun.NoChanges
import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotEnabled.IncrementalRun.ToBeComputedByIncrementalCompiler
@@ -13,12 +16,12 @@ import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotEnable
import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotEnabled.NotAvailableForNonIncrementalRun
import org.jetbrains.kotlin.incremental.LookupStorage
import org.jetbrains.kotlin.incremental.LookupSymbol
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinker.shrink
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinker.shrinkClasses
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinker.shrinkClasspath
import org.jetbrains.kotlin.incremental.storage.ListExternalizer
import org.jetbrains.kotlin.incremental.storage.LookupSymbolKey
import org.jetbrains.kotlin.incremental.storage.loadFromFile
import org.jetbrains.kotlin.incremental.storage.saveToFile
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
@@ -27,29 +30,29 @@ object ClasspathSnapshotShrinker {
/**
* Shrinks the given classes by retaining only classes that are referenced by the lookup symbols stored in the given [LookupStorage].
*/
fun shrink(
allClasses: List<ClassSnapshotWithHash>,
fun shrinkClasspath(
allClasses: List<AccessibleClassSnapshot>,
lookupStorage: LookupStorage,
metrics: BuildMetricsReporter = DoNothingBuildMetricsReporter
): List<ClassSnapshotWithHash> {
val lookupSymbols = metrics.measure(BuildTime.GET_LOOKUP_SYMBOLS) {
metrics: MetricsReporter = MetricsReporter()
): List<AccessibleClassSnapshot> {
val lookupSymbols = metrics.getLookupSymbols {
lookupStorage.lookupSymbols
.map { LookupSymbol(it.name, it.scope) }
.filterLookupSymbols(allClasses.map { it.classSnapshot })
.map { LookupSymbol(name = it.name, scope = it.scope) }
.filterLookupSymbols(allClasses)
}
return shrink(allClasses, lookupSymbols, metrics)
return shrinkClasses(allClasses, lookupSymbols, metrics)
}
/** Shrinks the given classes by retaining only classes that are referenced by the given lookup symbols. */
fun shrink(
allClasses: List<ClassSnapshotWithHash>,
fun shrinkClasses(
allClasses: List<AccessibleClassSnapshot>,
lookupSymbols: List<ProgramSymbol>,
metrics: BuildMetricsReporter = DoNothingBuildMetricsReporter
): List<ClassSnapshotWithHash> {
val referencedClasses = metrics.measure(BuildTime.FIND_REFERENCED_CLASSES) {
metrics: MetricsReporter = MetricsReporter()
): List<AccessibleClassSnapshot> {
val referencedClasses = metrics.findReferencedClasses {
findReferencedClasses(allClasses, lookupSymbols)
}
return metrics.measure(BuildTime.FIND_TRANSITIVELY_REFERENCED_CLASSES) {
return metrics.findTransitivelyReferencedClasses {
findTransitivelyReferencedClasses(allClasses, referencedClasses)
}
}
@@ -57,12 +60,12 @@ object ClasspathSnapshotShrinker {
/**
* Finds classes that are referenced by the given lookup symbols.
*
* Note: It's okay to over-approximate referenced classes.
* Note: It's okay to over-approximate the result.
*/
private fun findReferencedClasses(
allClasses: List<ClassSnapshotWithHash>,
allClasses: List<AccessibleClassSnapshot>,
lookupSymbols: List<ProgramSymbol>
): List<ClassSnapshotWithHash> {
): List<AccessibleClassSnapshot> {
val lookedUpClassIds: Set<ClassId> = lookupSymbols.mapNotNullTo(mutableSetOf()) {
when (it) {
is ClassSymbol -> it.classId
@@ -70,18 +73,14 @@ object ClasspathSnapshotShrinker {
is PackageMember -> null
}
}
val lookedUpPackageMembers: Set<PackageMember> = lookupSymbols.filterIsInstanceTo(mutableSetOf())
val lookedUpPackageMembers: PackageMemberSet =
lookupSymbols.filterIsInstanceTo<PackageMember, MutableSet<PackageMember>>(mutableSetOf()).compact()
return allClasses.filter {
val isPackageFacade =
it.classSnapshot is KotlinClassSnapshot && it.classSnapshot.classInfo.classKind != KotlinClassHeader.Kind.CLASS
if (isPackageFacade) {
// If packageMembers == null (e.g., if classKind == KotlinClassHeader.Kind.MULTIFILE_CLASS -- see
// `KotlinClassSnapshot.packageMembers`'s kdoc), it means that we don't have the information, so we will always include the
// class (it's okay to over-approximate the result).
(it.classSnapshot as KotlinClassSnapshot).packageMembers?.any { member -> member in lookedUpPackageMembers } ?: true
} else {
it.classSnapshot.getClassId() in lookedUpClassIds
when (it) {
is RegularKotlinClassSnapshot, is JavaClassSnapshot -> it.classId in lookedUpClassIds
is PackageFacadeKotlinClassSnapshot -> it.packageMembers.containsElementsIn(lookedUpPackageMembers)
is MultifileClassKotlinClassSnapshot -> it.constants.containsElementsIn(lookedUpPackageMembers)
}
}
}
@@ -93,10 +92,10 @@ object ClasspathSnapshotShrinker {
* The returned list includes the given referenced classes plus the transitively referenced ones.
*/
private fun findTransitivelyReferencedClasses(
allClasses: List<ClassSnapshotWithHash>,
referencedClasses: List<ClassSnapshotWithHash>
): List<ClassSnapshotWithHash> {
val classIdToClassSnapshot = allClasses.associateBy { it.classSnapshot.getClassId() }
allClasses: List<AccessibleClassSnapshot>,
referencedClasses: List<AccessibleClassSnapshot>
): List<AccessibleClassSnapshot> {
val classIdToClassSnapshot = allClasses.associateBy { it.classId }
val classIds: Set<ClassId> = classIdToClassSnapshot.keys // Use Set for presence check
val classNameToClassId = classIds.associateBy { JvmClassName.byClassId(it) }
val classNameToClassIdResolver = { className: JvmClassName -> classNameToClassId[className] }
@@ -105,15 +104,30 @@ object ClasspathSnapshotShrinker {
// No need to collect supertypes outside the given set of classes (e.g., "java/lang/Object")
@Suppress("SimpleRedundantLet")
classIdToClassSnapshot[classId]?.let {
it.classSnapshot.getSupertypes(classNameToClassIdResolver).filter { supertype -> supertype in classIds }.toSet()
it.getSupertypes(classNameToClassIdResolver).filterTo(mutableSetOf()) { supertype -> supertype in classIds }
} ?: emptySet()
}
val referencedClassIds = referencedClasses.map { it.classSnapshot.getClassId() }.toSet()
val referencedClassIds = referencedClasses.mapTo(mutableSetOf()) { it.classId }
val transitivelyReferencedClassIds: Set<ClassId> =
ImpactAnalysis.findImpactedClassesInclusive(referencedClassIds, supertypesResolver) // Use Set for presence check
return allClasses.filter { it.classSnapshot.getClassId() in transitivelyReferencedClassIds }
return allClasses.filter { it.classId in transitivelyReferencedClassIds }
}
/**
* Helper class to allow the caller of [ClasspathSnapshotShrinker] to provide a list of [BuildTime]s as different callers may want to
* record different [BuildTime]s (because the [BuildTime.parent]s are different).
*/
class MetricsReporter(
private val metrics: BuildMetricsReporter? = null,
private val getLookupSymbols: BuildTime? = null,
private val findReferencedClasses: BuildTime? = null,
private val findTransitivelyReferencedClasses: BuildTime? = null
) {
fun <T> getLookupSymbols(fn: () -> T) = metrics?.measure(getLookupSymbols!!, fn) ?: fn()
fun <T> findReferencedClasses(fn: () -> T) = metrics?.measure(findReferencedClasses!!, fn) ?: fn()
fun <T> findTransitivelyReferencedClasses(fn: () -> T) = metrics?.measure(findTransitivelyReferencedClasses!!, fn) ?: fn()
}
}
@@ -136,8 +150,8 @@ object ClasspathSnapshotShrinker {
* snapshotting), even though it seems more efficient to do so. For correctness, we need to look at the entire classpath first, remove
* duplicate classes, and then remove inaccessible classes.
*/
internal fun ClasspathSnapshot.removeDuplicateAndInaccessibleClasses(): List<ClassSnapshotWithHash> {
return getNonDuplicateClassSnapshots().filter { it.classSnapshot !is InaccessibleClassSnapshot }
internal fun ClasspathSnapshot.removeDuplicateAndInaccessibleClasses(): List<AccessibleClassSnapshot> {
return getNonDuplicateClassSnapshots().filterIsInstance<AccessibleClassSnapshot>()
}
/**
@@ -145,8 +159,8 @@ internal fun ClasspathSnapshot.removeDuplicateAndInaccessibleClasses(): List<Cla
*
* If there are duplicate classes on the classpath, retain only the first one to match the compiler's behavior.
*/
private fun ClasspathSnapshot.getNonDuplicateClassSnapshots(): List<ClassSnapshotWithHash> {
val classSnapshots = LinkedHashMap<String, ClassSnapshotWithHash>(classpathEntrySnapshots.sumOf { it.classSnapshots.size })
private fun ClasspathSnapshot.getNonDuplicateClassSnapshots(): List<ClassSnapshot> {
val classSnapshots = LinkedHashMap<String, ClassSnapshot>(classpathEntrySnapshots.sumOf { it.classSnapshots.size })
for (classpathEntrySnapshot in classpathEntrySnapshots) {
for ((unixStyleRelativePath, classSnapshot) in classpathEntrySnapshot.classSnapshots) {
classSnapshots.putIfAbsent(unixStyleRelativePath, classSnapshot)
@@ -160,12 +174,12 @@ private sealed class ShrinkMode {
object NoChanges : ShrinkMode()
class IncrementalNoNewLookups(
val shrunkCurrentClasspathAgainstPreviousLookups: List<ClassSnapshotWithHash>,
val shrunkCurrentClasspathAgainstPreviousLookups: List<AccessibleClassSnapshot>,
) : ShrinkMode()
class Incremental(
val currentClasspathSnapshot: List<ClassSnapshotWithHash>,
val shrunkCurrentClasspathAgainstPreviousLookups: List<ClassSnapshotWithHash>,
val currentClasspathSnapshot: List<AccessibleClassSnapshot>,
val shrunkCurrentClasspathAgainstPreviousLookups: List<AccessibleClassSnapshot>,
val addedLookupSymbols: Set<LookupSymbolKey>
) : ShrinkMode()
@@ -175,8 +189,8 @@ private sealed class ShrinkMode {
internal fun shrinkAndSaveClasspathSnapshot(
classpathChanges: ClasspathChanges.ClasspathSnapshotEnabled,
lookupStorage: LookupStorage,
currentClasspathSnapshot: List<ClassSnapshotWithHash>?, // Not null iff classpathChanges is ToBeComputedByIncrementalCompiler
shrunkCurrentClasspathAgainstPreviousLookups: List<ClassSnapshotWithHash>?, // Same as above
currentClasspathSnapshot: List<AccessibleClassSnapshot>?, // Not null iff classpathChanges is ToBeComputedByIncrementalCompiler
shrunkCurrentClasspathAgainstPreviousLookups: List<AccessibleClassSnapshot>?, // Same as above
metrics: BuildMetricsReporter
) {
// In the following, we'll try to shrink the classpath snapshot incrementally when possible.
@@ -191,7 +205,7 @@ internal fun shrinkAndSaveClasspathSnapshot(
} else {
val shrunkPreviousClasspathAgainstPreviousLookups =
metrics.measure(BuildTime.LOAD_SHRUNK_PREVIOUS_CLASSPATH_SNAPSHOT_AFTER_COMPILATION) {
ListExternalizer(ClassSnapshotWithHashExternalizer)
ListExternalizer(AccessibleClassSnapshotExternalizer)
.loadFromFile(classpathChanges.classpathSnapshotFiles.shrunkPreviousClasspathSnapshotFile)
}
ShrinkMode.Incremental(
@@ -219,7 +233,7 @@ internal fun shrinkAndSaveClasspathSnapshot(
}
// Shrink current classpath against current lookups
val shrunkCurrentClasspath: List<ClassSnapshotWithHash>? = when (shrinkMode) {
val shrunkCurrentClasspath: List<AccessibleClassSnapshot>? = when (shrinkMode) {
is ShrinkMode.NoChanges -> null
is ShrinkMode.IncrementalNoNewLookups -> {
// There are no new lookups, so
@@ -227,13 +241,12 @@ internal fun shrinkAndSaveClasspathSnapshot(
shrinkMode.shrunkCurrentClasspathAgainstPreviousLookups
}
is ShrinkMode.Incremental -> metrics.measure(BuildTime.SHRINK_CURRENT_CLASSPATH_SNAPSHOT_AFTER_COMPILATION) {
val shrunkClasses = shrinkMode.shrunkCurrentClasspathAgainstPreviousLookups.map { it.classSnapshot.getClassId() }.toSet()
val notYetShrunkClasses = shrinkMode.currentClasspathSnapshot.filter { it.classSnapshot.getClassId() !in shrunkClasses }
val shrunkClasses = shrinkMode.shrunkCurrentClasspathAgainstPreviousLookups.mapTo(mutableSetOf()) { it.classId }
val notYetShrunkClasses = shrinkMode.currentClasspathSnapshot.filter { it.classId !in shrunkClasses }
val addedLookupSymbols = shrinkMode.addedLookupSymbols
.map { LookupSymbol(it.name, it.scope) }
.filterLookupSymbols(shrinkMode.currentClasspathSnapshot.map { it.classSnapshot })
// Don't provide a BuildMetricsReporter for the following call as the sub-BuildTimes in it have a different parent
val shrunkRemainingClassesAgainstNewLookups = shrink(notYetShrunkClasses, addedLookupSymbols)
.map { LookupSymbol(name = it.name, scope = it.scope) }
.filterLookupSymbols(notYetShrunkClasses)
val shrunkRemainingClassesAgainstNewLookups = shrinkClasses(notYetShrunkClasses, addedLookupSymbols)
shrinkMode.shrunkCurrentClasspathAgainstPreviousLookups + shrunkRemainingClassesAgainstNewLookups
}
@@ -244,8 +257,7 @@ internal fun shrinkAndSaveClasspathSnapshot(
.removeDuplicateAndInaccessibleClasses()
}
metrics.measure(BuildTime.SHRINK_CURRENT_CLASSPATH_SNAPSHOT_AFTER_COMPILATION) {
// Don't provide a BuildMetricsReporter for the following call as the sub-BuildTimes in it have a different parent
shrink(classpathSnapshot, lookupStorage)
shrinkClasspath(classpathSnapshot, lookupStorage)
}
}
}
@@ -257,7 +269,7 @@ internal fun shrinkAndSaveClasspathSnapshot(
}
} else {
metrics.measure(BuildTime.SAVE_SHRUNK_CURRENT_CLASSPATH_SNAPSHOT_AFTER_COMPILATION) {
ListExternalizer(ClassSnapshotWithHashExternalizer).saveToFile(
ListExternalizer(AccessibleClassSnapshotExternalizer).saveToFile(
classpathChanges.classpathSnapshotFiles.shrunkPreviousClasspathSnapshotFile,
shrunkCurrentClasspath!!
)
@@ -7,6 +7,8 @@ package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.incremental.*
import org.jetbrains.kotlin.incremental.ChangesCollector.Companion.getNonPrivateMemberNames
import org.jetbrains.kotlin.incremental.classpathDiff.ClassSnapshotGranularity.*
import org.jetbrains.kotlin.incremental.storage.toByteArray
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader.Kind.*
import org.jetbrains.kotlin.name.ClassId
import java.io.File
@@ -29,7 +31,7 @@ object ClasspathEntrySnapshotter {
ClassFileWithContents(ClassFile(classpathEntry, unixStyleRelativePath), contents)
}
val snapshots = ClassSnapshotter.snapshot(classes).map { it.withHash }
val snapshots = ClassSnapshotter.snapshot(classes)
val relativePathsToSnapshotsMap = classes.map { it.classFile.unixStyleRelativePath }.zip(snapshots).toMap(LinkedHashMap())
return ClasspathEntrySnapshot(relativePathsToSnapshotsMap)
@@ -40,7 +42,11 @@ object ClasspathEntrySnapshotter {
object ClassSnapshotter {
/** Creates [ClassSnapshot]s of the given classes. */
fun snapshot(classes: List<ClassFileWithContents>, includeDebugInfoInJavaSnapshot: Boolean? = null): List<ClassSnapshot> {
fun snapshot(
classes: List<ClassFileWithContents>,
granularity: ClassSnapshotGranularity = CLASS_MEMBER_LEVEL,
includeDebugInfoInJavaSnapshot: Boolean = false
): List<ClassSnapshot> {
// Find inaccessible classes first
val classesInfo: List<BasicClassInfo> = classes.map { it.classInfo }
val inaccessibleClassesInfo: Set<BasicClassInfo> = getInaccessibleClasses(classesInfo).toSet()
@@ -48,23 +54,35 @@ object ClassSnapshotter {
return classes.map {
when {
it.classInfo in inaccessibleClassesInfo -> InaccessibleClassSnapshot
it.classInfo.isKotlinClass -> snapshotKotlinClass(it)
else -> JavaClassSnapshotter.snapshot(it, includeDebugInfoInJavaSnapshot)
it.classInfo.isKotlinClass -> snapshotKotlinClass(it, granularity)
else -> JavaClassSnapshotter.snapshot(it, granularity, includeDebugInfoInJavaSnapshot)
}
}
}
/** Creates [KotlinClassSnapshot] of the given Kotlin class (the caller must ensure that the given class is a Kotlin class). */
private fun snapshotKotlinClass(classFile: ClassFileWithContents): KotlinClassSnapshot {
private fun snapshotKotlinClass(classFile: ClassFileWithContents, granularity: ClassSnapshotGranularity): KotlinClassSnapshot {
val kotlinClassInfo =
KotlinClassInfo.createFrom(classFile.classInfo.classId, classFile.classInfo.kotlinClassHeader!!, classFile.contents)
val packageMembers = when (kotlinClassInfo.classKind) {
CLASS, MULTIFILE_CLASS -> null // See `KotlinClassSnapshot.packageMembers`'s kdoc
else -> (kotlinClassInfo.protoData as PackagePartProtoData).getNonPrivateMemberNames().map {
PackageMember(kotlinClassInfo.classId.packageFqName, it)
}
val classId = kotlinClassInfo.classId
val classAbiHash = KotlinClassInfoExternalizer.toByteArray(kotlinClassInfo).md5()
val classMemberLevelSnapshot = kotlinClassInfo.takeIf { granularity == CLASS_MEMBER_LEVEL }
return when (kotlinClassInfo.classKind) {
CLASS -> RegularKotlinClassSnapshot(classId, classAbiHash, classMemberLevelSnapshot, classFile.classInfo.supertypes)
FILE_FACADE, MULTIFILE_CLASS_PART -> PackageFacadeKotlinClassSnapshot(
classId, classAbiHash, classMemberLevelSnapshot,
packageMembers = PackageMemberSet(
mapOf(classId.packageFqName to (kotlinClassInfo.protoData as PackagePartProtoData).getNonPrivateMemberNames())
)
)
MULTIFILE_CLASS -> MultifileClassKotlinClassSnapshot(
classId, classAbiHash, classMemberLevelSnapshot,
constants = PackageMemberSet(mapOf(classId.packageFqName to kotlinClassInfo.constantsMap.keys))
)
SYNTHETIC_CLASS -> error("Unexpected class $classId with class kind ${SYNTHETIC_CLASS.name} (synthetic classes should have been removed earlier)")
UNKNOWN -> error("Can't handle class $classId with class kind ${UNKNOWN.name}")
}
return KotlinClassSnapshot(kotlinClassInfo, classFile.classInfo.supertypes, packageMembers)
}
/**
@@ -54,24 +54,42 @@ object JavaClassChangesComputer {
previousClassSnapshot: JavaClassSnapshot,
changes: ChangeSet.Collector
) {
if (currentClassSnapshot.classAbiHash == previousClassSnapshot.classAbiHash) return
val classId = currentClassSnapshot.classId.also { check(it == previousClassSnapshot.classId) }
if (currentClassSnapshot.classAbiExcludingMembers.abiHash != previousClassSnapshot.classAbiExcludingMembers.abiHash) {
changes.addChangedClass(classId)
if (currentClassSnapshot.classMemberLevelSnapshot != null && previousClassSnapshot.classMemberLevelSnapshot != null) {
if (currentClassSnapshot.classMemberLevelSnapshot.classAbiExcludingMembers.abiHash
!= previousClassSnapshot.classMemberLevelSnapshot.classAbiExcludingMembers.abiHash
) {
changes.addChangedClass(classId)
} else {
collectClassMemberChanges(
classId,
currentClassSnapshot.classMemberLevelSnapshot.fieldsAbi,
previousClassSnapshot.classMemberLevelSnapshot.fieldsAbi,
changes
)
collectClassMemberChanges(
classId,
currentClassSnapshot.classMemberLevelSnapshot.methodsAbi,
previousClassSnapshot.classMemberLevelSnapshot.methodsAbi,
changes
)
}
} else {
collectClassMemberChanges(classId, currentClassSnapshot.fieldsAbi, previousClassSnapshot.fieldsAbi, changes)
collectClassMemberChanges(classId, currentClassSnapshot.methodsAbi, previousClassSnapshot.methodsAbi, changes)
changes.addChangedClass(classId)
}
}
/** Collects changes between two lists of fields/methods within a class. */
private fun collectClassMemberChanges(
classId: ClassId,
currentMemberSnapshots: List<AbiSnapshot>,
previousMemberSnapshots: List<AbiSnapshot>,
currentMemberSnapshots: List<JavaElementSnapshot>,
previousMemberSnapshots: List<JavaElementSnapshot>,
changes: ChangeSet.Collector
) {
val currentMemberHashes: Map<Long, AbiSnapshot> = currentMemberSnapshots.associateBy { it.abiHash }
val previousMemberHashes: Map<Long, AbiSnapshot> = previousMemberSnapshots.associateBy { it.abiHash }
val currentMemberHashes: Map<Long, JavaElementSnapshot> = currentMemberSnapshots.associateBy { it.abiHash }
val previousMemberHashes: Map<Long, JavaElementSnapshot> = previousMemberSnapshots.associateBy { it.abiHash }
val addedMembers = currentMemberHashes.keys - previousMemberHashes.keys
val removedMembers = previousMemberHashes.keys - currentMemberHashes.keys
@@ -6,7 +6,9 @@
package org.jetbrains.kotlin.incremental.classpathDiff
import com.google.gson.GsonBuilder
import org.jetbrains.kotlin.incremental.classpathDiff.ClassSnapshotGranularity.CLASS_MEMBER_LEVEL
import org.jetbrains.kotlin.incremental.md5
import org.jetbrains.kotlin.incremental.storage.toByteArray
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.tree.ClassNode
@@ -14,7 +16,11 @@ import org.jetbrains.org.objectweb.asm.tree.ClassNode
/** Computes a [JavaClassSnapshot] of a Java class. */
object JavaClassSnapshotter {
fun snapshot(classFile: ClassFileWithContents, includeDebugInfoInSnapshot: Boolean? = null): JavaClassSnapshot {
fun snapshot(
classFile: ClassFileWithContents,
granularity: ClassSnapshotGranularity,
includeDebugInfoInSnapshot: Boolean
): JavaClassSnapshot {
// We will extract ABI information from the given class and store it into the `abiClass` variable.
// It is acceptable to collect more info than required, but it is incorrect to collect less info than required.
// There are 2 approaches:
@@ -52,9 +58,12 @@ object JavaClassSnapshotter {
abiClass.methods.clear()
val classAbiExcludingMembers = abiClass.let { snapshotJavaElement(it, it.name, includeDebugInfoInSnapshot) }
val detailedSnapshot = JavaClassMemberLevelSnapshot(classAbiExcludingMembers, fieldsAbi, methodsAbi)
return JavaClassSnapshot(
classFile.classInfo.classId, classFile.classInfo.supertypes,
classAbiExcludingMembers, fieldsAbi, methodsAbi
classId = classFile.classInfo.classId,
classAbiHash = JavaClassMemberLevelSnapshotExternalizer.toByteArray(detailedSnapshot).md5(),
classMemberLevelSnapshot = detailedSnapshot.takeIf { granularity == CLASS_MEMBER_LEVEL },
supertypes = classFile.classInfo.supertypes
)
}
@@ -73,15 +82,19 @@ object JavaClassSnapshotter {
.create()
}
private fun snapshotJavaElement(javaElement: Any, javaElementName: String, includeDebugInfoInSnapshot: Boolean? = null): AbiSnapshot {
return if (includeDebugInfoInSnapshot == true) {
private fun snapshotJavaElement(
javaElement: Any,
javaElementName: String,
includeDebugInfoInSnapshot: Boolean
): JavaElementSnapshot {
return if (includeDebugInfoInSnapshot) {
val abiValue = gsonForDebug.toJson(javaElement)
val abiHash = abiValue.toByteArray().md5()
AbiSnapshotForTests(javaElementName, abiHash, abiValue)
JavaElementSnapshotForTests(javaElementName, abiHash, abiValue)
} else {
val abiValue = gson.toJson(javaElement)
val abiHash = abiValue.toByteArray().md5()
AbiSnapshot(javaElementName, abiHash)
JavaElementSnapshot(javaElementName, abiHash)
}
}
}
@@ -6,7 +6,6 @@
package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.incremental.LookupSymbol
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
@@ -21,6 +20,39 @@ data class ClassMember(val classId: ClassId, val memberName: String) : ProgramSy
data class PackageMember(val packageFqName: FqName, val memberName: String) : ProgramSymbol()
/** Compact representation for a set of [PackageMember]s. */
class PackageMemberSet(val packageToMembersMap: Map<FqName, Set<String>>) {
operator fun contains(other: PackageMember): Boolean {
return packageToMembersMap[other.packageFqName]?.let { other.memberName in it } ?: false
}
fun containsElementsIn(other: PackageMemberSet): Boolean {
return this.packageToMembersMap.keys.intersect(other.packageToMembersMap.keys).any { commonPackage ->
val otherMembers = other.packageToMembersMap[commonPackage]!!
this.packageToMembersMap[commonPackage]!!.any { it in otherMembers }
}
}
}
fun Set<PackageMember>.compact(): PackageMemberSet {
val map = mutableMapOf<FqName, MutableSet<String>>()
forEach {
map.getOrPut(it.packageFqName) { mutableSetOf() }.add(it.memberName)
}
return PackageMemberSet(map)
}
fun List<PackageMemberSet>.combine(): PackageMemberSet {
val combinedMap = mutableMapOf<FqName, MutableSet<String>>()
forEach {
it.packageToMembersMap.forEach { (packageFqName, memberNames) ->
combinedMap.getOrPut(packageFqName) { mutableSetOf() }.addAll(memberNames)
}
}
return PackageMemberSet(combinedMap)
}
/**
* Finds [LookupSymbol]s that potentially refer to classes on the given classpath, and returns the [ProgramSymbol]s corresponding to those
* [LookupSymbol]s.
@@ -30,24 +62,23 @@ data class PackageMember(val packageFqName: FqName, val memberName: String) : Pr
*
* The given classpath must not contain duplicate classes.
*
* It's okay if the returned result is an over-approximation.
* It's okay to over-approximate the result.
*/
internal fun Collection<LookupSymbol>.filterLookupSymbols(classpath: List<ClassSnapshot>): List<ProgramSymbol> {
val (packageFacades, regularClasses) = classpath.partition {
it is KotlinClassSnapshot && it.classInfo.classKind != KotlinClassHeader.Kind.CLASS
internal fun Collection<LookupSymbol>.filterLookupSymbols(classpath: List<AccessibleClassSnapshot>): List<ProgramSymbol> {
val regularClassesOnClasspath: List<ClassId> = classpath.mapNotNull {
when (it) {
is RegularKotlinClassSnapshot -> it.classId
is JavaClassSnapshot -> it.classId
is PackageFacadeKotlinClassSnapshot, is MultifileClassKotlinClassSnapshot -> null
}
}
val regularClassesOnClasspath: List<ClassId> = regularClasses.map { it.getClassId() }
val packageMembersOnClasspath: Set<PackageMember> =
packageFacades.flatMap {
if ((it as KotlinClassSnapshot).classInfo.classKind == KotlinClassHeader.Kind.MULTIFILE_CLASS) {
// If classKind == MULTIFILE_CLASS, we don't have the information about its package members (see
// `KotlinClassSnapshot.packageMembers`'s kdoc). However, package members in a MULTIFILE_CLASS should be found in
// MULTIFILE_CLASS_PART classes, so it's okay to ignore MULTIFILE_CLASS here.
emptyList()
} else {
it.packageMembers!!
}
}.toSet() // Use Set for presence check
val packageMembersOnClasspath: PackageMemberSet = classpath.mapNotNull {
when (it) {
is RegularKotlinClassSnapshot, is JavaClassSnapshot -> null
is PackageFacadeKotlinClassSnapshot -> it.packageMembers
is MultifileClassKotlinClassSnapshot -> it.constants
}
}.combine()
// It's rare but possible for 2 ClassIds to have the same FqName (e.g., ClassId `com/example/Foo` and ClassId `com/example.Foo` both
// have FqName `com.example.Foo`. ClassId `com/example/Foo` indicates class `Foo` in package `com/example', whereas ClassId
@@ -8,20 +8,22 @@ package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.build.report.metrics.DoNothingBuildMetricsReporter
import org.jetbrains.kotlin.incremental.ChangesEither
import org.jetbrains.kotlin.incremental.LookupSymbol
import org.jetbrains.kotlin.incremental.classpathDiff.ClassSnapshotGranularity.CLASS_LEVEL
import org.jetbrains.kotlin.incremental.classpathDiff.ClassSnapshotGranularity.CLASS_MEMBER_LEVEL
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.ClassFileUtil.asFile
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.ClassFileUtil.snapshot
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.CompileUtil.compileAll
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.kotlin.resolve.sam.SAM_LOOKUP_NAME
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File
import kotlin.test.fail
abstract class ClasspathChangesComputerTest : ClasspathSnapshotTestCommon() {
private val testDataDir =
File("compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest")
companion object {
val testDataDir =
File("compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest")
}
abstract class ClasspathChangesComputerTest : ClasspathSnapshotTestCommon() {
@Test
abstract fun testAbiVersusNonAbiChanges()
@@ -29,6 +31,12 @@ abstract class ClasspathChangesComputerTest : ClasspathSnapshotTestCommon() {
@Test
abstract fun testModifiedAddedRemovedElements()
@Test
abstract fun testModifiedAddedRemovedElements_ClassLevelSnapshot()
@Test
abstract fun testMixedClassSnapshotGranularities()
@Test
abstract fun testImpactAnalysis()
}
@@ -80,6 +88,49 @@ class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
).assertEquals(changes)
}
@Test
override fun testModifiedAddedRemovedElements_ClassLevelSnapshot() {
val changes = computeClasspathChanges(File(testDataDir, "testModifiedAddedRemovedElements/src/kotlin"), tmpDir, CLASS_LEVEL)
Changes(
lookupSymbols = setOf(
LookupSymbol(name = "ModifiedClassUnchangedMembers", scope = "com.example"),
LookupSymbol(name = "ModifiedClassChangedMembers", scope = "com.example"),
LookupSymbol(name = "AddedClass", scope = "com.example"),
LookupSymbol(name = "RemovedClass", scope = "com.example"),
),
fqNames = setOf(
"com.example.ModifiedClassUnchangedMembers",
"com.example.ModifiedClassChangedMembers",
"com.example.AddedClass",
"com.example.RemovedClass",
)
).assertEquals(changes)
}
@Test
override fun testMixedClassSnapshotGranularities() {
val currentClasspathSnapshot = testMixedClassSnapshotGranularities_snapshotClasspath("kotlin", "current-classpath", tmpDir)
val previousClasspathSnapshot = testMixedClassSnapshotGranularities_snapshotClasspath("kotlin", "previous-classpath", tmpDir)
val changes = computeClasspathChanges(currentClasspathSnapshot, previousClasspathSnapshot)
Changes(
lookupSymbols = setOf(
LookupSymbol(name = "CoarseGrainedFirstBuild_CoarseGrainedSecondBuild_Class", scope = "com.example"),
LookupSymbol(name = "CoarseGrainedFirstBuild_FineGrainedSecondBuild_Class", scope = "com.example"),
LookupSymbol(name = "FineGrainedFirstBuild_CoarseGrainedSecondBuild_Class", scope = "com.example"),
LookupSymbol(name = "modifiedProperty", scope = "com.example.FineGrainedFirstBuild_FineGrainedSecondBuild_Class"),
LookupSymbol(name = "modifiedFunction", scope = "com.example.FineGrainedFirstBuild_FineGrainedSecondBuild_Class"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.FineGrainedFirstBuild_FineGrainedSecondBuild_Class")
),
fqNames = setOf(
"com.example.CoarseGrainedFirstBuild_CoarseGrainedSecondBuild_Class",
"com.example.CoarseGrainedFirstBuild_FineGrainedSecondBuild_Class",
"com.example.FineGrainedFirstBuild_CoarseGrainedSecondBuild_Class",
"com.example.FineGrainedFirstBuild_FineGrainedSecondBuild_Class"
)
).assertEquals(changes)
}
@Test
override fun testImpactAnalysis() {
val changes = computeClasspathChanges(File(testDataDir, "testImpactAnalysis_KotlinOnly/src"), tmpDir)
@@ -230,6 +281,49 @@ class JavaOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
).assertEquals(changes)
}
@Test
override fun testModifiedAddedRemovedElements_ClassLevelSnapshot() {
val changes = computeClasspathChanges(File(testDataDir, "testModifiedAddedRemovedElements/src/java"), tmpDir, CLASS_LEVEL)
Changes(
lookupSymbols = setOf(
LookupSymbol(name = "ModifiedClassUnchangedMembers", scope = "com.example"),
LookupSymbol(name = "ModifiedClassChangedMembers", scope = "com.example"),
LookupSymbol(name = "AddedClass", scope = "com.example"),
LookupSymbol(name = "RemovedClass", scope = "com.example")
),
fqNames = setOf(
"com.example.ModifiedClassUnchangedMembers",
"com.example.ModifiedClassChangedMembers",
"com.example.AddedClass",
"com.example.RemovedClass"
)
).assertEquals(changes)
}
@Test
override fun testMixedClassSnapshotGranularities() {
val currentClasspathSnapshot = testMixedClassSnapshotGranularities_snapshotClasspath("java", "current-classpath", tmpDir)
val previousClasspathSnapshot = testMixedClassSnapshotGranularities_snapshotClasspath("java", "previous-classpath", tmpDir)
val changes = computeClasspathChanges(currentClasspathSnapshot, previousClasspathSnapshot)
Changes(
lookupSymbols = setOf(
LookupSymbol(name = "CoarseGrainedFirstBuild_CoarseGrainedSecondBuild_Class", scope = "com.example"),
LookupSymbol(name = "CoarseGrainedFirstBuild_FineGrainedSecondBuild_Class", scope = "com.example"),
LookupSymbol(name = "FineGrainedFirstBuild_CoarseGrainedSecondBuild_Class", scope = "com.example"),
LookupSymbol(name = "modifiedField", scope = "com.example.FineGrainedFirstBuild_FineGrainedSecondBuild_Class"),
LookupSymbol(name = "modifiedMethod", scope = "com.example.FineGrainedFirstBuild_FineGrainedSecondBuild_Class"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.FineGrainedFirstBuild_FineGrainedSecondBuild_Class")
),
fqNames = setOf(
"com.example.CoarseGrainedFirstBuild_CoarseGrainedSecondBuild_Class",
"com.example.CoarseGrainedFirstBuild_FineGrainedSecondBuild_Class",
"com.example.FineGrainedFirstBuild_CoarseGrainedSecondBuild_Class",
"com.example.FineGrainedFirstBuild_FineGrainedSecondBuild_Class"
)
).assertEquals(changes)
}
@Test
override fun testImpactAnalysis() {
val changes = computeClasspathChanges(File(testDataDir, "testImpactAnalysis_JavaOnly/src"), tmpDir)
@@ -262,7 +356,7 @@ class KotlinAndJavaClasspathChangesComputerTest : ClasspathSnapshotTestCommon()
@Test
fun testImpactAnalysis() {
val changes =
computeClasspathChanges(File(ClasspathChangesComputerTest.testDataDir, "testImpactAnalysis_KotlinAndJava/src"), tmpDir)
computeClasspathChanges(File(testDataDir, "testImpactAnalysis_KotlinAndJava/src"), tmpDir)
Changes(
lookupSymbols = setOf(
LookupSymbol(name = "changedProperty", scope = "com.example.ChangedKotlinSuperClass"),
@@ -296,25 +390,44 @@ class KotlinAndJavaClasspathChangesComputerTest : ClasspathSnapshotTestCommon()
}
}
private fun computeClasspathChanges(classpathSourceDir: File, tmpDir: TemporaryFolder): Changes {
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir)
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir)
return computeClasspathChanges(currentSnapshot, previousSnapshot)
private fun testMixedClassSnapshotGranularities_snapshotClasspath(
language: String, classpathSourceDirName: String, tmpDir: TemporaryFolder
): ClasspathSnapshot {
val classes = compileAll(File("$testDataDir/testMixedClassSnapshotGranularities/src/$language/$classpathSourceDirName/0"), tmpDir)
fun getGranularity(classFile: ClassFile): ClassSnapshotGranularity {
val granularity = when (val className = classFile.asFile().nameWithoutExtension) {
"CoarseGrainedFirstBuild_CoarseGrainedSecondBuild_Class" -> CLASS_LEVEL to CLASS_LEVEL
"CoarseGrainedFirstBuild_FineGrainedSecondBuild_Class" -> CLASS_LEVEL to CLASS_MEMBER_LEVEL
"FineGrainedFirstBuild_CoarseGrainedSecondBuild_Class" -> CLASS_MEMBER_LEVEL to CLASS_LEVEL
"FineGrainedFirstBuild_FineGrainedSecondBuild_Class" -> CLASS_MEMBER_LEVEL to CLASS_MEMBER_LEVEL
else -> error("Unrecognized class: $className")
}
return when (classpathSourceDirName) {
"previous-classpath" -> granularity.first
"current-classpath" -> granularity.second
else -> error("Unrecognized classpathSourceDirName: $classpathSourceDirName")
}
}
return classes.map { it.snapshot(getGranularity(it)) }.toClasspathSnapshot()
}
private fun snapshotClasspath(classpathSourceDir: File, tmpDir: TemporaryFolder): ClasspathSnapshot {
val classpath = mutableListOf<File>()
val classpathEntrySnapshots = classpathSourceDir.listFiles()!!.sortedBy { it.name }.map { classpathEntrySourceDir ->
val classFiles = compileAll(classpathEntrySourceDir, classpath, tmpDir)
classpath.addAll(listOfNotNull(classFiles.firstOrNull()?.classRoot))
private fun List<ClassSnapshot>.toClasspathSnapshot(): ClasspathSnapshot {
val classpathEntrySnapshot = ClasspathEntrySnapshot(associateByTo(LinkedHashMap()) {
JvmClassName.byClassId((it as AccessibleClassSnapshot).classId).internalName + ".class"
})
return ClasspathSnapshot(listOf(classpathEntrySnapshot))
}
val relativePaths = classFiles.map { it.unixStyleRelativePath }
val classSnapshots = classFiles.snapshot().map { it.withHash }
ClasspathEntrySnapshot(
classSnapshots = relativePaths.zip(classSnapshots).toMap(LinkedHashMap())
)
}
return ClasspathSnapshot(classpathEntrySnapshots)
private fun computeClasspathChanges(
classpathSourceDir: File,
tmpDir: TemporaryFolder,
granularity: ClassSnapshotGranularity? = null
): Changes {
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir, granularity)
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir, granularity)
return computeClasspathChanges(currentSnapshot, previousSnapshot)
}
private fun computeClasspathChanges(
@@ -36,8 +36,8 @@ class KotlinClassesClasspathSnapshotSerializerTest : ClasspathSnapshotSerializer
override val sourceFile = TestSourceFile(
KotlinSourceFile(
baseDir = File(testDataDir, "src/kotlin"), relativePath = "com/example/SimpleClass.kt",
preCompiledClassFile = ClassFile(File(testDataDir, "classes/kotlin"), "com/example/SimpleClass.class")
baseDir = File(testDataDir, "kotlin/testSimpleClass/src"), relativePath = "com/example/SimpleClass.kt",
preCompiledClassFile = ClassFile(File(testDataDir, "kotlin/testSimpleClass/classes"), "com/example/SimpleClass.class")
), tmpDir
)
}
@@ -46,7 +46,7 @@ class JavaClassesClasspathSnapshotSerializerTest : ClasspathSnapshotSerializerTe
override val sourceFile = TestSourceFile(
JavaSourceFile(
baseDir = File(testDataDir, "src/java"), relativePath = "com/example/SimpleClass.java",
baseDir = File(testDataDir, "java/testSimpleClass/src"), relativePath = "com/example/SimpleClass.java",
), tmpDir
)
}
@@ -6,14 +6,17 @@
package org.jetbrains.kotlin.incremental.classpathDiff
import com.google.gson.GsonBuilder
import org.jetbrains.kotlin.cli.common.isWindows
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.ClassFileUtil.asFile
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.ClassFileUtil.snapshot
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.CompileUtil.compile
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.CompileUtil.compileAll
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.SourceFile.KotlinSourceFile
import org.jetbrains.kotlin.test.KotlinTestUtils
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import java.io.File
import java.lang.ProcessBuilder.Redirect
abstract class ClasspathSnapshotTestCommon {
@@ -68,12 +71,12 @@ abstract class ClasspathSnapshotTestCommon {
} else {
val srcDir = tmpDir.newFolder()
asFile().copyTo(File(srcDir, unixStyleRelativePath))
compileAll(srcDir, classpath = emptyList(), tmpDir)
compileAll(srcDir, tmpDir)
}
}
/** Compiles the source files in the given directory and returns all generated .class files. */
fun compileAll(srcDir: File, classpath: List<File>, tmpDir: TemporaryFolder): List<ClassFile> {
fun compileAll(srcDir: File, tmpDir: TemporaryFolder, classpath: List<File> = emptyList()): List<ClassFile> {
val kotlinClasses = compileKotlin(srcDir, classpath, tmpDir)
val javaClasspath = classpath + listOfNotNull(kotlinClasses.firstOrNull()?.classRoot)
@@ -126,11 +129,20 @@ abstract class ClasspathSnapshotTestCommon {
}
val classesDir = tmpDir.newFolder()
org.jetbrains.kotlin.test.MockLibraryUtil.compileKotlin(
// Note: Calling the following is simpler:
// org.jetbrains.kotlin.test.MockLibraryUtil.compileKotlin(
// srcDir.path, classesDir, extraClasspath = classpath.map { it.path }.toTypedArray())
// However, it currently fails with UnsupportedClassVersionError, so we have to launch a new kotlinc process instead.
val kotlincBinary = if (isWindows) "dist/kotlinc/bin/kotlinc.bat" else "dist/kotlinc/bin/kotlinc"
check(File(kotlincBinary).exists()) { "'${File(kotlincBinary).absolutePath}' not found. Run ./gradlew dist first." }
val commandAndArgs = listOf(
kotlincBinary,
srcDir.path,
classesDir,
extraClasspath = classpath.map { it.path }.toTypedArray()
"-d", classesDir.path,
"-classpath", (listOf(srcDir) + classpath).joinToString(File.pathSeparator) { it.path }
)
runCommandInNewProcess(commandAndArgs)
return getClassFilesInDir(classesDir)
}
@@ -164,11 +176,52 @@ abstract class ClasspathSnapshotTestCommon {
fun ClassFile.readBytes() = asFile().readBytes()
fun ClassFile.snapshot(): ClassSnapshot = listOf(this).snapshot().single()
fun ClassFile.snapshot(granularity: ClassSnapshotGranularity? = null): ClassSnapshot = listOf(this).snapshot(granularity).single()
fun List<ClassFile>.snapshot(): List<ClassSnapshot> {
val classFilesWithContents = this.map { ClassFileWithContents(it, it.readBytes()) }
return ClassSnapshotter.snapshot(classFilesWithContents)
fun List<ClassFile>.snapshot(granularity: ClassSnapshotGranularity? = null): List<ClassSnapshot> {
val classes = map { ClassFileWithContents(it, it.readBytes()) }
return if (granularity == null) {
ClassSnapshotter.snapshot(classes)
} else {
ClassSnapshotter.snapshot(classes, granularity = granularity)
}
}
}
}
internal fun snapshotClasspath(
classpathSourceDir: File,
tmpDir: TemporaryFolder,
granularity: ClassSnapshotGranularity? = null
): ClasspathSnapshot {
val classpath = mutableListOf<File>()
val classpathEntrySnapshots = classpathSourceDir.listFiles()!!.sortedBy { it.name }.map { classpathEntrySourceDir ->
val classFiles = compileAll(classpathEntrySourceDir, tmpDir, classpath)
classpath.addAll(listOfNotNull(classFiles.firstOrNull()?.classRoot))
val relativePaths = classFiles.map { it.unixStyleRelativePath }
val classSnapshots = classFiles.snapshot(granularity)
ClasspathEntrySnapshot(
classSnapshots = relativePaths.zip(classSnapshots).toMap(LinkedHashMap())
)
}
return ClasspathSnapshot(classpathEntrySnapshots)
}
private fun runCommandInNewProcess(commandAndArgs: List<String>) {
val processBuilder = ProcessBuilder(commandAndArgs)
processBuilder.redirectInput(Redirect.INHERIT)
processBuilder.redirectOutput(Redirect.INHERIT)
processBuilder.redirectErrorStream(true)
val process = processBuilder.start()
val exitCode = try {
process.waitFor()
} finally {
process.destroyForcibly()
}
check(exitCode == 0) {
"Process returned exit code: $exitCode\n" +
"commandAndArgs = ${commandAndArgs.joinToString(" ")}"
}
}
@@ -6,94 +6,159 @@
package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.ClassFileUtil.readBytes
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.ClassFileUtil.snapshot
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.SourceFile.JavaSourceFile
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.SourceFile.KotlinSourceFile
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.TestSourceFile
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import java.io.File
abstract class ClasspathSnapshotterTest : ClasspathSnapshotTestCommon() {
private val testDataDir =
File("compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotterTest")
companion object {
val testDataDir =
File("compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotterTest")
}
class KotlinOnlyClasspathSnapshotterTest : ClasspathSnapshotTestCommon() {
protected abstract val sourceFile: TestSourceFile
protected abstract val sourceFileWithAbiChange: TestSourceFile
protected abstract val sourceFileWithNonAbiChange: TestSourceFile
private val expectedSnapshotFile: File
get() = sourceFile.asFile().let {
val srcDir = File(it.path.substringBeforeLast("src") + "src")
val relativePath = it.relativeTo(srcDir)
testDataDir.resolve("expected-snapshot").resolve(relativePath.parent).resolve(relativePath.nameWithoutExtension + ".json")
}
@Suppress("SameParameterValue")
private fun getSourceFile(testName: String, relativePath: String) = TestSourceFile(
KotlinSourceFile(
baseDir = File("$testDataDir/kotlin/$testName/src"), relativePath = relativePath,
preCompiledClassFile = ClassFile(File("$testDataDir/kotlin/$testName/classes"), relativePath.replace(".kt", ".class"))
), tmpDir
)
@Test
fun `test ClassSnapshotter's result against expected snapshot`() {
val classSnapshot = sourceFile.compileSingle().let {
ClassSnapshotter.snapshot(listOf(ClassFileWithContents(it, it.readBytes())), includeDebugInfoInJavaSnapshot = true)
}.single()
fun testSimpleClass() {
val sourceFile = getSourceFile("testSimpleClass", "com/example/SimpleClass.kt")
val actualSnapshot = sourceFile.compileAndSnapshot().toGson()
val expectedSnapshot = sourceFile.getExpectedSnapshotFile().readText()
assertEquals(expectedSnapshotFile.readText(), classSnapshot.toGson())
assertEquals(expectedSnapshot, actualSnapshot)
// Check that the snapshot contains ABI info
actualSnapshot.assertContains("publicProperty", "publicFunction")
// Private properties and functions' names/signatures are currently part of the snapshot. We will fix this later.
actualSnapshot.assertContains("privateProperty", "privateFunction")
// Check that the snapshot does not contain non-ABI info
actualSnapshot.assertDoesNotContain(
"publicProperty's value",
"privateProperty's value",
"publicFunction's body",
"privateFunction's body"
)
}
@Test
fun `test ClassSnapshotter extracts ABI info from a class`() {
// After an ABI change, the snapshot must change
assertNotEquals(sourceFile.compileAndSnapshot().toGson(), sourceFileWithAbiChange.compileAndSnapshot().toGson())
fun testSimpleClass_ClassLevelSnapshot() {
val sourceFile = getSourceFile("testSimpleClass", "com/example/SimpleClass.kt")
val classFile = sourceFile.compileSingle()
val actualSnapshot = classFile.snapshot(ClassSnapshotGranularity.CLASS_LEVEL).toGson()
val expectedSnapshot = sourceFile.getExpectedSnapshotFile(ClassSnapshotGranularity.CLASS_LEVEL).readText()
assertEquals(expectedSnapshot, actualSnapshot)
// Check that the snapshot does not contain class member details
actualSnapshot.assertDoesNotContain("publicProperty", "privateProperty", "publicFunction", "privateFunction")
}
@Test
fun `test ClassSnapshotter does not extract non-ABI info from a class`() {
// After a non-ABI change, the snapshot must not change
assertEquals(sourceFile.compileAndSnapshot().toGson(), sourceFileWithNonAbiChange.compileAndSnapshot().toGson())
fun testPackageFacadeClasses() {
val classpathSnapshot = snapshotClasspath(File("$testDataDir/kotlin/testPackageFacadeClasses/src"), tmpDir)
val classSnapshots = classpathSnapshot.classpathEntrySnapshots.single().classSnapshots
val fileFacadeSnapshot = classSnapshots["com/example/FileFacadeKt.class"]!!.toGson()
val multifileClassSnapshot = classSnapshots["com/example/MultifileClass.class"]!!.toGson()
val multifileClassPart1Snapshot = classSnapshots["com/example/MultifileClass__MultifileClass1Kt.class"]!!.toGson()
val multifileClassPart2Snapshot = classSnapshots["com/example/MultifileClass__MultifileClass2Kt.class"]!!.toGson()
// Check that the snapshots contain ABI info
fileFacadeSnapshot.assertContains("propertyInFileFacade", "functionInFileFacade")
multifileClassPart1Snapshot.assertContains("propertyInMultifileClass1", "functionInMultifileClass1")
multifileClassPart2Snapshot.assertContains("propertyInMultifileClass2", "functionInMultifileClass2")
// Check that the snapshots do not contain non-ABI info
fileFacadeSnapshot.assertDoesNotContain("propertyInFileFacade's value", "functionInFileFacade's body")
multifileClassPart1Snapshot.assertDoesNotContain("propertyInMultifileClass1's value", "functionInMultifileClass1's body")
multifileClassPart2Snapshot.assertDoesNotContain("propertyInMultifileClass2's value", "functionInMultifileClass2's body")
// Classes with MULTIFILE_CLASS kind have no proto data
multifileClassSnapshot.assertDoesNotContain(
"propertyInMultifileClass1",
"functionInMultifileClass1",
"propertyInMultifileClass2",
"functionInMultifileClass2"
)
}
}
class KotlinOnlyClasspathSnapshotterTest : ClasspathSnapshotterTest() {
class JavaOnlyClasspathSnapshotterTest : ClasspathSnapshotTestCommon() {
override val sourceFile = TestSourceFile(
KotlinSourceFile(
baseDir = File(testDataDir, "src/kotlin"), relativePath = "com/example/SimpleClass.kt",
preCompiledClassFile = ClassFile(File(testDataDir, "classes/kotlin"), "com/example/SimpleClass.class")
), tmpDir
@Suppress("SameParameterValue")
private fun getSourceFile(testName: String, relativePath: String) = TestSourceFile(
JavaSourceFile(baseDir = File("$testDataDir/java/$testName/src"), relativePath = relativePath), tmpDir
)
override val sourceFileWithAbiChange = TestSourceFile(
KotlinSourceFile(
baseDir = File(testDataDir, "src-changed/kotlin/abi-change"), relativePath = "com/example/SimpleClass.kt",
preCompiledClassFile = ClassFile(File(testDataDir, "classes-changed/kotllin/abi-change"), "com/example/SimpleClass.class")
), tmpDir
)
private fun TestSourceFile.compileAndSnapshotWithDebugInfo(): ClassSnapshot {
val classFile = compileSingle()
return ClassSnapshotter.snapshot(
listOf(ClassFileWithContents(classFile, classFile.readBytes())),
includeDebugInfoInJavaSnapshot = true
).single()
}
override val sourceFileWithNonAbiChange = TestSourceFile(
KotlinSourceFile(
baseDir = File(testDataDir, "src-changed/kotlin/non-abi-change"), relativePath = "com/example/SimpleClass.kt",
preCompiledClassFile = ClassFile(File(testDataDir, "classes-changed/kotlin/non-abi-change"), "com/example/SimpleClass.class")
), tmpDir
)
@Test
fun testSimpleClass() {
val sourceFile = getSourceFile("testSimpleClass", "com/example/SimpleClass.java")
val actualSnapshot = sourceFile.compileAndSnapshotWithDebugInfo().toGson()
val expectedSnapshot = sourceFile.getExpectedSnapshotFile().readText()
assertEquals(expectedSnapshot, actualSnapshot)
// Check that the snapshot contains ABI info
actualSnapshot.assertContains("publicField", "publicMethod")
// Check that the snapshot does not contain non-ABI info
actualSnapshot.assertDoesNotContain(
"privateField",
"privateMethod",
"publicField's value",
"privateField's value",
"publicMethod's body",
"privateMethod's body"
)
}
@Test
fun testSimpleClass_ClassLevelSnapshot() {
val sourceFile = getSourceFile("testSimpleClass", "com/example/SimpleClass.java")
val classFile = sourceFile.compileSingle()
val actualSnapshot = classFile.snapshot(ClassSnapshotGranularity.CLASS_LEVEL).toGson()
val expectedSnapshot = sourceFile.getExpectedSnapshotFile(ClassSnapshotGranularity.CLASS_LEVEL).readText()
assertEquals(expectedSnapshot, actualSnapshot)
// Check that the snapshot does not contain class member details
actualSnapshot.assertDoesNotContain("publicField", "privateField", "publicMethod", "privateMethod")
}
}
class JavaOnlyClasspathSnapshotterTest : ClasspathSnapshotterTest() {
override val sourceFile = TestSourceFile(
JavaSourceFile(
baseDir = File(testDataDir, "src/java"), relativePath = "com/example/SimpleClass.java",
), tmpDir
)
override val sourceFileWithAbiChange = TestSourceFile(
JavaSourceFile(
baseDir = File(testDataDir, "src-changed/java/abi-change"), relativePath = "com/example/SimpleClass.java",
), tmpDir
)
override val sourceFileWithNonAbiChange = TestSourceFile(
JavaSourceFile(
baseDir = File(testDataDir, "src-changed/java/non-abi-change"), relativePath = "com/example/SimpleClass.java",
), tmpDir
)
private fun TestSourceFile.getExpectedSnapshotFile(granularity: ClassSnapshotGranularity? = null): File {
val relativePath = sourceFile.unixStyleRelativePath.substringBeforeLast(".") + ".json"
val expectedSnapshotDirName = if (granularity == null) "expected-snapshot" else "expected-snapshot-${granularity.name}"
return sourceFile.baseDir.resolve("../$expectedSnapshotDirName/$relativePath")
}
private fun String.assertContains(vararg elements: String) {
elements.forEach {
assertTrue(contains(it))
}
}
private fun String.assertDoesNotContain(vararg elements: String) {
elements.forEach {
assertFalse(contains(it))
}
}
@@ -1,5 +1,6 @@
package com.example
@Suppress("NOTHING_TO_INLINE")
class SomeClass {
// Constants are not allowed in a class, only allowed at the top level or in an object.
@@ -1,5 +1,6 @@
package com.example
@Suppress("NOTHING_TO_INLINE")
class SomeClass {
// Constants are not allowed in a class, only allowed at the top level or in an object.
@@ -0,0 +1,10 @@
package com.example;
public class CoarseGrainedFirstBuild_CoarseGrainedSecondBuild_Class {
public long modifiedField = 0;
public long modifiedMethod() {
return 0;
}
}
@@ -0,0 +1,10 @@
package com.example;
public class CoarseGrainedFirstBuild_FineGrainedSecondBuild_Class {
public long modifiedField = 0;
public long modifiedMethod() {
return 0;
}
}
@@ -0,0 +1,10 @@
package com.example;
public class FineGrainedFirstBuild_CoarseGrainedSecondBuild_Class {
public long modifiedField = 0;
public long modifiedMethod() {
return 0;
}
}
@@ -0,0 +1,10 @@
package com.example;
public class FineGrainedFirstBuild_FineGrainedSecondBuild_Class {
public long modifiedField = 0;
public long modifiedMethod() {
return 0;
}
}
@@ -0,0 +1,10 @@
package com.example;
public class CoarseGrainedFirstBuild_CoarseGrainedSecondBuild_Class {
public int modifiedField = 0;
public int modifiedMethod() {
return 0;
}
}
@@ -0,0 +1,10 @@
package com.example;
public class CoarseGrainedFirstBuild_FineGrainedSecondBuild_Class {
public int modifiedField = 0;
public int modifiedMethod() {
return 0;
}
}
@@ -0,0 +1,10 @@
package com.example;
public class FineGrainedFirstBuild_CoarseGrainedSecondBuild_Class {
public int modifiedField = 0;
public int modifiedMethod() {
return 0;
}
}
@@ -0,0 +1,10 @@
package com.example;
public class FineGrainedFirstBuild_FineGrainedSecondBuild_Class {
public int modifiedField = 0;
public int modifiedMethod() {
return 0;
}
}
@@ -0,0 +1,6 @@
package com.example
class CoarseGrainedFirstBuild_CoarseGrainedSecondBuild_Class {
val modifiedProperty: Long = 0
fun modifiedFunction(): Long = 0
}
@@ -0,0 +1,6 @@
package com.example
class CoarseGrainedFirstBuild_FineGrainedSecondBuild_Class {
val modifiedProperty: Long = 0
fun modifiedFunction(): Long = 0
}
@@ -0,0 +1,6 @@
package com.example
class FineGrainedFirstBuild_CoarseGrainedSecondBuild_Class {
val modifiedProperty: Long = 0
fun modifiedFunction(): Long = 0
}
@@ -0,0 +1,6 @@
package com.example
class FineGrainedFirstBuild_FineGrainedSecondBuild_Class {
val modifiedProperty: Long = 0
fun modifiedFunction(): Long = 0
}
@@ -0,0 +1,6 @@
package com.example
class CoarseGrainedFirstBuild_CoarseGrainedSecondBuild_Class {
val modifiedProperty: Int = 0
fun modifiedFunction(): Int = 0
}
@@ -0,0 +1,6 @@
package com.example
class CoarseGrainedFirstBuild_FineGrainedSecondBuild_Class {
val modifiedProperty: Int = 0
fun modifiedFunction(): Int = 0
}
@@ -0,0 +1,6 @@
package com.example
class FineGrainedFirstBuild_CoarseGrainedSecondBuild_Class {
val modifiedProperty: Int = 0
fun modifiedFunction(): Int = 0
}
@@ -0,0 +1,6 @@
package com.example
class FineGrainedFirstBuild_FineGrainedSecondBuild_Class {
val modifiedProperty: Int = 0
fun modifiedFunction(): Int = 0
}
@@ -1,6 +1,6 @@
package com.example
@java.lang.Deprecated // Changed annotation
@kotlin.Deprecated("") // Changed annotation
class ModifiedClassUnchangedMembers {
val unchangedProperty = 0
fun unchangedFunction() {}
@@ -1,52 +0,0 @@
{
"classId": {
"packageFqName": {
"fqName": {
"fqName": "com.example"
}
},
"relativeClassName": {
"fqName": {
"fqName": "SimpleClass"
}
},
"local": false
},
"supertypes": [
{
"internalName": "java/lang/Object"
}
],
"classAbiExcludingMembers": {
"abiValue": "{\n \"version\": 52,\n \"access\": 32,\n \"name\": \"com/example/SimpleClass\",\n \"superName\": \"java/lang/Object\",\n \"interfaces\": [],\n \"sourceFile\": \"SimpleClass.java\",\n \"innerClasses\": [],\n \"fields\": [],\n \"methods\": [],\n \"api\": 589824\n}",
"name": "com/example/SimpleClass",
"abiHash": 5093067435262353961
},
"fieldsAbi": [
{
"abiValue": "{\n \"access\": 1,\n \"name\": \"publicField\",\n \"desc\": \"I\",\n \"api\": 589824\n}",
"name": "publicField",
"abiHash": -4066101678319939363
}
],
"methodsAbi": [
{
"abiValue": "{\n \"access\": 0,\n \"name\": \"\\u003cinit\\u003e\",\n \"desc\": \"()V\",\n \"exceptions\": [],\n \"visibleAnnotableParameterCount\": 0,\n \"invisibleAnnotableParameterCount\": 0,\n \"instructions\": {\n \"size\": 0\n },\n \"tryCatchBlocks\": [],\n \"maxStack\": 0,\n \"maxLocals\": 0,\n \"localVariables\": [],\n \"visited\": false,\n \"api\": 589824\n}",
"name": "\u003cinit\u003e",
"abiHash": -8222317811013429324
},
{
"abiValue": "{\n \"access\": 1,\n \"name\": \"publicMethod\",\n \"desc\": \"()I\",\n \"exceptions\": [],\n \"visibleAnnotableParameterCount\": 0,\n \"invisibleAnnotableParameterCount\": 0,\n \"instructions\": {\n \"size\": 0\n },\n \"tryCatchBlocks\": [],\n \"maxStack\": 0,\n \"maxLocals\": 0,\n \"localVariables\": [],\n \"visited\": false,\n \"api\": 589824\n}",
"name": "publicMethod",
"abiHash": -4555436938653125191
}
],
"className$delegate": {
"initializer": {},
"_value": {}
},
"withHash$delegate": {
"initializer": {},
"_value": {}
}
}
@@ -0,0 +1,21 @@
{
"classId": {
"packageFqName": {
"fqName": {
"fqName": "com.example"
}
},
"relativeClassName": {
"fqName": {
"fqName": "SimpleClass"
}
},
"local": false
},
"classAbiHash": 2419143040117344894,
"supertypes": [
{
"internalName": "java/lang/Object"
}
]
}
@@ -0,0 +1,47 @@
{
"classId": {
"packageFqName": {
"fqName": {
"fqName": "com.example"
}
},
"relativeClassName": {
"fqName": {
"fqName": "SimpleClass"
}
},
"local": false
},
"classAbiHash": 7253633932031152470,
"classMemberLevelSnapshot": {
"classAbiExcludingMembers": {
"abiValue": "{\n \"version\": 52,\n \"access\": 33,\n \"name\": \"com/example/SimpleClass\",\n \"superName\": \"java/lang/Object\",\n \"interfaces\": [],\n \"sourceFile\": \"SimpleClass.java\",\n \"innerClasses\": [],\n \"fields\": [],\n \"methods\": [],\n \"api\": 589824\n}",
"name": "com/example/SimpleClass",
"abiHash": -1986494095366785349
},
"fieldsAbi": [
{
"abiValue": "{\n \"access\": 1,\n \"name\": \"publicField\",\n \"desc\": \"Ljava/lang/String;\",\n \"api\": 589824\n}",
"name": "publicField",
"abiHash": 1021768301411937004
}
],
"methodsAbi": [
{
"abiValue": "{\n \"access\": 1,\n \"name\": \"\\u003cinit\\u003e\",\n \"desc\": \"()V\",\n \"exceptions\": [],\n \"visibleAnnotableParameterCount\": 0,\n \"invisibleAnnotableParameterCount\": 0,\n \"instructions\": {\n \"size\": 0\n },\n \"tryCatchBlocks\": [],\n \"maxStack\": 0,\n \"maxLocals\": 0,\n \"localVariables\": [],\n \"visited\": false,\n \"api\": 589824\n}",
"name": "\u003cinit\u003e",
"abiHash": 5725188035643409224
},
{
"abiValue": "{\n \"access\": 1,\n \"name\": \"publicMethod\",\n \"desc\": \"()Ljava/lang/String;\",\n \"exceptions\": [],\n \"visibleAnnotableParameterCount\": 0,\n \"invisibleAnnotableParameterCount\": 0,\n \"instructions\": {\n \"size\": 0\n },\n \"tryCatchBlocks\": [],\n \"maxStack\": 0,\n \"maxLocals\": 0,\n \"localVariables\": [],\n \"visited\": false,\n \"api\": 589824\n}",
"name": "publicMethod",
"abiHash": 8210873051092995586
}
]
},
"supertypes": [
{
"internalName": "java/lang/Object"
}
]
}
@@ -0,0 +1,16 @@
package com.example;
public class SimpleClass {
public String publicField = "publicField's value";
private String privateField = "privateField's value";
public String publicMethod() {
return "publicMethod's body";
}
private String privateMethod() {
return "privateMethod's body";
}
}
@@ -0,0 +1,4 @@
package com.example
val propertyInFileFacade = "propertyInFileFacade's value"
fun functionInFileFacade() = "functionInFileFacade's body"
@@ -0,0 +1,7 @@
@file:JvmName("MultifileClass")
@file:JvmMultifileClass
package com.example
val propertyInMultifileClass1 = "propertyInMultifileClass1's value"
fun functionInMultifileClass1() = "functionInMultifileClass1's body"
@@ -0,0 +1,7 @@
@file:JvmName("MultifileClass")
@file:JvmMultifileClass
package com.example
val propertyInMultifileClass2 = "propertyInMultifileClass2's value"
fun functionInMultifileClass2() = "functionInMultifileClass2's body"
@@ -0,0 +1,21 @@
{
"classId": {
"packageFqName": {
"fqName": {
"fqName": "com.example"
}
},
"relativeClassName": {
"fqName": {
"fqName": "SimpleClass"
}
},
"local": false
},
"classAbiHash": -2605231127310346403,
"supertypes": [
{
"internalName": "java/lang/Object"
}
]
}
@@ -1,5 +1,19 @@
{
"classInfo": {
"classId": {
"packageFqName": {
"fqName": {
"fqName": "com.example"
}
},
"relativeClassName": {
"fqName": {
"fqName": "SimpleClass"
}
},
"local": false
},
"classAbiHash": -2605231127310346403,
"classMemberLevelSnapshot": {
"classId": {
"packageFqName": {
"fqName": {
@@ -15,7 +29,7 @@
},
"classKind": "CLASS",
"classHeaderData": [
"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\b\n\u0002\b\u0005\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\b\u0010\b\u001a\u00020\u0004H\u0002J\u0006\u0010\t\u001a\u00020\u0004R\u000e\u0010\u0003\u001a\u00020\u0004X‚D¢\u0006\u0002\n\u0000R\u0014\u0010\u0005\u001a\u00020\u0004X†D¢\u0006\b\n\u0000\u001a\u0004\b\u0006\u0010\u0007"
"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0002\b\u0005\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\b\u0010\b\u001a\u00020\u0004H\u0002J\u0006\u0010\t\u001a\u00020\u0004R\u000e\u0010\u0003\u001a\u00020\u0004X‚D¢\u0006\u0002\n\u0000R\u0014\u0010\u0005\u001a\u00020\u0004X†D¢\u0006\b\n\u0000\u001a\u0004\b\u0006\u0010\u0007"
],
"classHeaderStrings": [
"Lcom/example/SimpleClass;",
@@ -25,7 +39,7 @@
"",
"publicProperty",
"getPublicProperty",
"()I",
"()Ljava/lang/String;",
"privateFunction",
"publicFunction"
],
@@ -48,9 +62,5 @@
{
"internalName": "java/lang/Object"
}
],
"withHash$delegate": {
"initializer": {},
"_value": {}
}
]
}
@@ -0,0 +1,12 @@
package com.example
class SimpleClass {
val publicProperty: String = "publicProperty's value"
private val privateProperty: String = "privateProperty's value"
fun publicFunction(): String = "publicFunction's body"
private fun privateFunction(): String = "privateFunction's body"
}
@@ -1,16 +0,0 @@
package com.example;
class SimpleClass {
public long publicField = 0;
private long privateField = 0;
public long publicMethod() {
return 0;
}
private long privateMethod() {
return 0;
}
}
@@ -1,16 +0,0 @@
package com.example;
class SimpleClass {
public int publicField = 1000;
private int privateField = 1000;
public int publicMethod() {
return 1000;
}
private int privateMethod() {
return 1000;
}
}
@@ -1,12 +0,0 @@
package com.example
class SimpleClass {
val publicProperty: Long = 0
private val privateProperty: Long = 0
fun publicFunction(): Long = 0
private fun privateFunction(): Long = 0
}
@@ -1,12 +0,0 @@
package com.example
class SimpleClass {
val publicProperty: Int = 1000
private val privateProperty: Int = 1000
fun publicFunction(): Int = 1000
private fun privateFunction(): Int = 1000
}
@@ -1,16 +0,0 @@
package com.example;
class SimpleClass {
public int publicField = 0;
private int privateField = 0;
public int publicMethod() {
return 0;
}
private int privateMethod() {
return 0;
}
}
@@ -1,12 +0,0 @@
package com.example
class SimpleClass {
val publicProperty: Int = 0
private val privateProperty: Int = 0
fun publicFunction(): Int = 0
private fun privateFunction(): Int = 0
}
@@ -11,7 +11,7 @@ import org.jetbrains.kotlin.gradle.internal.tasks.allOutputFiles
import org.jetbrains.kotlin.gradle.logging.GradleKotlinLogger
import org.jetbrains.kotlin.gradle.logging.kotlinDebug
import org.jetbrains.kotlin.incremental.cleanDirectoryContents
import org.jetbrains.kotlin.incremental.forceDeleteRecursively
import org.jetbrains.kotlin.incremental.deleteRecursivelyOrThrow
import java.io.File
fun throwGradleExceptionIfError(
@@ -63,7 +63,7 @@ internal fun cleanOutputsAndLocalState(
}
file.isFile -> {
log.debug("Deleting output file: $file")
file.forceDeleteRecursively()
file.deleteRecursivelyOrThrow()
}
}
}