KT-45777: Move classpath diffing to incremental Kotlin compiler (1/2)

This commit only changes the files' paths, the next commit will update
the files' contents.

See the next commit for more context.
This commit is contained in:
Hung Nguyen
2021-11-04 20:22:21 +00:00
committed by teamcityserver
parent c46e9943cc
commit dfaf195e1d
102 changed files with 24 additions and 24 deletions
@@ -0,0 +1,103 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.incremental.ClasspathChanges
import org.jetbrains.kotlin.incremental.LookupSymbol
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
/** Intermediate data to compute [ClasspathChanges] (see [toClasspathChanges]). */
class ChangeSet(
/** Set of changed classes, preferably ordered by not required. */
val changedClasses: Set<ClassId>,
/**
* Map from a [ClassId] to the names of changed Java fields/methods or Kotlin properties/functions within that class.
*
* The map and sets are preferably ordered but not required.
*
* The [ClassId]s must not appear in [changedClasses] to avoid redundancy.
*/
val changedClassMembers: Map<ClassId, Set<String>>,
/** Map from a package name to the names of changed Kotlin top-level properties/functions within that package. */
val changedTopLevelMembers: Map<FqName, Set<String>>
) {
init {
check(changedClassMembers.keys.none { it in changedClasses })
}
class Collector {
private val changedClasses = mutableSetOf<ClassId>()
private val changedClassMembers = mutableMapOf<ClassId, MutableSet<String>>()
private val changedTopLevelMembers = mutableMapOf<FqName, MutableSet<String>>()
fun addChangedClasses(classNames: Collection<ClassId>) = changedClasses.addAll(classNames)
fun addChangedClass(className: ClassId) = changedClasses.add(className)
fun addChangedClassMembers(className: ClassId, memberNames: Collection<String>) {
if (memberNames.isNotEmpty()) {
changedClassMembers.computeIfAbsent(className) { mutableSetOf() }.addAll(memberNames)
}
}
fun addChangedClassMember(className: ClassId, memberName: String) = addChangedClassMembers(className, listOf(memberName))
fun addChangedTopLevelMembers(packageName: FqName, topLevelMembers: Collection<String>) {
if (topLevelMembers.isNotEmpty()) {
changedTopLevelMembers.computeIfAbsent(packageName) { mutableSetOf() }.addAll(topLevelMembers)
}
}
fun addChangedTopLevelMember(packageName: FqName, topLevelMember: String) =
addChangedTopLevelMembers(packageName, listOf(topLevelMember))
fun getChanges(): ChangeSet {
// Remove redundancy in the change set first
changedClassMembers.keys.intersect(changedClasses).forEach { changedClassMembers.remove(it) }
return ChangeSet(changedClasses.toSet(), changedClassMembers.toMap(), changedTopLevelMembers.toMap())
}
}
fun isEmpty() = changedClasses.isEmpty() && changedClassMembers.isEmpty() && changedTopLevelMembers.isEmpty()
operator fun plus(other: ChangeSet) = ChangeSet(
changedClasses + other.changedClasses,
changedClassMembers + other.changedClassMembers,
changedTopLevelMembers + other.changedTopLevelMembers
)
fun toClasspathChanges(): ClasspathChanges.Available {
val lookupSymbols = mutableSetOf<LookupSymbol>()
val fqNames = mutableSetOf<FqName>()
changedClasses.forEach {
val classFqName = it.asSingleFqName()
lookupSymbols.add(LookupSymbol(name = classFqName.shortName().asString(), scope = classFqName.parent().asString()))
fqNames.add(classFqName)
}
for ((changedClass, changedClassMembers) in changedClassMembers) {
val classFqName = changedClass.asSingleFqName()
changedClassMembers.forEach {
lookupSymbols.add(LookupSymbol(name = it, scope = classFqName.asString()))
}
fqNames.add(classFqName)
}
for ((changedPackage, changedTopLevelMembers) in changedTopLevelMembers) {
changedTopLevelMembers.forEach {
lookupSymbols.add(LookupSymbol(name = it, scope = changedPackage.asString()))
}
fqNames.add(changedPackage)
}
return ClasspathChanges.Available(lookupSymbols, fqNames)
}
}
@@ -0,0 +1,36 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.incremental.classpathDiff
import java.io.File
/** Information to locate a .class file. */
class ClassFile(
/** Directory or jar containing the .class file. */
val classRoot: File,
/**
* The relative path from [classRoot] to the .class file.
*
* Any '\' characters in the path will be replaced with '/' to create [unixStyleRelativePath].
*/
relativePath: String
) {
/** The Unix-style relative path (with '/' as separators) from [classRoot] to the .class file. */
val unixStyleRelativePath: String
init {
unixStyleRelativePath = relativePath.replace('\\', '/')
}
}
/** Information to locate a .class file, plus their contents. */
class ClassFileWithContents(
val classFile: ClassFile,
val contents: ByteArray
)
@@ -0,0 +1,396 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.incremental.classpathDiff
import com.intellij.openapi.util.io.FileUtil
import org.jetbrains.kotlin.incremental.classpathDiff.ImpactAnalysis.computeImpactedSet
import org.jetbrains.kotlin.incremental.*
import org.jetbrains.kotlin.incremental.storage.FileToCanonicalPathConverter
import org.jetbrains.kotlin.metadata.deserialization.TypeTable
import org.jetbrains.kotlin.metadata.deserialization.supertypes
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.kotlin.serialization.deserialization.getClassId
import java.io.File
import java.util.*
/** Computes [ClasspathChanges] between two [ClasspathSnapshot]s .*/
object ClasspathChangesComputer {
fun compute(
currentClasspathEntrySnapshotFiles: List<File>,
previousClasspathEntrySnapshotFiles: List<File>,
unchangedCurrentClasspathEntrySnapshotFiles: List<File>
): ClasspathChanges {
// To improve performance, we will compute changes for the changed snapshot files only, ignoring unchanged ones. (Duplicate classes
// will make this a bit tricky, but it will be dealt with below.)
// First, align unchanged snapshot files in the current classpath with unchanged snapshot files in the previous classpath. Gradle
// has this information, but doesn't expose it, so we have to reconstruct it here.
val unchangedCurrentToPreviousAlignment: Map<File, File> =
alignUnchangedSnapshotFiles(unchangedCurrentClasspathEntrySnapshotFiles, previousClasspathEntrySnapshotFiles)
// Use sets to make presence checks faster
val unchangedCurrentFiles: Set<File> = unchangedCurrentToPreviousAlignment.keys
val unchangedPreviousFiles: Set<File> = unchangedCurrentToPreviousAlignment.values.toSet()
// We will split the current files into 2 groups:
// 1a) Unchanged current files
// 1b) Added files
// We will split the previous files into 2 groups:
// 2a) Unchanged previous files
// 2b) Removed files
// If the classpath doesn't contain duplicate classes, comparing (1b) with (2b) would be enough.
// However, if the classpath contains duplicate classes, comparing (1b) with (2b) would not be enough.
// Therefore, to deal with duplicate classes while still being able to compare (1b) with (2b), we will find snapshot files in groups
// (1a) and (2a) that have duplicate classes with groups (1b) or (2b) and add them to groups (1b) and (2b). Duplicate classes in
// groups (1b) and (2b) will then be handled in a separate step (see ClasspathChangesComputer.getNonDuplicateClassSnapshots).
val addedFiles: List<File> = currentClasspathEntrySnapshotFiles.filter { it !in unchangedCurrentFiles }
val removedFiles: List<File> = previousClasspathEntrySnapshotFiles.filter { it !in unchangedPreviousFiles }
val adjustedAddedFiles = addedFiles.toMutableSet()
val adjustedRemovedFiles = removedFiles.toMutableSet()
unchangedCurrentToPreviousAlignment.forEach { (unchangedCurrentFile, unchangedPreviousFile) ->
if (unchangedCurrentFile.containsDuplicatesWith(addedFiles) || unchangedPreviousFile.containsDuplicatesWith(removedFiles)) {
adjustedAddedFiles.add(unchangedCurrentFile)
adjustedRemovedFiles.add(unchangedPreviousFile)
}
}
// Keep the original order of added/removed files as it is important for the handling of duplicate classes.
val finalAddedFiles: List<File> = currentClasspathEntrySnapshotFiles.filter { it in adjustedAddedFiles }
val finalRemovedFiles: List<File> = previousClasspathEntrySnapshotFiles.filter { it in adjustedRemovedFiles }
val changedCurrentSnapshot = ClasspathSnapshotSerializer.load(finalAddedFiles)
val changedPreviousSnapshot = ClasspathSnapshotSerializer.load(finalRemovedFiles)
return compute(changedCurrentSnapshot, changedPreviousSnapshot)
}
/**
* Maps the unchanged snapshot files of the current build to the unchanged snapshot files of the previous build (selected from all the
* snapshot files of the previous build).
*
* Note that the unchanged files of the current build were detected by Gradle, so a mapping must exist for each of them, we only have to
* find it.
*
* IMPORTANT: The alignment algorithm must use the same input normalization that is used for snapshot files in the Gradle task.
* Currently, snapshot files are annotated with `@Classpath` and are regular files (not jars), so (only) their contents and order
* matter.
*/
private fun alignUnchangedSnapshotFiles(unchangedCurrentSnapshotFiles: List<File>, previousSnapshotFiles: List<File>): Map<File, File> {
val sizeToPreviousFiles: Map<Long, List<IndexedValue<File>>> = previousSnapshotFiles.withIndex().groupBy { it.value.length() }
var startIndexToSearch = 0
return unchangedCurrentSnapshotFiles.associateWith { unchangedCurrentFile ->
val candidates = (sizeToPreviousFiles[unchangedCurrentFile.length()] ?: emptyList()).filter { it.index >= startIndexToSearch }
val unchangedPreviousFileWithIndex: IndexedValue<File> = if (candidates.size == 1) {
// A matching file must exist, so if there is only one candidate, it is the one.
candidates.single()
} else {
// If there are multiple matching files, select the first one. (Even if it doesn't match Gradle's alignment, it is still a
// correct alignment.)
val unchangedContents = unchangedCurrentFile.readBytes()
candidates.firstOrNull { candidate ->
unchangedContents.contentEquals(candidate.value.readBytes())
} ?: error("Can't find previous snapshot file of unchanged current snapshot file '${unchangedCurrentFile.path}'")
}
startIndexToSearch = unchangedPreviousFileWithIndex.index + 1
unchangedPreviousFileWithIndex.value
}
}
/** Returns `true` if this snapshot file contains a duplicate class with another snapshot file in the given list. */
@Suppress("unused", "UNUSED_PARAMETER")
private fun File.containsDuplicatesWith(otherSnapshotFiles: List<File>): Boolean {
// FIXME: Implement and optimize this method
return false
}
fun compute(currentClasspathSnapshot: ClasspathSnapshot, previousClasspathSnapshot: ClasspathSnapshot): ClasspathChanges {
val currentClassSnapshots = currentClasspathSnapshot.getNonDuplicateClassSnapshots()
val previousClassSnapshots = previousClasspathSnapshot.getNonDuplicateClassSnapshots()
return computeClassChanges(currentClassSnapshots, previousClassSnapshots)
}
/**
* Returns all [ClassSnapshot]s in this [ClasspathSnapshot].
*
* If there are duplicate classes on the classpath, retain only the first one to match the compiler's behavior.
*/
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)
}
}
return classSnapshots.values.toList()
}
/**
* Computes changes between two lists of [ClassSnapshot]s.
*
* Each list must not contain duplicate classes.
*/
fun computeClassChanges(currentClassSnapshots: List<ClassSnapshot>, previousClassSnapshots: List<ClassSnapshot>): ClasspathChanges {
if (currentClassSnapshots.any { it is ContentHashJavaClassSnapshot }
|| previousClassSnapshots.any { it is ContentHashJavaClassSnapshot }) {
return ClasspathChanges.NotAvailable.UnableToCompute
}
// Ignore `EmptyJavaClassSnapshot`s as they don't impact the result
val currentNonEmptyClassSnapshots = currentClassSnapshots.filter { it !is EmptyJavaClassSnapshot }
val previousNonEmptyClassSnapshots = previousClassSnapshots.filter { it !is EmptyJavaClassSnapshot }
val (currentAsmBasedSnapshots, currentProtoBasedSnapshots) =
currentNonEmptyClassSnapshots.partition { it is RegularJavaClassSnapshot }
val (previousAsmBasedSnapshots, previousProtoBasedSnapshots) =
previousNonEmptyClassSnapshots.partition { it is RegularJavaClassSnapshot }
val changeSet1 = computeChangesForProtoBasedSnapshots(currentProtoBasedSnapshots, previousProtoBasedSnapshots)
@Suppress("UNCHECKED_CAST")
val changeSet2 = JavaClassChangesComputer.compute(
currentAsmBasedSnapshots as List<RegularJavaClassSnapshot>,
previousAsmBasedSnapshots as List<RegularJavaClassSnapshot>
)
val allChanges = changeSet1 + changeSet2
if (allChanges.isEmpty()) {
return allChanges.toClasspathChanges()
}
val impactedSet = computeImpactedSet(allChanges, previousNonEmptyClassSnapshots)
return impactedSet.toClasspathChanges()
}
private fun computeChangesForProtoBasedSnapshots(
currentClassSnapshots: List<ClassSnapshot>,
previousClassSnapshots: List<ClassSnapshot>
): ChangeSet {
val workingDir =
FileUtil.createTempDirectory(this::class.java.simpleName, "_WorkingDir_${UUID.randomUUID()}", /* deleteOnExit */ true)
val incrementalJvmCache = IncrementalJvmCache(workingDir, /* targetOutputDir */ null, FileToCanonicalPathConverter)
// Step 1:
// - Add previous class snapshots to incrementalJvmCache.
// - Internally, incrementalJvmCache maintains a set of dirty classes to detect removed classes. Add previous classes to this set
// to detect removed classes later (see step 2).
// - The ChangesCollector result will contain symbols in the previous classes (we actually don't need them, but it's part of the
// API's effects).
val unusedChangesCollector = ChangesCollector()
for (previousSnapshot in previousClassSnapshots) {
when (previousSnapshot) {
is KotlinClassSnapshot -> {
incrementalJvmCache.saveClassToCache(
kotlinClassInfo = previousSnapshot.classInfo,
sourceFiles = null,
changesCollector = unusedChangesCollector
)
incrementalJvmCache.markDirty(previousSnapshot.classInfo.className)
}
is ProtoBasedJavaClassSnapshot -> {
incrementalJvmCache.saveJavaClassProto(
source = null,
serializedJavaClass = previousSnapshot.serializedJavaClass,
collector = unusedChangesCollector
)
incrementalJvmCache.markDirty(JvmClassName.byClassId(previousSnapshot.serializedJavaClass.classId))
}
is RegularJavaClassSnapshot, is ContentHashJavaClassSnapshot, is EmptyJavaClassSnapshot -> {
error("Unexpected type (it should have been handled earlier): ${previousSnapshot.javaClass.name}")
}
}
}
// Step 2:
// - Add current class snapshots to incrementalJvmCache. This will overwrite any previous class snapshots that have the same
// `JvmClassName`. The remaining previous class snapshots will be removed in step 3.
// - Internally, incrementalJvmCache will remove current classes from the set of dirty classes. After this, the remaining dirty
// classes will be classes that are present on the previous classpath but not on the current classpath (i.e., removed classes).
// - The intermediate ChangesCollector result will contain symbols in added classes and changed (added/modified/removed) symbols
// in modified classes. We will collect symbols in removed classes in step 3.
val changesCollector = ChangesCollector()
for (currentSnapshot in currentClassSnapshots) {
when (currentSnapshot) {
is KotlinClassSnapshot -> {
incrementalJvmCache.saveClassToCache(
kotlinClassInfo = currentSnapshot.classInfo,
sourceFiles = null,
changesCollector = changesCollector
)
}
is ProtoBasedJavaClassSnapshot -> {
incrementalJvmCache.saveJavaClassProto(
source = null,
serializedJavaClass = currentSnapshot.serializedJavaClass,
collector = changesCollector
)
}
is RegularJavaClassSnapshot, is ContentHashJavaClassSnapshot, is EmptyJavaClassSnapshot -> {
error("Unexpected type (it should have been handled earlier): ${currentSnapshot.javaClass.name}")
}
}
}
// Step 3:
// - Detect removed classes: They are the remaining dirty classes.
// - Remove class snapshots of removed classes from incrementalJvmCache.
// - The final ChangesCollector result will contain symbols in added classes, changed (added/modified/removed) symbols in modified
// classes, and symbols in removed classes.
incrementalJvmCache.clearCacheForRemovedClasses(changesCollector)
// Normalize the changes and clean up
val dirtyData = changesCollector.getDirtyData(listOf(incrementalJvmCache), EmptyICReporter)
workingDir.deleteRecursively()
return dirtyData.normalize(currentClassSnapshots, previousClassSnapshots)
}
private fun DirtyData.normalize(currentClassSnapshots: List<ClassSnapshot>, previousClassSnapshots: List<ClassSnapshot>): ChangeSet {
val allClassIds = currentClassSnapshots.map { it.getClassId() }.toSet() + previousClassSnapshots.map { it.getClassId() }
val fqNameToClassId = LinkedHashMap<FqName, ClassId>(allClassIds.size)
allClassIds.forEach { classId ->
val fqName = classId.asSingleFqName()
check(!fqNameToClassId.contains(fqName)) {
"Ambiguous FqName $fqName corresponds to two different `ClassId`s: ${fqNameToClassId[fqName]} and $classId"
}
fqNameToClassId[fqName] = classId
}
return ChangeSet.Collector().run {
dirtyLookupSymbols.forEach {
fqNameToClassId[FqName(it.scope)]?.let { classIdOfScope ->
// If scope is a class, lookup symbol is a class member and maybe inner class
fqNameToClassId[FqName("${it.scope}.${it.name}")]?.let { innerClass ->
addChangedClass(innerClass)
} ?: addChangedClassMember(classIdOfScope, it.name)
return@forEach
}
// scope is a package, so changed symbol is a top-level member and maybe a class
val potentialClassFqName = if (it.scope.isEmpty()) FqName(it.name) else FqName("${it.scope}.${it.name}")
fqNameToClassId[potentialClassFqName]?.let { classId ->
// Lookup symbol is a class
addChangedClass(classId)
} ?: addChangedTopLevelMember(FqName(it.scope), it.name)
}
val changes = getChanges()
// dirtyClassesFqNames should be derived from dirtyLookupSymbols. Double-check that this is the case.
val changedFqNames: Set<FqName> =
changes.changedClasses.map { it.asSingleFqName() }.toSet() +
changes.changedClassMembers.keys.map { it.asSingleFqName() } +
changes.changedTopLevelMembers.keys
check(dirtyClassesFqNames.toSet() == changedFqNames) {
"Two sets differ:\n" +
"dirtyClassesFqNames: $dirtyClassesFqNames\n" +
"changedFqNames: $changedFqNames"
}
changes
}
}
}
private fun ClassSnapshot.getClassId(): ClassId {
return when (this) {
is KotlinClassSnapshot -> classInfo.classId
is RegularJavaClassSnapshot -> classId
is ProtoBasedJavaClassSnapshot -> serializedJavaClass.classId
is EmptyJavaClassSnapshot, is ContentHashJavaClassSnapshot -> {
error("Unexpected type (it should have been handled earlier): ${javaClass.name}")
}
}
}
private object ImpactAnalysis {
/**
* Computes the set of classes/class members that are impacted by the given changes.
*
* For example, if a superclass has changed, any of its subclasses will be impacted even if it has not changed, and unchanged source
* files in the previous compilation that depended on the subclasses will need to be recompiled.
*
* The returned set is also a [ChangeSet], which includes the given changes plus the impacted ones.
*/
fun computeImpactedSet(changes: ChangeSet, previousClassSnapshots: List<ClassSnapshot>): ChangeSet {
val classIdToSubclasses = getClassIdToSubclassesMap(previousClassSnapshots)
return ChangeSet.Collector().run {
addChangedClasses(findSubclassesInclusive(changes.changedClasses, classIdToSubclasses))
for ((changedClass, changedClassMembers) in changes.changedClassMembers) {
findSubclassesInclusive(setOf(changedClass), classIdToSubclasses).forEach {
addChangedClassMembers(it, changedClassMembers)
}
}
for ((changedPackage, changedClassMembers) in changes.changedTopLevelMembers) {
addChangedTopLevelMembers(changedPackage, changedClassMembers)
}
getChanges()
}
}
private fun getClassIdToSubclassesMap(classSnapshots: List<ClassSnapshot>): Map<ClassId, Set<ClassId>> {
val classIds = classSnapshots.map { it.getClassId() }
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()
classSnapshot.getSupertypes(classNameToClassIdResolver).forEach { supertype ->
// No need to collect supertypes outside the considered class snapshots (e.g., "java/lang/Object")
if (supertype in classIds) {
classIdToSubclasses.computeIfAbsent(supertype) { mutableSetOf() }.add(classId)
}
}
}
return classIdToSubclasses
}
private fun ClassSnapshot.getSupertypes(classIdResolver: (JvmClassName) -> ClassId?): List<ClassId> {
return when (this) {
is RegularJavaClassSnapshot -> supertypes.mapNotNull {
// The following call returns null if supertype is outside the considered class snapshots (e.g., "java/lang/Object").
// Use `mapNotNull` as we don't need to collect those supertypes (see getClassIdToSubclassesMap).
classIdResolver.invoke(it)
}
is KotlinClassSnapshot -> supertypes.mapNotNull {
// Same as above
classIdResolver.invoke(it)
}
is ProtoBasedJavaClassSnapshot -> {
val (proto, nameResolver) = serializedJavaClass.toProtoData()
proto.supertypes(TypeTable(proto.typeTable)).map { nameResolver.getClassId(it.className) }
}
is EmptyJavaClassSnapshot, is ContentHashJavaClassSnapshot -> {
error("Unexpected type (it should have been handled earlier): ${javaClass.name}")
}
}
}
/**
* Finds direct and indirect subclasses of the given classes. The return set includes both the given classes and their direct and
* indirect subclasses.
*/
private fun findSubclassesInclusive(classIds: Set<ClassId>, classIdsToSubclasses: Map<ClassId, Set<ClassId>>): Set<ClassId> {
val visitedClasses = mutableSetOf<ClassId>()
val toVisitClasses = classIds.toMutableSet()
while (toVisitClasses.isNotEmpty()) {
val nextToVisit = mutableSetOf<ClassId>()
toVisitClasses.forEach {
nextToVisit.addAll(classIdsToSubclasses[it] ?: emptyList())
}
visitedClasses.addAll(toVisitClasses)
toVisitClasses.clear()
toVisitClasses.addAll(nextToVisit - visitedClasses)
}
return visitedClasses
}
}
@@ -0,0 +1,113 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.incremental.KotlinClassInfo
import org.jetbrains.kotlin.incremental.SerializedJavaClass
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
/** Snapshot of a classpath. It consists of a list of [ClasspathEntrySnapshot]s. */
class ClasspathSnapshot(val classpathEntrySnapshots: List<ClasspathEntrySnapshot>)
/**
* Snapshot of a classpath entry (directory or jar). It consists of a list of [ClassSnapshot]s.
*
* NOTE: It's important that the path to the classpath entry is not part of this snapshot. The reason is that classpath entries produced by
* different builds or on different machines but having the same contents should be considered the same for better build performance.
*/
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, ClassSnapshot>
)
/**
* Snapshot of a class. It contains minimal information about a class to compute the source files that need to be recompiled during an
* incremental run of the `KotlinCompile` task.
*
* It's important that this class contain only the minimal required information, as it will be part of the classpath snapshot of the
* `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
/** [ClassSnapshot] of a Kotlin class. */
class KotlinClassSnapshot(
val classInfo: KotlinClassInfo,
val supertypes: List<JvmClassName>
) : ClassSnapshot()
/** [ClassSnapshot] of a Java class. */
sealed class JavaClassSnapshot : ClassSnapshot()
/** [JavaClassSnapshot] of a typical Java class. */
class RegularJavaClassSnapshot(
/** [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>
) : JavaClassSnapshot() {
val className by lazy {
JvmClassName.byClassId(classId).also {
check(it == JvmClassName.byInternalName(classAbiExcludingMembers.name))
}
}
}
/** The ABI snapshot of a Java element (e.g., class, field, or method). */
open class AbiSnapshot(
/** The name of the Java element. It is part of the Java element's ABI. */
val name: String,
/** The hash of the Java element's ABI. */
val abiHash: Long
)
/** TEST-ONLY: An [AbiSnapshot] that is used for testing only and must not be used in production code. */
class AbiSnapshotForTests(
name: String,
abiHash: Long,
/** The Java element's ABI, captured in a [String]. */
@Suppress("unused") val abiValue: String
) : AbiSnapshot(name, abiHash)
/** [JavaClassSnapshot] of a typical Java class which uses protos internally. */
class ProtoBasedJavaClassSnapshot(val serializedJavaClass: SerializedJavaClass) : JavaClassSnapshot()
/**
* [JavaClassSnapshot] of a Java class where there is nothing to capture.
*
* For example, the snapshot of a local class is empty as a local class can't be referenced from other source files and therefore any
* changes in a local class will not cause recompilation of other source files.
*/
object EmptyJavaClassSnapshot : JavaClassSnapshot()
/**
* [JavaClassSnapshot] of a Java class where a proper snapshot can't be created for some reason, so we use the hash of the class contents as
* the snapshot instead, so that at least it's still correct when used as an input of the `KotlinCompile` task (when the class contents have
* changed, this snapshot will also change).
*/
class ContentHashJavaClassSnapshot(val contentHash: Long) : JavaClassSnapshot()
@@ -0,0 +1,214 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.incremental.classpathDiff
import com.intellij.util.io.DataExternalizer
import org.jetbrains.kotlin.incremental.JavaClassProtoMapValueExternalizer
import org.jetbrains.kotlin.incremental.KotlinClassInfo
import org.jetbrains.kotlin.incremental.storage.*
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
import java.io.*
/** Utility to serialize a [ClasspathSnapshot]. */
object ClasspathSnapshotSerializer {
fun load(classpathEntrySnapshotFiles: List<File>): ClasspathSnapshot {
return ClasspathSnapshot(classpathEntrySnapshotFiles.map {
ClasspathEntrySnapshotSerializer.load(it)
})
}
}
object ClasspathEntrySnapshotSerializer : DataSerializer<ClasspathEntrySnapshot> {
override fun save(output: DataOutput, snapshot: ClasspathEntrySnapshot) {
LinkedHashMapExternalizer(StringExternalizer, ClassSnapshotDataSerializer).save(output, snapshot.classSnapshots)
}
override fun read(input: DataInput): ClasspathEntrySnapshot {
return ClasspathEntrySnapshot(
classSnapshots = LinkedHashMapExternalizer(StringExternalizer, ClassSnapshotDataSerializer).read(input)
)
}
}
object ClassSnapshotDataSerializer : DataSerializer<ClassSnapshot> {
override fun save(output: DataOutput, snapshot: ClassSnapshot) {
output.writeBoolean(snapshot is KotlinClassSnapshot)
when (snapshot) {
is KotlinClassSnapshot -> KotlinClassSnapshotExternalizer.save(output, snapshot)
is JavaClassSnapshot -> JavaClassSnapshotExternalizer.save(output, snapshot)
}
}
override fun read(input: DataInput): ClassSnapshot {
val isKotlinClassSnapshot = input.readBoolean()
return if (isKotlinClassSnapshot) {
KotlinClassSnapshotExternalizer.read(input)
} else {
JavaClassSnapshotExternalizer.read(input)
}
}
}
object KotlinClassSnapshotExternalizer : DataExternalizer<KotlinClassSnapshot> {
override fun save(output: DataOutput, snapshot: KotlinClassSnapshot) {
KotlinClassInfoExternalizer.save(output, snapshot.classInfo)
ListExternalizer(JvmClassNameExternalizer).save(output, snapshot.supertypes)
}
override fun read(input: DataInput): KotlinClassSnapshot {
return KotlinClassSnapshot(
classInfo = KotlinClassInfoExternalizer.read(input),
supertypes = ListExternalizer(JvmClassNameExternalizer).read(input)
)
}
}
object KotlinClassInfoExternalizer : DataExternalizer<KotlinClassInfo> {
override fun save(output: DataOutput, info: KotlinClassInfo) {
ClassIdExternalizer.save(output, info.classId)
output.writeInt(info.classKind.id)
ListExternalizer(StringExternalizer).save(output, info.classHeaderData.toList())
ListExternalizer(StringExternalizer).save(output, info.classHeaderStrings.toList())
NullableValueExternalizer(StringExternalizer).save(output, info.multifileClassName)
LinkedHashMapExternalizer(StringExternalizer, ConstantExternalizer).save(output, info.constantsMap)
LinkedHashMapExternalizer(StringExternalizer, LongExternalizer).save(output, info.inlineFunctionsMap)
}
override fun read(input: DataInput): KotlinClassInfo {
return KotlinClassInfo(
classId = ClassIdExternalizer.read(input),
classKind = KotlinClassHeader.Kind.getById(input.readInt()),
classHeaderData = ListExternalizer(StringExternalizer).read(input).toTypedArray(),
classHeaderStrings = ListExternalizer(StringExternalizer).read(input).toTypedArray(),
multifileClassName = NullableValueExternalizer(StringExternalizer).read(input),
constantsMap = LinkedHashMapExternalizer(StringExternalizer, ConstantExternalizer).read(input),
inlineFunctionsMap = LinkedHashMapExternalizer(StringExternalizer, LongExternalizer).read(input)
)
}
}
object JavaClassSnapshotExternalizer : DataExternalizer<JavaClassSnapshot> {
override fun save(output: DataOutput, snapshot: JavaClassSnapshot) {
output.writeString(snapshot.javaClass.name)
when (snapshot) {
is RegularJavaClassSnapshot -> RegularJavaClassSnapshotExternalizer.save(output, snapshot)
is ProtoBasedJavaClassSnapshot -> ProtoBasedJavaClassSnapshotExternalizer.save(output, snapshot)
is EmptyJavaClassSnapshot -> EmptyJavaClassSnapshotExternalizer.save(output, snapshot)
is ContentHashJavaClassSnapshot -> ContentHashJavaClassSnapshotExternalizer.save(output, snapshot)
}
}
override fun read(input: DataInput): JavaClassSnapshot {
return when (val className = input.readString()) {
RegularJavaClassSnapshot::class.java.name -> RegularJavaClassSnapshotExternalizer.read(input)
ProtoBasedJavaClassSnapshot::class.java.name -> ProtoBasedJavaClassSnapshotExternalizer.read(input)
EmptyJavaClassSnapshot::class.java.name -> EmptyJavaClassSnapshotExternalizer.read(input)
ContentHashJavaClassSnapshot::class.java.name -> ContentHashJavaClassSnapshotExternalizer.read(input)
else -> error("Unrecognized class name: $className")
}
}
}
object RegularJavaClassSnapshotExternalizer : DataExternalizer<RegularJavaClassSnapshot> {
override fun save(output: DataOutput, snapshot: RegularJavaClassSnapshot) {
ClassIdExternalizer.save(output, snapshot.classId)
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): RegularJavaClassSnapshot {
return RegularJavaClassSnapshot(
classId = ClassIdExternalizer.read(input),
supertypes = ListExternalizer(JvmClassNameExternalizer).read(input),
classAbiExcludingMembers = AbiSnapshotExternalizer.read(input),
fieldsAbi = ListExternalizer(AbiSnapshotExternalizer).read(input),
methodsAbi = ListExternalizer(AbiSnapshotExternalizer).read(input)
)
}
}
object AbiSnapshotExternalizer : DataExternalizer<AbiSnapshot> {
override fun save(output: DataOutput, value: AbiSnapshot) {
output.writeString(value.name)
LongExternalizer.save(output, value.abiHash)
}
override fun read(input: DataInput): AbiSnapshot {
return AbiSnapshot(name = input.readString(), abiHash = LongExternalizer.read(input))
}
}
object ProtoBasedJavaClassSnapshotExternalizer : DataExternalizer<ProtoBasedJavaClassSnapshot> {
override fun save(output: DataOutput, snapshot: ProtoBasedJavaClassSnapshot) {
JavaClassProtoMapValueExternalizer.save(output, snapshot.serializedJavaClass)
}
override fun read(input: DataInput): ProtoBasedJavaClassSnapshot {
return ProtoBasedJavaClassSnapshot(serializedJavaClass = JavaClassProtoMapValueExternalizer.read(input))
}
}
object EmptyJavaClassSnapshotExternalizer : DataExternalizer<EmptyJavaClassSnapshot> {
override fun save(output: DataOutput, snapshot: EmptyJavaClassSnapshot) {
// Nothing to save
}
override fun read(input: DataInput): EmptyJavaClassSnapshot {
return EmptyJavaClassSnapshot
}
}
object ContentHashJavaClassSnapshotExternalizer : DataExternalizer<ContentHashJavaClassSnapshot> {
override fun save(output: DataOutput, snapshot: ContentHashJavaClassSnapshot) {
LongExternalizer.save(output, snapshot.contentHash)
}
override fun read(input: DataInput): ContentHashJavaClassSnapshot {
return ContentHashJavaClassSnapshot(contentHash = LongExternalizer.read(input))
}
}
interface DataSerializer<T> : DataExternalizer<T> {
fun save(file: File, value: T) {
return DataOutputStream(FileOutputStream(file).buffered()).use {
save(it, value)
}
}
fun load(file: File): T {
return DataInputStream(FileInputStream(file).buffered()).use {
read(it)
}
}
fun toByteArray(value: T): ByteArray {
val byteArrayOutputStream = ByteArrayOutputStream()
DataOutputStream(byteArrayOutputStream.buffered()).use {
save(it, value)
}
return byteArrayOutputStream.toByteArray()
}
fun fromByteArray(byteArray: ByteArray): T {
return DataInputStream(ByteArrayInputStream(byteArray).buffered()).use {
read(it)
}
}
}
@@ -0,0 +1,357 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.incremental.*
import org.jetbrains.kotlin.load.java.descriptors.JavaClassDescriptor
import org.jetbrains.kotlin.metadata.deserialization.TypeTable
import org.jetbrains.kotlin.metadata.deserialization.supertypes
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.kotlin.serialization.deserialization.getClassId
import org.jetbrains.kotlin.utils.DFS
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.ClassVisitor
import org.jetbrains.org.objectweb.asm.Opcodes
import java.io.File
import java.util.zip.ZipInputStream
/** Computes a [ClasspathEntrySnapshot] of a classpath entry (directory or jar). */
object ClasspathEntrySnapshotter {
private val DEFAULT_CLASS_FILTER = { unixStyleRelativePath: String, isDirectory: Boolean ->
!isDirectory
&& unixStyleRelativePath.endsWith(".class", ignoreCase = true)
&& !unixStyleRelativePath.endsWith("module-info.class", ignoreCase = true)
&& !unixStyleRelativePath.startsWith("meta-inf", ignoreCase = true)
}
fun snapshot(classpathEntry: File, protoBased: Boolean? = null): ClasspathEntrySnapshot {
val classes =
DirectoryOrJarContentsReader.read(classpathEntry, DEFAULT_CLASS_FILTER)
.map { (unixStyleRelativePath, contents) ->
ClassFileWithContents(ClassFile(classpathEntry, unixStyleRelativePath), contents)
}
val snapshots = try {
ClassSnapshotter.snapshot(classes, protoBased)
} catch (e: Throwable) {
if (isKnownProblematicClasspathEntry(classpathEntry)) {
classes.map { ContentHashJavaClassSnapshot(it.contents.md5()) }
} else throw e
}
val relativePathsToSnapshotsMap = classes.map { it.classFile.unixStyleRelativePath }.zipToMap(snapshots)
return ClasspathEntrySnapshot(relativePathsToSnapshotsMap)
}
/** Returns `true` if it is known that the snapshot of the given classpath entry can't be created for some reason. */
private fun isKnownProblematicClasspathEntry(classpathEntry: File): Boolean {
if (classpathEntry.name.startsWith("tools-jar-api")) {
// [FAULTY JAR] kotlin/dependencies/tools-jar-api/build/libs/tools-jar-api-1.6.255-SNAPSHOT.jar contains class
// com/sun/tools/javac/comp/Infer$GraphStrategy$NodeNotFoundException, but doesn't contain its outer class
// com/sun/tools/javac/comp/Infer$GraphStrategy.
// This happens with a few other similar classes in this jar.
// Therefore, this is a faulty jar, and our snapshotting logic cannot process it.
return true
}
if (classpathEntry.name.startsWith("platform-impl")) {
// ~/.gradle/kotlin-build-dependencies/repo/kotlin.build/ideaIC/203.8084.24/artifacts/lib/platform-impl.jar contains class
// com/intellij/application/options/codeStyle/OptionTableWithPreviewPanel.IntOption. When processing that class,
// BinaryJavaAnnotation$Companion.computeTargetType$resolution_common_jvm in Annotations.kt requires the targetType to be
// JavaClassifierType, but the actual type is PlainJavaPrimitiveType.
// TODO: It's likely that this requirement is incorrect, but let's fix it later.
return true
}
return false
}
}
/** Creates [ClassSnapshot]s of classes. */
object ClassSnapshotter {
/**
* Creates [ClassSnapshot]s of the given classes.
*
* Note that for Java (non-Kotlin) classes, creating a [ClassSnapshot] for a nested class will require accessing the outer class (and
* possibly vice versa). Therefore, outer classes and nested classes must be passed together in one invocation of this method.
*/
fun snapshot(
classes: List<ClassFileWithContents>,
protoBased: Boolean? = null,
includeDebugInfoInSnapshot: Boolean? = null
): List<ClassSnapshot> {
// Snapshot Kotlin classes first
val kotlinClassSnapshots: Map<ClassFileWithContents, KotlinClassSnapshot?> = classes.associateWith {
trySnapshotKotlinClass(it)
}
// Snapshot the remaining Java classes in one invocation
val javaClasses: List<ClassFileWithContents> = classes.filter { kotlinClassSnapshots[it] == null }
val snapshots: List<JavaClassSnapshot> = snapshotJavaClasses(javaClasses, protoBased, includeDebugInfoInSnapshot)
val javaClassSnapshots: Map<ClassFileWithContents, JavaClassSnapshot> = javaClasses.zipToMap(snapshots)
return classes.map { kotlinClassSnapshots[it] ?: javaClassSnapshots[it]!! }
}
/** Creates [KotlinClassSnapshot] of the given class, or returns `null` if the class is not a Kotlin class. */
private fun trySnapshotKotlinClass(clazz: ClassFileWithContents): KotlinClassSnapshot? {
return KotlinClassInfo.tryCreateFrom(clazz.contents)?.let {
KotlinClassSnapshot(it, computeSupertypes(it, clazz.contents))
}
}
// TODO: Find a faster way to get supertypes without loading protos (e.g., attach to an existing ASM visitor)
private fun computeSupertypes(classInfo: KotlinClassInfo, classContents: ByteArray): List<JvmClassName> {
return try {
val (nameResolver, proto) = JvmProtoBufUtil.readClassDataFrom(classInfo.classHeaderData, classInfo.classHeaderStrings)
val supertypeClassIds = proto.supertypes(TypeTable(proto.typeTable)).map { nameResolver.getClassId(it.className) }
supertypeClassIds.map { JvmClassName.byClassId(it) }
} catch (e: Exception) {
// The above method call currently fails on a few classes for some reason:
// - org.jetbrains.kotlin.protobuf.InvalidProtocolBufferException: Message was missing required fields.
// (Lite runtime could not determine which fields were missing) for SomeClassKt.class
// - java.lang.NullPointerException: parseDelimitedFrom(this, EXTENSION_REGISTRY) must not be null for
// kotlin-stdlib-1.6.255-SNAPSHOT.jar
// Fall back to ASM visitor to get the supertypes.
val supertypeClassNames = mutableListOf<String>()
ClassReader(classContents).accept(object : ClassVisitor(Opcodes.API_VERSION) {
override fun visit(
version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array<String>?
) {
superName?.let { supertypeClassNames.add(it) }
interfaces?.let { supertypeClassNames.addAll(it) }
}
}, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG)
supertypeClassNames.map { JvmClassName.byInternalName(it) }
}
}
/**
* Creates [JavaClassSnapshot]s of the given Java classes.
*
* Note that creating a [JavaClassSnapshot] for a nested class will require accessing the outer class (and possibly vice versa).
* Therefore, outer classes and nested classes must be passed together in one invocation of this method.
*/
private fun snapshotJavaClasses(
classes: List<ClassFileWithContents>,
protoBased: Boolean? = null,
includeDebugInfoInSnapshot: Boolean? = null
): List<JavaClassSnapshot> {
val classNames: List<JavaClassName> = classes.map { JavaClassName.compute(it.contents) }
val classNameToClassFile: Map<JavaClassName, ClassFileWithContents> = classNames.zipToMap(classes)
// We divide classes into 2 categories:
// - Special classes, which includes local, anonymous, or synthetic classes, and their nested classes. These classes can't be
// referenced from other source files, so any changes in these classes will not cause recompilation of other source files.
// Therefore, the snapshots of these classes are empty.
// - Regular classes: Any classes that do not belong to the above category.
val specialClasses = getSpecialClasses(classNames).toSet()
// Snapshot special classes first
val specialClassSnapshots: Map<JavaClassName, JavaClassSnapshot?> = classNames.associateWith {
if (it in specialClasses) {
EmptyJavaClassSnapshot
} else null
}
// Snapshot the remaining regular classes
val regularClasses: List<JavaClassName> = classNames.filter { specialClassSnapshots[it] == null }
val regularClassIds = computeJavaClassIds(regularClasses)
val regularClassFiles: List<ClassFileWithContents> = regularClasses.map { classNameToClassFile[it]!! }
val snapshots: List<JavaClassSnapshot> = if (protoBased ?: protoBasedDefaultValue) {
snapshotJavaClassesProtoBased(regularClassIds, regularClassFiles)
} else {
regularClassIds.mapIndexed { index, classId ->
JavaClassSnapshotter.snapshot(classId, regularClassFiles[index].contents, includeDebugInfoInSnapshot)
}
}
val regularClassSnapshots: Map<JavaClassName, JavaClassSnapshot> = regularClasses.zipToMap(snapshots)
return classNames.map { specialClassSnapshots[it] ?: regularClassSnapshots[it]!! }
}
private fun snapshotJavaClassesProtoBased(
classIds: List<ClassId>,
classFilesWithContents: List<ClassFileWithContents>
): List<JavaClassSnapshot> {
val classesContents = classFilesWithContents.map { it.contents }
val descriptors: List<JavaClassDescriptor?> = JavaClassDescriptorCreator.create(classIds, classesContents)
val snapshots: List<JavaClassSnapshot> = descriptors.mapIndexed { index, descriptor ->
val classFileWithContents = classFilesWithContents[index]
if (descriptor != null) {
try {
ProtoBasedJavaClassSnapshot(descriptor.toSerializedJavaClass())
} catch (e: Throwable) {
if (isKnownExceptionWhenReadingDescriptor(e)) {
ContentHashJavaClassSnapshot(classFileWithContents.contents.md5())
} else throw e
}
} else {
if (isKnownProblematicClass(classFileWithContents.classFile)) {
ContentHashJavaClassSnapshot(classFileWithContents.contents.md5())
} else {
error(
"Failed to create JavaClassDescriptor for class '${classFileWithContents.classFile.unixStyleRelativePath}'" +
" in '${classFileWithContents.classFile.classRoot.path}'"
)
}
}
}
return snapshots
}
/** Returns local, anonymous, or synthetic classes, and their nested classes. */
private fun getSpecialClasses(classNames: List<JavaClassName>): List<JavaClassName> {
val specialClasses: MutableMap<JavaClassName, Boolean> = HashMap(classNames.size)
val nameToClassName: Map<String, JavaClassName> = classNames.associateBy { it.name }
fun JavaClassName.isSpecial(): Boolean {
specialClasses[this]?.let { return it }
return if (isAnonymous || isSynthetic) {
true
} else when (this) {
is TopLevelClass -> false
is NestedNonLocalClass -> {
nameToClassName[outerName]?.isSpecial() ?: error("Can't find outer class '$outerName' of class '$name'")
}
is LocalClass -> true
}.also {
specialClasses[this] = it
}
}
return classNames.filter { it.isSpecial() }
}
/** Returns `true` if it is known that the given exception can be thrown when calling [JavaClassDescriptor.toSerializedJavaClass]. */
private fun isKnownExceptionWhenReadingDescriptor(throwable: Throwable): Boolean {
// When building the Kotlin repo with `./gradlew publish -Pbootstrap.local=true
// -Pbootstrap.local.path=/path/to/kotlin/build/repo -Pkotlin.incremental.useClasspathSnapshot=true`, the build can fail with:
// org.gradle.api.internal.artifacts.transform.TransformException: Execution failed for ClasspathEntrySnapshotTransform: ~/.gradle/wrapper/dists/gradle-6.9-bin/2ecsmyp3bolyybemj56vfn4mt/gradle-6.9/lib/kotlin-reflect-1.4.20.jar
// Caused by: java.lang.IncompatibleClassChangeError: Expected static method 'java.lang.Object org.jetbrains.kotlin.utils.DFS.dfsFromNode(java.lang.Object, org.jetbrains.kotlin.utils.DFS$Neighbors, org.jetbrains.kotlin.utils.DFS$Visited, org.jetbrains.kotlin.utils.DFS$NodeHandler)'
// at org.jetbrains.kotlin.builtins.FunctionTypesKt.isTypeOrSubtypeOf(functionTypes.kt:31)
// (... at JavaClassDescriptor.toSerializedJavaClass)
// The reason is that:
// - org/jetbrains/kotlin/builtins/FunctionTypesKt.class is located in ~/.gradle/caches/jars-8/66425fb82fd14126e9aa07dcd3100b42/kotlin-compiler-embeddable-1.6.255-20210909.213620-55.jar
// - org/jetbrains/kotlin/utils/DFS.class is located in ~/.gradle/caches/jars-8/c716e2e2d26b16f6f1462e59ba44cf3b/buildSrc.jar
// And somehow the two classes are incompatible (probably similar to the NoSuchMethodError documented at JavaClassDescriptorCreatorKt.createBinaryJavaClass).
// This happens to a few other jars inside gradle-6.9/lib.
// However, outside the Kotlin repo build, we don't have this issue (org/jetbrains/kotlin/builtins/FunctionTypesKt.class and
// org/jetbrains/kotlin/utils/DFS.class will be located in the same kotlin-compiler-embeddable.jar).
// Therefore, we special-case the Kotlin repo build below.
// TODO: See how we can address this issue.
return (throwable is IncompatibleClassChangeError &&
DFS::class.java.classLoader.getResource(DFS::class.java.name.replace('.', '/') + ".class")
?.path?.contains("buildSrc.jar") == true
)
}
/** Returns `true` if it is known that the snapshot of the given class can't be created for some reason. */
private fun isKnownProblematicClass(classFile: ClassFile): Boolean {
if (classFile.classRoot.name.startsWith("groovy")
&& classFile.unixStyleRelativePath.endsWith("\$CollectorHelper.class")
) {
// [FAULTY JAR] In groovy-all-1.3-2.5.12.jar and groovy-2.5.11.jar, the bytecode of
// groovy/cli/OptionField\$CollectorHelper.class indicates that its outer class is groovy/cli/OptionField, but the bytecode of
// groovy/cli/OptionField.class does not list any nested classes.
// This happens with a few other CollectorHelper classes in these jars.
// Therefore, these are faulty jars, and our snapshotting logic cannot process it.
return true
}
if (classFile.classRoot.name.startsWith("gradle-api")
&& classFile.unixStyleRelativePath.startsWith("org/gradle/internal/impldep/META-INF/versions")
) {
// [FAULTY JAR] gradle-api-6.9.jar has the following entries:
// - org/gradle/internal/impldep/org/junit/platform/commons/util/ModuleUtils.class
// - org/gradle/internal/impldep/META-INF/versions/9/org/junit/platform/commons/util/ModulesUtils.class
// - org/gradle/internal/impldep/META-INF/versions/9/org/junit/platform/commons/util/ModuleUtils$ModuleReferenceScanner.class
// The META-INF directories are located not at the top level (which is not expected), and those directories escaped our filter
// which filters out top-level META-INF directories. We then failed to snapshot ModuleUtils$ModuleReferenceScanner.class as
// there are 2 versions of ModuleUtils.class, and the one outside the META-INF directory doesn't have any nested classes.
// Therefore, this is a faulty jar, and our snapshotting logic cannot process it.
return true
}
return false
}
}
/** Utility to read the contents of a directory or jar. */
private object DirectoryOrJarContentsReader {
/**
* Returns a map from Unix-style relative paths of entries to their contents. The paths are relative to the given container (directory
* or jar).
*
* The map entries need to satisfy the given filter.
*
* The map entries are sorted based on their Unix-style relative paths (to ensure deterministic results across filesystems).
*
* Note: If a jar has duplicate entries after filtering, only the first one is retained.
*/
fun read(
directoryOrJar: File,
entryFilter: ((unixStyleRelativePath: String, isDirectory: Boolean) -> Boolean)? = null
): LinkedHashMap<String, ByteArray> {
return if (directoryOrJar.isDirectory) {
readDirectory(directoryOrJar, entryFilter)
} else {
check(directoryOrJar.isFile && directoryOrJar.path.endsWith(".jar", ignoreCase = true))
readJar(directoryOrJar, entryFilter)
}
}
private fun readDirectory(
directory: File,
entryFilter: ((unixStyleRelativePath: String, isDirectory: Boolean) -> Boolean)? = null
): LinkedHashMap<String, ByteArray> {
val relativePathsToContents = mutableMapOf<String, ByteArray>()
directory.walk().forEach { file ->
val unixStyleRelativePath = file.relativeTo(directory).invariantSeparatorsPath
if (entryFilter == null || entryFilter(unixStyleRelativePath, file.isDirectory)) {
relativePathsToContents[unixStyleRelativePath] = file.readBytes()
}
}
return relativePathsToContents.toSortedMap().toMap(LinkedHashMap())
}
private fun readJar(
jarFile: File,
entryFilter: ((unixStyleRelativePath: String, isDirectory: Boolean) -> Boolean)? = null
): LinkedHashMap<String, ByteArray> {
val relativePathsToContents = mutableMapOf<String, ByteArray>()
ZipInputStream(jarFile.inputStream().buffered()).use { zipInputStream ->
while (true) {
val entry = zipInputStream.nextEntry ?: break
val unixStyleRelativePath = entry.name
if (entryFilter == null || entryFilter(unixStyleRelativePath, entry.isDirectory)) {
relativePathsToContents.computeIfAbsent(unixStyleRelativePath) { zipInputStream.readBytes() }
}
}
}
return relativePathsToContents.toSortedMap().toMap(LinkedHashMap())
}
}
/**
* Combines two lists of the same size into a map.
*
* This method is more efficient than calling `[Iterable.zip].toMap()` as it doesn't create short-lived intermediate [Pair]s as done by
* [Iterable.zip].
*/
private fun <K, V> List<K>.zipToMap(other: List<V>): LinkedHashMap<K, V> {
check(this.size == other.size)
val map = LinkedHashMap<K, V>(size)
indices.forEach { index ->
map[this[index]] = other[index]
}
return map
}
private const val protoBasedDefaultValue = false
@@ -0,0 +1,86 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.kotlin.resolve.sam.SAM_LOOKUP_NAME
/** Computes [ChangeSet] between two lists of [JavaClassSnapshot]s .*/
object JavaClassChangesComputer {
/**
* Computes [ChangeSet] between two lists of [JavaClassSnapshot]s.
*
* Each list must not contain duplicate classes (having the same [JvmClassName]/[ClassId]).
*/
fun compute(
currentJavaClassSnapshots: List<RegularJavaClassSnapshot>,
previousJavaClassSnapshots: List<RegularJavaClassSnapshot>
): ChangeSet {
val currentClasses: Map<ClassId, RegularJavaClassSnapshot> = currentJavaClassSnapshots.associateBy { it.classId }
val previousClasses: Map<ClassId, RegularJavaClassSnapshot> = previousJavaClassSnapshots.associateBy { it.classId }
// No need to collect added classes as they don't impact recompilation
val removedClasses = previousClasses.keys - currentClasses.keys
val unchangedOrModifiedClasses = previousClasses.keys - removedClasses
return ChangeSet.Collector().run {
addChangedClasses(removedClasses)
unchangedOrModifiedClasses.forEach {
collectClassChanges(currentClasses[it]!!, previousClasses[it]!!, this)
}
getChanges()
}
}
/**
* Collects changes between two [JavaClassSnapshot]s.
*
* The two classes must have the same [ClassId].
*/
private fun collectClassChanges(
currentClassSnapshot: RegularJavaClassSnapshot,
previousClassSnapshot: RegularJavaClassSnapshot,
changes: ChangeSet.Collector
) {
val classId = currentClassSnapshot.classId.also { check(it == previousClassSnapshot.classId) }
if (currentClassSnapshot.classAbiExcludingMembers.abiHash != previousClassSnapshot.classAbiExcludingMembers.abiHash) {
changes.addChangedClass(classId)
} else {
collectClassMemberChanges(classId, currentClassSnapshot.fieldsAbi, previousClassSnapshot.fieldsAbi, changes)
collectClassMemberChanges(classId, currentClassSnapshot.methodsAbi, previousClassSnapshot.methodsAbi, changes)
}
}
/** Collects changes between two lists of fields/methods within a class. */
private fun collectClassMemberChanges(
classId: ClassId,
currentMemberSnapshots: List<AbiSnapshot>,
previousMemberSnapshots: List<AbiSnapshot>,
changes: ChangeSet.Collector
) {
val currentMemberHashes: Set<Long> = currentMemberSnapshots.map { it.abiHash }.toSet()
val previousMemberHashes: Map<Long, AbiSnapshot> = previousMemberSnapshots.associateBy { it.abiHash }
val addedMembers = currentMemberHashes - previousMemberHashes.keys
val removedMembers = previousMemberHashes.keys - currentMemberHashes
// Note:
// - No need to collect added members as they don't impact recompilation.
// - Modified members have a current version and a previous version. The current version will appear in addedMembers (which will
// not be collected), and the previous version will appear in removedMembers (which will be collected).
// - Multiple members may have the same name (but never the same signature (name + desc) or ABI hash). It's okay to report the
// same name multiple times.
changes.addChangedClassMembers(classId, removedMembers.map { previousMemberHashes[it]!!.name })
// TODO: Check whether the condition to add SAM_LOOKUP_NAME below is too broad, and correct it if necessary.
// Currently, it matches the logic in ChangesCollector.getDirtyData in buildUtil.kt.
if (addedMembers.isNotEmpty() || removedMembers.isNotEmpty()) {
changes.addChangedClassMember(classId, SAM_LOOKUP_NAME.asString())
}
}
}
@@ -0,0 +1,87 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.incremental.classpathDiff
import com.google.gson.GsonBuilder
import org.jetbrains.kotlin.incremental.md5
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.tree.ClassNode
/** Computes a [JavaClassSnapshot] of a Java class. */
object JavaClassSnapshotter {
fun snapshot(classId: ClassId, classContents: ByteArray, includeDebugInfoInSnapshot: Boolean? = null): 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:
// 1. Collect ABI info directly. The collected info must be exhaustive (now and in the future when there are updates to Java/ASM).
// 2. Collect all info and remove non-ABI info. The removed info should be exhaustive, but even if it's not, it is still
// acceptable.
// In the following, we will use the second approach as it is safer.
val abiClass = ClassNode()
// First, collect all info.
// Note the parsing options passed to ClassReader:
// - SKIP_CODE is set as method bodies will not be part of the ABI of the class.
// - SKIP_DEBUG is not set as it would skip method parameters, which may be used by annotation processors like Room.
// - SKIP_FRAMES and EXPAND_FRAMES are not relevant when SKIP_CODE is set.
val classReader = ClassReader(classContents)
classReader.accept(abiClass, ClassReader.SKIP_CODE)
// Then, remove non-ABI info:
// - Method bodies have already been removed (see SKIP_CODE above).
// - If the class is private, its snapshot will be empty. Otherwise, remove its private fields and methods.
if (abiClass.access.isPrivate()) {
return EmptyJavaClassSnapshot
}
abiClass.fields.removeIf { it.access.isPrivate() }
abiClass.methods.removeIf { it.access.isPrivate() }
// Sort fields and methods as their order is not important (we still use List instead of Set as we want the serialized snapshot to
// be deterministic).
abiClass.fields.sortWith(compareBy({ it.name }, { it.desc }))
abiClass.methods.sortWith(compareBy({ it.name }, { it.desc }))
val supertypes = (listOf(abiClass.superName) + abiClass.interfaces.toList()).map { JvmClassName.byInternalName(it) }
val fieldsAbi = abiClass.fields.map { snapshotJavaElement(it, it.name, includeDebugInfoInSnapshot) }
val methodsAbi = abiClass.methods.map { snapshotJavaElement(it, it.name, includeDebugInfoInSnapshot) }
abiClass.fields.clear()
abiClass.methods.clear()
val classAbiExcludingMembers = abiClass.let { snapshotJavaElement(it, it.name, includeDebugInfoInSnapshot) }
return RegularJavaClassSnapshot(classId, supertypes, classAbiExcludingMembers, fieldsAbi, methodsAbi)
}
private fun Int.isPrivate() = (this and Opcodes.ACC_PRIVATE) != 0
private val gson by lazy {
// Use serializeSpecialFloatingPointValues() to avoid
// "java.lang.IllegalArgumentException: NaN is not a valid double value as per JSON specification. To override this behavior, use
// GsonBuilder.serializeSpecialFloatingPointValues() method."
// on jars such as ~/.gradle/kotlin-build-dependencies/repo/kotlin.build/ideaIC/203.8084.24/artifacts/lib/rhino-1.7.12.jar.
GsonBuilder()
.serializeSpecialFloatingPointValues()
.setPrettyPrinting()
.create()
}
private fun snapshotJavaElement(javaElement: Any, javaElementName: String, includeDebugInfoInSnapshot: Boolean? = null): AbiSnapshot {
// TODO: Optimize this method later if necessary. Currently we focus on correctness first.
val abiValue = gson.toJson(javaElement)
val abiHash = abiValue.toByteArray().md5()
return if (includeDebugInfoInSnapshot == true) {
AbiSnapshotForTests(javaElementName, abiHash, abiValue)
} else {
AbiSnapshot(javaElementName, abiHash)
}
}
}
@@ -0,0 +1,337 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.compileAll
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.snapshot
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.snapshotAll
import org.jetbrains.kotlin.incremental.ClasspathChanges
import org.jetbrains.kotlin.incremental.LookupSymbol
import org.jetbrains.kotlin.resolve.sam.SAM_LOOKUP_NAME
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.io.File
import kotlin.test.fail
abstract class ClasspathChangesComputerTest : ClasspathSnapshotTestCommon() {
// TODO Add more test cases:
// - private/non-private fields
// - inline functions
// - changing supertype by adding somethings that changes/does not change the supertype ABI
// - adding an annotation
@Test
abstract fun testAbiChange_changePublicMethodSignature()
@Test
abstract fun testNonAbiChange_changeMethodImplementation()
@Test
abstract fun testVariousAbiChanges()
@Test
abstract fun testImpactAnalysis()
}
class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
@Test
override fun testAbiChange_changePublicMethodSignature() {
val sourceFile = SimpleKotlinClass(tmpDir)
val previousSnapshot = sourceFile.compileAndSnapshot()
val currentSnapshot = sourceFile.changePublicMethodSignature().compileAndSnapshot()
val changes = ClasspathChangesComputer.computeClassChanges(listOf(currentSnapshot), listOf(previousSnapshot)).normalize()
Changes(
lookupSymbols = setOf(
LookupSymbol(name = "publicFunction", scope = "com.example.SimpleKotlinClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.SimpleKotlinClass")
),
fqNames = setOf("com.example.SimpleKotlinClass")
).assertEquals(changes)
}
@Test
override fun testNonAbiChange_changeMethodImplementation() {
val sourceFile = SimpleKotlinClass(tmpDir)
val previousSnapshot = sourceFile.compileAndSnapshot()
val currentSnapshot = sourceFile.changeMethodImplementation().compileAndSnapshot()
val changes = ClasspathChangesComputer.computeClassChanges(listOf(currentSnapshot), listOf(previousSnapshot)).normalize()
Changes(emptySet(), emptySet()).assertEquals(changes)
}
@Test
override fun testVariousAbiChanges() {
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testVariousAbiChanges/src/kotlin").canonicalFile
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir)
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir)
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize()
Changes(
lookupSymbols = setOf(
// ModifiedClassUnchangedMembers
LookupSymbol(name = "ModifiedClassUnchangedMembers", scope = "com.example"),
// ModifiedClassChangedMembers
LookupSymbol(name = "modifiedProperty", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "addedProperty", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "removedProperty", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "modifiedFunction", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "addedFunction", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "removedFunction", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.ModifiedClassChangedMembers"),
// AddedClass
LookupSymbol(name = "AddedClass", scope = "com.example"),
// RemovedClass
LookupSymbol(name = "RemovedClass", scope = "com.example"),
// Top-level properties and functions
LookupSymbol(name = "modifiedTopLevelProperty", scope = "com.example"),
LookupSymbol(name = "addedTopLevelProperty", scope = "com.example"),
LookupSymbol(name = "removedTopLevelProperty", scope = "com.example"),
LookupSymbol(name = "movedTopLevelProperty", scope = "com.example"),
LookupSymbol(name = "modifiedTopLevelFunction", scope = "com.example"),
LookupSymbol(name = "addedTopLevelFunction", scope = "com.example"),
LookupSymbol(name = "removedTopLevelFunction", scope = "com.example"),
LookupSymbol(name = "movedTopLevelFunction", scope = "com.example"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example")
),
fqNames = setOf(
"com.example.ModifiedClassUnchangedMembers",
"com.example.ModifiedClassChangedMembers",
"com.example.AddedClass",
"com.example.RemovedClass",
"com.example"
)
).assertEquals(changes)
}
@Test
override fun testImpactAnalysis() {
val classpathSourceDir =
File(testDataDir, "../ClasspathChangesComputerTest/testImpactAnalysis_KotlinOrJava/src/kotlin").canonicalFile
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir)
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir)
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize()
Changes(
lookupSymbols = setOf(
LookupSymbol(name = "changedProperty", scope = "com.example.ChangedSuperClass"),
LookupSymbol(name = "changedProperty", scope = "com.example.SubClass"),
LookupSymbol(name = "changedProperty", scope = "com.example.SubSubClass"),
LookupSymbol(name = "changedFunction", scope = "com.example.ChangedSuperClass"),
LookupSymbol(name = "changedFunction", scope = "com.example.SubClass"),
LookupSymbol(name = "changedFunction", scope = "com.example.SubSubClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.ChangedSuperClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.SubClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.SubSubClass")
),
fqNames = setOf(
"com.example.ChangedSuperClass",
"com.example.SubClass",
"com.example.SubSubClass"
)
).assertEquals(changes)
}
}
@RunWith(Parameterized::class)
class JavaOnlyClasspathChangesComputerTest(private val protoBased: Boolean) : ClasspathChangesComputerTest() {
companion object {
@Parameterized.Parameters(name = "protoBased={0}")
@JvmStatic
fun parameters() = listOf(true, false)
}
@Test
override fun testAbiChange_changePublicMethodSignature() {
val sourceFile = SimpleJavaClass(tmpDir)
val previousSnapshot = sourceFile.compile().snapshot(protoBased)
val currentSnapshot = sourceFile.changePublicMethodSignature().compile().snapshot(protoBased)
val changes = ClasspathChangesComputer.computeClassChanges(listOf(currentSnapshot), listOf(previousSnapshot)).normalize()
Changes(
lookupSymbols = setOf(
LookupSymbol(name = "publicMethod", scope = "com.example.SimpleJavaClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.SimpleJavaClass")
),
fqNames = setOf("com.example.SimpleJavaClass")
).assertEquals(changes)
}
@Test
override fun testNonAbiChange_changeMethodImplementation() {
val sourceFile = SimpleJavaClass(tmpDir)
val previousSnapshot = sourceFile.compile().snapshot(protoBased)
val currentSnapshot = sourceFile.changeMethodImplementation().compile().snapshot(protoBased)
val changes = ClasspathChangesComputer.computeClassChanges(listOf(currentSnapshot), listOf(previousSnapshot)).normalize()
Changes(emptySet(), emptySet()).assertEquals(changes)
}
@Test
override fun testVariousAbiChanges() {
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testVariousAbiChanges/src/java").canonicalFile
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir, protoBased)
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir, protoBased)
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize()
Changes(
lookupSymbols = setOf(
// ModifiedClassUnchangedMembers
LookupSymbol(name = "ModifiedClassUnchangedMembers", scope = "com.example"),
// ModifiedClassChangedMembers
LookupSymbol(name = "modifiedField", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "removedField", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "modifiedMethod", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "removedMethod", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.ModifiedClassChangedMembers"),
// RemovedClass
LookupSymbol(name = "RemovedClass", scope = "com.example")
) + if (protoBased) {
setOf(
// ModifiedClassChangedMembers
LookupSymbol(name = "addedField", scope = "com.example.ModifiedClassChangedMembers"),
LookupSymbol(name = "addedMethod", scope = "com.example.ModifiedClassChangedMembers"),
// AddedClass
LookupSymbol(name = "AddedClass", scope = "com.example"),
)
} else {
emptySet()
},
fqNames = setOf(
"com.example.ModifiedClassUnchangedMembers",
"com.example.ModifiedClassChangedMembers",
"com.example.RemovedClass"
) + if (protoBased) {
setOf("com.example.AddedClass")
} else {
emptySet()
}
).assertEquals(changes)
}
@Test
override fun testImpactAnalysis() {
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testImpactAnalysis_KotlinOrJava/src/java").canonicalFile
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir, protoBased)
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir, protoBased)
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize()
Changes(
lookupSymbols = setOf(
LookupSymbol(name = "changedField", scope = "com.example.ChangedSuperClass"),
LookupSymbol(name = "changedField", scope = "com.example.SubClass"),
LookupSymbol(name = "changedField", scope = "com.example.SubSubClass"),
LookupSymbol(name = "changedMethod", scope = "com.example.ChangedSuperClass"),
LookupSymbol(name = "changedMethod", scope = "com.example.SubClass"),
LookupSymbol(name = "changedMethod", scope = "com.example.SubSubClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.ChangedSuperClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.SubClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.SubSubClass")
),
fqNames = setOf(
"com.example.ChangedSuperClass",
"com.example.SubClass",
"com.example.SubSubClass"
)
).assertEquals(changes)
}
}
class KotlinAndJavaClasspathChangesComputerTest : ClasspathSnapshotTestCommon() {
@Test
fun testImpactAnalysis() {
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testImpactAnalysis_KotlinAndJava/src").canonicalFile
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir)
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir)
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize()
Changes(
lookupSymbols = setOf(
LookupSymbol(name = "changedProperty", scope = "com.example.ChangedKotlinSuperClass"),
LookupSymbol(name = "changedProperty", scope = "com.example.KotlinSubClassOfKotlinSuperClass"),
LookupSymbol(name = "changedProperty", scope = "com.example.JavaSubClassOfKotlinSuperClass"),
LookupSymbol(name = "changedFunction", scope = "com.example.ChangedKotlinSuperClass"),
LookupSymbol(name = "changedFunction", scope = "com.example.KotlinSubClassOfKotlinSuperClass"),
LookupSymbol(name = "changedFunction", scope = "com.example.JavaSubClassOfKotlinSuperClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.ChangedKotlinSuperClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.KotlinSubClassOfKotlinSuperClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.JavaSubClassOfKotlinSuperClass"),
LookupSymbol(name = "changedField", scope = "com.example.ChangedJavaSuperClass"),
LookupSymbol(name = "changedField", scope = "com.example.KotlinSubClassOfJavaSuperClass"),
LookupSymbol(name = "changedField", scope = "com.example.JavaSubClassOfJavaSuperClass"),
LookupSymbol(name = "changedMethod", scope = "com.example.ChangedJavaSuperClass"),
LookupSymbol(name = "changedMethod", scope = "com.example.KotlinSubClassOfJavaSuperClass"),
LookupSymbol(name = "changedMethod", scope = "com.example.JavaSubClassOfJavaSuperClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.ChangedJavaSuperClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.KotlinSubClassOfJavaSuperClass"),
LookupSymbol(name = SAM_LOOKUP_NAME.asString(), scope = "com.example.JavaSubClassOfJavaSuperClass")
),
fqNames = setOf(
"com.example.ChangedKotlinSuperClass",
"com.example.KotlinSubClassOfKotlinSuperClass",
"com.example.JavaSubClassOfKotlinSuperClass",
"com.example.ChangedJavaSuperClass",
"com.example.KotlinSubClassOfJavaSuperClass",
"com.example.JavaSubClassOfJavaSuperClass"
)
).assertEquals(changes)
}
}
private fun snapshotClasspath(classpathSourceDir: File, tmpDir: TemporaryFolder, protoBased: Boolean = true): 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))
val relativePaths = classFiles.map { it.unixStyleRelativePath }
val classSnapshots = classFiles.snapshotAll(protoBased)
ClasspathEntrySnapshot(
classSnapshots = relativePaths.zip(classSnapshots).toMap(LinkedHashMap())
)
}
return ClasspathSnapshot(classpathEntrySnapshots)
}
/** Adapted version of [ClasspathChanges.Available] for readability in this test. */
private data class Changes(val lookupSymbols: Set<LookupSymbol>, val fqNames: Set<String>)
private fun ClasspathChanges.normalize(): Changes {
this as ClasspathChanges.Available
return Changes(lookupSymbols, fqNames.map { it.asString() }.toSet())
}
private fun Changes.assertEquals(actual: Changes) {
listOfNotNull(
compare(expected = this.lookupSymbols, actual = actual.lookupSymbols),
compare(expected = this.fqNames, actual = actual.fqNames)
).also {
if (it.isNotEmpty()) {
fail(it.joinToString("\n"))
}
}
}
private fun compare(expected: Set<*>, actual: Set<*>): String? {
return if (expected != actual) {
"Two sets differ:\n" +
"Elements in expected set but not in actual set: ${expected - actual}\n" +
"Elements in actual set but not in expected set: ${actual - expected}"
} else null
}
@@ -0,0 +1,44 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.readBytes
import org.junit.Assert.assertEquals
import org.junit.Test
abstract class ClasspathSnapshotSerializerTest : ClasspathSnapshotTestCommon() {
protected abstract val testSourceFile: ChangeableTestSourceFile
@Test
open fun `test ClassSnapshotDataSerializer`() {
val originalSnapshot = testSourceFile.compileAndSnapshot()
val serializedSnapshot = ClassSnapshotDataSerializer.toByteArray(originalSnapshot)
val deserializedSnapshot = ClassSnapshotDataSerializer.fromByteArray(serializedSnapshot)
assertEquals(originalSnapshot.toGson(), deserializedSnapshot.toGson())
}
}
class KotlinClassesClasspathSnapshotSerializerTest : ClasspathSnapshotSerializerTest() {
override val testSourceFile = SimpleKotlinClass(tmpDir)
}
class JavaClassesClasspathSnapshotSerializerTest : ClasspathSnapshotSerializerTest() {
override val testSourceFile = SimpleJavaClass(tmpDir)
@Test
override fun `test ClassSnapshotDataSerializer`() {
val originalSnapshot = testSourceFile.compile().let {
ClassSnapshotter.snapshot(listOf(ClassFileWithContents(it, it.readBytes())), includeDebugInfoInSnapshot = false)
}.single()
val serializedSnapshot = ClassSnapshotDataSerializer.toByteArray(originalSnapshot)
val deserializedSnapshot = ClassSnapshotDataSerializer.fromByteArray(serializedSnapshot)
assertEquals(originalSnapshot.toGson(), deserializedSnapshot.toGson())
}
}
@@ -0,0 +1,256 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.incremental.classpathDiff
import com.google.gson.GsonBuilder
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.Util.compile
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.compileAll
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.snapshot
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.snapshotAll
import org.jetbrains.kotlin.test.KotlinTestUtils
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import java.io.File
abstract class ClasspathSnapshotTestCommon {
companion object {
val testDataDir =
File("libraries/tools/kotlin-gradle-plugin/src/testData/org/jetbrains/kotlin/gradle/incremental/ClasspathSnapshotTestCommon")
}
@get:Rule
val tmpDir = TemporaryFolder()
// Use Gson to compare objects
private val gson by lazy { GsonBuilder().setPrettyPrinting().create() }
protected fun Any.toGson(): String = gson.toJson(this)
sealed class SourceFile(val baseDir: File, relativePath: String) {
val unixStyleRelativePath: String
init {
unixStyleRelativePath = relativePath.replace('\\', '/')
}
fun asFile() = File(baseDir, unixStyleRelativePath)
class KotlinSourceFile(baseDir: File, relativePath: String, val preCompiledClassFiles: List<ClassFile>) :
SourceFile(baseDir, relativePath) {
constructor(baseDir: File, relativePath: String, preCompiledClassFile: ClassFile) :
this(baseDir, relativePath, listOf(preCompiledClassFile))
}
class JavaSourceFile(baseDir: File, relativePath: String) : SourceFile(baseDir, relativePath)
}
/** Same as [SourceFile] but with a [TemporaryFolder] to store the results of operations on the [SourceFile]. */
open class TestSourceFile(val sourceFile: SourceFile, private val tmpDir: TemporaryFolder) {
fun replace(oldValue: String, newValue: String, preCompiledKotlinClassFile: ClassFile? = null): TestSourceFile {
val fileContents = sourceFile.asFile().readText()
check(fileContents.contains(oldValue)) { "String '$oldValue' not found in file '${sourceFile.asFile().path}'" }
val newSourceFile = when (sourceFile) {
is KotlinSourceFile -> KotlinSourceFile(tmpDir.newFolder(), sourceFile.unixStyleRelativePath, preCompiledKotlinClassFile!!)
is JavaSourceFile -> JavaSourceFile(tmpDir.newFolder(), sourceFile.unixStyleRelativePath)
}
newSourceFile.asFile().parentFile.mkdirs()
newSourceFile.asFile().writeText(fileContents.replace(oldValue, newValue))
return TestSourceFile(newSourceFile, tmpDir)
}
/**
* Compiles this source file and returns a single generated .class file, or fails if zero or more than one .class file was
* generated.
*
* Alternatively, the caller can call [compileAll] to get all generated .class files.
*/
fun compile(): ClassFile = compileAll().single()
/** Compiles this source file and returns all generated .class files. */
@Suppress("MemberVisibilityCanBePrivate")
fun compileAll(): List<ClassFile> = sourceFile.compile(tmpDir)
/**
* Compiles this source file and returns the snapshot of a single generated .class file, or fails if zero or more than one .class
* file was generated.
*
* Alternatively, the caller can call [compileAndSnapshotAll] to get the snapshots of all generated .class files.
*/
fun compileAndSnapshot() = compile().snapshot()
/** Compiles this source file and returns the snapshots of all generated .class files. */
fun compileAndSnapshotAll(): List<ClassSnapshot> = compileAll().snapshotAll()
}
object Util {
/** 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> {
val kotlinClasses = compileKotlin(srcDir, classpath, tmpDir)
val javaClasspath = classpath + listOfNotNull(kotlinClasses.firstOrNull()?.classRoot)
val javaClasses = compileJava(srcDir, javaClasspath, tmpDir)
return kotlinClasses + javaClasses
}
private fun compileKotlin(srcDir: File, classpath: List<File>, tmpDir: TemporaryFolder): List<ClassFile> {
val preCompiledKotlinClassesDir = srcDir.path.let {
File(it.substringBeforeLast("src") + "classes" + it.substringAfterLast("src"))
}
preCompileKotlinFilesIfNecessary(srcDir, preCompiledKotlinClassesDir, classpath, tmpDir)
return getClassFilesInDir(preCompiledKotlinClassesDir)
}
private val preCompiledKotlinClassesDirs = mutableSetOf<File>()
/**
* If <kotlin-repo>/dist/kotlinc/lib/kotlin-compiler.jar is available (e.g., by running ./gradlew dist), we will be able to call the
* Kotlin compiler to generate classes. However, kotlin-compiler.jar is currently not available in CI builds, so we need to
* pre-compile the classes locally and put them in the test data to check in.
*/
@Synchronized // To safe-guard shared variable preCompiledKotlinClassesDirs
private fun preCompileKotlinFilesIfNecessary(
srcDir: File,
preCompiledKotlinClassesDir: File,
classpath: List<File>,
tmpDir: TemporaryFolder,
preCompile: Boolean = false // Set to `true` to pre-compile Kotlin class files locally (DO NOT check in with preCompile = true)
) {
if (preCompile) {
if (!preCompiledKotlinClassesDirs.contains(preCompiledKotlinClassesDir)) {
val classFiles = doCompileKotlin(srcDir, classpath, tmpDir)
preCompiledKotlinClassesDir.deleteRecursively()
for (classFile in classFiles) {
File(preCompiledKotlinClassesDir, classFile.unixStyleRelativePath).apply {
parentFile.mkdirs()
classFile.asFile().copyTo(this)
}
}
preCompiledKotlinClassesDirs.add(preCompiledKotlinClassesDir)
}
}
}
fun SourceFile.compile(tmpDir: TemporaryFolder): List<ClassFile> {
return if (this is KotlinSourceFile) {
preCompiledClassFiles.forEach {
preCompileKotlinFilesIfNecessary(baseDir, it.classRoot, classpath = emptyList(), tmpDir)
}
preCompiledClassFiles
} else {
val srcDir = tmpDir.newFolder()
asFile().copyTo(File(srcDir, unixStyleRelativePath))
compileAll(srcDir, classpath = emptyList(), tmpDir)
}
}
private fun doCompileKotlin(srcDir: File, classpath: List<File>, tmpDir: TemporaryFolder): List<ClassFile> {
if (srcDir.walk().none { it.path.endsWith(".kt") }) {
return emptyList()
}
val classesDir = tmpDir.newFolder()
org.jetbrains.kotlin.test.MockLibraryUtil.compileKotlin(
srcDir.path,
classesDir,
extraClasspath = classpath.map { it.path }.toTypedArray()
)
return getClassFilesInDir(classesDir)
}
private fun compileJava(srcDir: File, classpath: List<File>, tmpDir: TemporaryFolder): List<ClassFile> {
val javaFiles = srcDir.walk().toList().filter { it.path.endsWith(".java") }
if (javaFiles.isEmpty()) {
return emptyList()
}
val classesDir = tmpDir.newFolder()
val classpathOption =
if (classpath.isNotEmpty()) listOf("-classpath", classpath.joinToString(File.pathSeparator)) else emptyList()
KotlinTestUtils.compileJavaFiles(javaFiles, listOf("-d", classesDir.path) + classpathOption)
return getClassFilesInDir(classesDir)
}
private fun getClassFilesInDir(classesDir: File): List<ClassFile> {
return classesDir.walk().toList()
.filter { it.isFile && it.path.endsWith(".class") }
.map { ClassFile(classesDir, it.toRelativeString(classesDir)) }
.sortedBy { it.unixStyleRelativePath.substringBefore(".class") }
}
// `ClassFile`s in production code could be in a jar, but the `ClassFile`s in tests are currently in a directory, so converting it
// to a File is possible.
@Suppress("MemberVisibilityCanBePrivate")
fun ClassFile.asFile() = File(classRoot, unixStyleRelativePath)
@Suppress("MemberVisibilityCanBePrivate")
fun ClassFile.readBytes() = asFile().readBytes()
fun ClassFile.snapshot(protoBased: Boolean? = null): ClassSnapshot = listOf(this).snapshotAll(protoBased).single()
fun List<ClassFile>.snapshotAll(protoBased: Boolean? = null): List<ClassSnapshot> {
val classFilesWithContents = this.map { ClassFileWithContents(it, it.readBytes()) }
return ClassSnapshotter.snapshot(classFilesWithContents, protoBased, includeDebugInfoInSnapshot = true)
}
}
abstract class ChangeableTestSourceFile(sourceFile: SourceFile, tmpDir: TemporaryFolder) : TestSourceFile(sourceFile, tmpDir) {
abstract fun changePublicMethodSignature(): TestSourceFile
abstract fun changeMethodImplementation(): TestSourceFile
}
class SimpleKotlinClass(tmpDir: TemporaryFolder) : ChangeableTestSourceFile(
KotlinSourceFile(
baseDir = File(testDataDir, "src/kotlin"), relativePath = "com/example/SimpleKotlinClass.kt",
preCompiledClassFile = ClassFile(File(testDataDir, "classes/kotlin/original"), "com/example/SimpleKotlinClass.class")
), tmpDir
) {
override fun changePublicMethodSignature() = replace(
"publicFunction()", "publicFunction(newParam: Int)",
preCompiledKotlinClassFile = ClassFile(
File(testDataDir, "classes/kotlin/changedPublicMethodSignature"), "com/example/SimpleKotlinClass.class"
)
)
override fun changeMethodImplementation() = replace(
"I'm in a public function", "This function's implementation has changed!",
preCompiledKotlinClassFile = ClassFile(
File(testDataDir, "classes/kotlin/changedMethodImplementation"), "com/example/SimpleKotlinClass.class"
)
)
}
class SimpleJavaClass(tmpDir: TemporaryFolder) : ChangeableTestSourceFile(
JavaSourceFile(File(testDataDir, "src/java"), "com/example/SimpleJavaClass.java"), tmpDir
) {
override fun changePublicMethodSignature() = replace("publicMethod()", "publicMethod(int newParam)")
override fun changeMethodImplementation() = replace("I'm in a public method", "This method's implementation has changed!")
}
class JavaClassWithNestedClasses(tmpDir: TemporaryFolder) : ChangeableTestSourceFile(
JavaSourceFile(File(testDataDir, "src/java"), "com/example/JavaClassWithNestedClasses.java"), tmpDir
) {
/** The source file contains multiple classes, select the one that we want to test. */
val nestedClassToTest = "com/example/JavaClassWithNestedClasses\$InnerClass"
override fun changePublicMethodSignature() = replace("publicMethod()", "publicMethod(int newParam)")
override fun changeMethodImplementation() = replace("I'm in a public method", "This method's implementation has changed!")
}
}
@@ -0,0 +1,145 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.snapshot
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.io.File
abstract class ClasspathSnapshotterTest : ClasspathSnapshotTestCommon() {
protected abstract val testSourceFile: ChangeableTestSourceFile
private lateinit var testClassSnapshot: ClassSnapshot
@Before
fun setUp() {
testClassSnapshot = testSourceFile.compileAndSnapshot()
}
@Test
fun `test ClassSnapshotter's result against expected snapshot`() {
assertEquals(getExpectedSnapshotFile().readText(), testClassSnapshot.toGson())
}
private fun getExpectedSnapshotFile() = testSourceFile.sourceFile.asFile().path.let {
File(it.substringBeforeLast("src") + "expected-snapshot" + it.substringAfterLast("src").substringBeforeLast('.') + ".json")
}
@Test
fun `test ClassSnapshotter extracts ABI info from a class`() {
// Change public method signature
val updatedSnapshot = testSourceFile.changePublicMethodSignature().compileAndSnapshot()
// The snapshot must change
assertNotEquals(testClassSnapshot.toGson(), updatedSnapshot.toGson())
}
@Test
fun `test ClassSnapshotter does not extract non-ABI info from a class`() {
// Change method implementation
val updatedSnapshot = testSourceFile.changeMethodImplementation().compileAndSnapshot()
// The snapshot must not change
assertEquals(testClassSnapshot.toGson(), updatedSnapshot.toGson())
}
}
class KotlinClassesClasspathSnapshotterTest : ClasspathSnapshotterTest() {
override val testSourceFile = SimpleKotlinClass(tmpDir)
}
@RunWith(Parameterized::class)
class JavaClassesClasspathSnapshotterTest(private val protoBased: Boolean) : ClasspathSnapshotTestCommon() {
companion object {
@Parameterized.Parameters(name = "protoBased={0}")
@JvmStatic
fun parameters() = listOf(true, false)
}
private val testSourceFile = SimpleJavaClass(tmpDir)
private lateinit var testClassSnapshot: ClassSnapshot
@Before
fun setUp() {
testClassSnapshot = testSourceFile.compile().snapshot(protoBased)
}
@Test
fun `test ClassSnapshotter's result against expected snapshot`() {
assertEquals(getExpectedSnapshotFile().readText(), testClassSnapshot.toGson())
}
private fun getExpectedSnapshotFile() = testSourceFile.sourceFile.asFile().path.let {
File(
it.substringBeforeLast("src") + "expected-snapshot" + it.substringAfterLast("src")
.substringBeforeLast('.') + "-protoBased=$protoBased.json"
)
}
@Test
fun `test ClassSnapshotter extracts ABI info from a class`() {
// Change public method signature
val updatedSnapshot = testSourceFile.changePublicMethodSignature().compile().snapshot(protoBased)
// The snapshot must change
assertNotEquals(testClassSnapshot.toGson(), updatedSnapshot.toGson())
}
@Test
fun `test ClassSnapshotter does not extract non-ABI info from a class`() {
// Change method implementation
val updatedSnapshot = testSourceFile.changeMethodImplementation().compile().snapshot(protoBased)
// The snapshot must not change
assertEquals(testClassSnapshot.toGson(), updatedSnapshot.toGson())
}
}
class JavaClassWithNestedClassesClasspathSnapshotterTest : ClasspathSnapshotTestCommon() {
private val testSourceFile = JavaClassWithNestedClasses(tmpDir)
private lateinit var testClassSnapshot: ClassSnapshot
@Before
fun setUp() {
testClassSnapshot = testSourceFile.compileAndSnapshotNestedClass()
}
private fun TestSourceFile.compileAndSnapshotNestedClass(): ClassSnapshot {
return compileAndSnapshotAll().single {
if (it is RegularJavaClassSnapshot) {
it.classAbiExcludingMembers.name == testSourceFile.nestedClassToTest
} else false
}
}
@Test
fun `test ClassSnapshotter's result against expected snapshot`() {
val expectedSnapshotFile = File("${testDataDir.path}/expected-snapshot/java/${testSourceFile.nestedClassToTest}.json")
assertEquals(expectedSnapshotFile.readText(), testClassSnapshot.toGson())
}
@Test
fun `test ClassSnapshotter extracts ABI info from a class`() {
val updatedSnapshot = testSourceFile.changePublicMethodSignature().compileAndSnapshotNestedClass()
assertNotEquals(testClassSnapshot.toGson(), updatedSnapshot.toGson())
}
@Test
fun `test ClassSnapshotter does not extract non-ABI info from a class`() {
val updatedSnapshot = testSourceFile.changeMethodImplementation().compileAndSnapshotNestedClass()
assertEquals(testClassSnapshot.toGson(), updatedSnapshot.toGson())
}
}
@@ -0,0 +1,10 @@
package com.example;
public class ChangedJavaSuperClass {
public String changedField = "";
public String changedMethod() {
return "";
}
}
@@ -0,0 +1,6 @@
package com.example;
open class ChangedKotlinSuperClass {
val changedProperty = ""
fun changedFunction() = ""
}
@@ -0,0 +1,4 @@
package com.example;
public class JavaSubClassOfKotlinSuperClass extends ChangedKotlinSuperClass {
}
@@ -0,0 +1,9 @@
package com.example;
public class ChangedJavaSuperClass {
public int changedField = 0;
public void changedMethod() {
}
}
@@ -0,0 +1,6 @@
package com.example;
open class ChangedKotlinSuperClass {
val changedProperty = 0
fun changedFunction() {}
}
@@ -0,0 +1,4 @@
package com.example;
public class JavaSubClassOfKotlinSuperClass extends ChangedKotlinSuperClass {
}
@@ -0,0 +1,10 @@
package com.example;
public class ChangedSuperClass {
public String changedField = "";
public String changedMethod() {
return "";
}
}
@@ -0,0 +1,9 @@
package com.example;
public class ChangedSuperClass {
public int changedField = 0;
public void changedMethod() {
}
}
@@ -0,0 +1,6 @@
package com.example;
open class ChangedSuperClass {
val changedProperty = ""
fun changedFunction() = ""
}
@@ -0,0 +1,6 @@
package com.example;
open class ChangedSuperClass {
val changedProperty = 0
fun changedFunction() {}
}
@@ -0,0 +1,9 @@
package com.example;
public class AddedClass {
public int someField = 0;
public void someMethod() {
}
}
@@ -0,0 +1,18 @@
package com.example;
public class ModifiedClassChangedMembers {
public int unchangedField = 0;
public String modifiedField = "";
public int addedField = 0;
public void unchangedMethod() {
}
public String modifiedMethod() {
return "";
}
public void addedMethod() {
}
}
@@ -0,0 +1,10 @@
package com.example;
@Deprecated // Changed annotation
public class ModifiedClassUnchangedMembers {
public int unchangedField = 0;
public void unchangedMethod() {
}
}
@@ -0,0 +1,9 @@
package com.example;
public class UnchangedClass {
public int someField = 0;
public void someMethod() {
}
}
@@ -0,0 +1,17 @@
package com.example;
public class ModifiedClassChangedMembers {
public int unchangedField = 0;
public int modifiedField = 0;
public int removedField = 0;
public void unchangedMethod() {
}
public void modifiedMethod() {
}
public void removedMethod() {
}
}
@@ -0,0 +1,9 @@
package com.example;
public class ModifiedClassUnchangedMembers {
public int unchangedField = 0;
public void unchangedMethod() {
}
}
@@ -0,0 +1,9 @@
package com.example;
public class RemovedClass {
public int someField = 0;
public void someMethod() {
}
}
@@ -0,0 +1,9 @@
package com.example;
public class UnchangedClass {
public int someField = 0;
public void someMethod() {
}
}
@@ -0,0 +1,6 @@
package com.example
class AddedClass {
val someProperty = 0
fun someFunction() {}
}
@@ -0,0 +1,10 @@
package com.example
class ModifiedClassChangedMembers {
val unchangedProperty = 0
val modifiedProperty = ""
val addedProperty = 0
fun unchangedFunction() {}
fun modifiedFunction() = ""
fun addedFunction() {}
}
@@ -0,0 +1,7 @@
package com.example
@java.lang.Deprecated // Changed annotation
class ModifiedClassUnchangedMembers {
val unchangedProperty = 0
fun unchangedFunction() {}
}
@@ -0,0 +1,6 @@
package com.example
class UnchangedClass {
val someProperty = 0
fun someFunction() {}
}
@@ -0,0 +1,11 @@
package com.example
val unchangedTopLevelProperty = 0
val modifiedTopLevelProperty = ""
val addedTopLevelProperty = 0
fun unchangedTopLevelFunction() {}
fun modifiedTopLevelFunction() = ""
fun addedTopLevelFunction() {}
fun movedTopLevelFunction() {}
@@ -0,0 +1,10 @@
package com.example
class ModifiedClassChangedMembers {
val unchangedProperty = 0
val modifiedProperty = 0
val removedProperty = 0
fun unchangedFunction() {}
fun modifiedFunction() {}
fun removedFunction() {}
}
@@ -0,0 +1,6 @@
package com.example
class ModifiedClassUnchangedMembers {
val unchangedProperty = 0
fun unchangedFunction() {}
}
@@ -0,0 +1,6 @@
package com.example
class RemovedClass {
val someProperty = 0
fun someFunction() {}
}
@@ -0,0 +1,6 @@
package com.example
class UnchangedClass {
val someProperty = 0
fun someFunction() {}
}
@@ -0,0 +1,10 @@
package com.example
val unchangedTopLevelProperty = 0
val modifiedTopLevelProperty = 0
val removedTopLevelProperty = 0
val movedTopLevelProperty = 0
fun unchangedTopLevelFunction() {}
fun modifiedTopLevelFunction() {}
fun removedTopLevelFunction() {}
@@ -0,0 +1,58 @@
{
"classId": {
"packageFqName": {
"fqName": {
"fqName": "com.example"
}
},
"relativeClassName": {
"fqName": {
"fqName": "JavaClassWithNestedClasses.InnerClass"
}
},
"local": false
},
"supertypes": [
{
"internalName": "java/lang/Object"
}
],
"classAbiExcludingMembers": {
"abiValue": "{\n \"version\": 52,\n \"access\": 33,\n \"name\": \"com/example/JavaClassWithNestedClasses$InnerClass\",\n \"superName\": \"java/lang/Object\",\n \"interfaces\": [],\n \"sourceFile\": \"JavaClassWithNestedClasses.java\",\n \"innerClasses\": [\n {\n \"name\": \"com/example/JavaClassWithNestedClasses$InnerClass\",\n \"outerName\": \"com/example/JavaClassWithNestedClasses\",\n \"innerName\": \"InnerClass\",\n \"access\": 1\n },\n {\n \"name\": \"com/example/JavaClassWithNestedClasses$InnerClass$InnerClassWithinInnerClass\",\n \"outerName\": \"com/example/JavaClassWithNestedClasses$InnerClass\",\n \"innerName\": \"InnerClassWithinInnerClass\",\n \"access\": 1\n },\n {\n \"name\": \"com/example/JavaClassWithNestedClasses$InnerClass$1LocalClassWithinInnerClass\",\n \"innerName\": \"LocalClassWithinInnerClass\",\n \"access\": 0\n }\n ],\n \"fields\": [],\n \"methods\": [],\n \"api\": 589824\n}",
"name": "com/example/JavaClassWithNestedClasses$InnerClass",
"abiHash": -5172784707052054073
},
"fieldsAbi": [
{
"abiValue": "{\n \"access\": 1,\n \"name\": \"publicField\",\n \"desc\": \"I\",\n \"api\": 589824\n}",
"name": "publicField",
"abiHash": -4066101678319939363
},
{
"abiValue": "{\n \"access\": 4112,\n \"name\": \"this$0\",\n \"desc\": \"Lcom/example/JavaClassWithNestedClasses;\",\n \"api\": 589824\n}",
"name": "this$0",
"abiHash": 3333476895593072424
}
],
"methodsAbi": [
{
"abiValue": "{\n \"access\": 1,\n \"name\": \"\\u003cinit\\u003e\",\n \"desc\": \"(Lcom/example/JavaClassWithNestedClasses;)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": -4959881636706217659
},
{
"abiValue": "{\n \"access\": 1,\n \"name\": \"publicMethod\",\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": "publicMethod",
"abiHash": -7202431168536137702
},
{
"abiValue": "{\n \"access\": 1,\n \"name\": \"someMethod\",\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": "someMethod",
"abiHash": 8928076277544870062
}
],
"className$delegate": {
"initializer": {},
"_value": {}
}
}
@@ -0,0 +1,48 @@
{
"classId": {
"packageFqName": {
"fqName": {
"fqName": "com.example"
}
},
"relativeClassName": {
"fqName": {
"fqName": "SimpleJavaClass"
}
},
"local": false
},
"supertypes": [
{
"internalName": "java/lang/Object"
}
],
"classAbiExcludingMembers": {
"abiValue": "{\n \"version\": 52,\n \"access\": 33,\n \"name\": \"com/example/SimpleJavaClass\",\n \"superName\": \"java/lang/Object\",\n \"interfaces\": [],\n \"sourceFile\": \"SimpleJavaClass.java\",\n \"innerClasses\": [],\n \"fields\": [],\n \"methods\": [],\n \"api\": 589824\n}",
"name": "com/example/SimpleJavaClass",
"abiHash": 4897366397927258364
},
"fieldsAbi": [
{
"abiValue": "{\n \"access\": 1,\n \"name\": \"publicField\",\n \"desc\": \"I\",\n \"api\": 589824\n}",
"name": "publicField",
"abiHash": -4066101678319939363
}
],
"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\": \"()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": "publicMethod",
"abiHash": -7202431168536137702
}
],
"className$delegate": {
"initializer": {},
"_value": {}
}
}
@@ -0,0 +1,45 @@
{
"classInfo": {
"classId": {
"packageFqName": {
"fqName": {
"fqName": "com.example"
}
},
"relativeClassName": {
"fqName": {
"fqName": "SimpleKotlinClass"
}
},
"local": false
},
"classKind": "CLASS",
"classHeaderData": [
"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\b\n\u0002\b\u0004\n\u0002\u0010\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\b\u0010\b\u001a\u00020\tH\u0002J\u0006\u0010\n\u001a\u00020\tR\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/SimpleKotlinClass;",
"",
"()V",
"privateProperty",
"",
"publicProperty",
"getPublicProperty",
"()I",
"privateFunction",
"",
"publicFunction"
],
"constantsMap": {},
"inlineFunctionsMap": {},
"className$delegate": {
"initializer": {},
"_value": {}
}
},
"supertypes": [
{
"internalName": "kotlin/Any"
}
]
}
@@ -0,0 +1,55 @@
package com.example;
public class JavaClassWithNestedClasses {
public class InnerClass {
public int publicField = 0;
private int privateField = 0;
public void publicMethod() {
System.out.println("I'm in a public method");
}
private void privateMethod() {
System.out.println("I'm in a private method");
}
public class InnerClassWithinInnerClass {
}
public void someMethod() {
class LocalClassWithinInnerClass {
}
}
}
public static class StaticNestedClass {
}
public void someMethod() {
class LocalClass {
class InnerClassWithinLocalClass {
}
}
Runnable objectOfAnonymousLocalClass = new Runnable() {
@Override
public void run() {
}
};
}
private Runnable objectOfAnonymousNonLocalClass = new Runnable() {
@Override
public void run() {
}
};
public class InnerClassWith$Sign {
}
}

Some files were not shown because too many files have changed in this diff Show More