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:
committed by
teamcityserver
parent
c46e9943cc
commit
dfaf195e1d
+103
@@ -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)
|
||||
}
|
||||
}
|
||||
+36
@@ -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
|
||||
)
|
||||
+396
@@ -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
|
||||
}
|
||||
}
|
||||
+113
@@ -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()
|
||||
+214
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+357
@@ -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
|
||||
+86
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+337
@@ -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
|
||||
}
|
||||
+44
@@ -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())
|
||||
}
|
||||
}
|
||||
+256
@@ -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!")
|
||||
}
|
||||
}
|
||||
+145
@@ -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())
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+10
@@ -0,0 +1,10 @@
|
||||
package com.example;
|
||||
|
||||
public class ChangedJavaSuperClass {
|
||||
|
||||
public String changedField = "";
|
||||
|
||||
public String changedMethod() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.example;
|
||||
|
||||
open class ChangedKotlinSuperClass {
|
||||
val changedProperty = ""
|
||||
fun changedFunction() = ""
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.example;
|
||||
|
||||
public class JavaSubClassOfJavaSuperClass extends ChangedJavaSuperClass {
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.example;
|
||||
|
||||
public class JavaSubClassOfKotlinSuperClass extends ChangedKotlinSuperClass {
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example
|
||||
|
||||
class KotlinSubClassOfJavaSuperClass : ChangedJavaSuperClass()
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example
|
||||
|
||||
class KotlinSubClassOfKotlinSuperClass : ChangedKotlinSuperClass()
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.example;
|
||||
|
||||
public class UnimpactedJavaClass {
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example
|
||||
|
||||
class UnimpactedKotlinClass
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.example;
|
||||
|
||||
public class ChangedJavaSuperClass {
|
||||
|
||||
public int changedField = 0;
|
||||
|
||||
public void changedMethod() {
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.example;
|
||||
|
||||
open class ChangedKotlinSuperClass {
|
||||
val changedProperty = 0
|
||||
fun changedFunction() {}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.example;
|
||||
|
||||
public class JavaSubClassOfJavaSuperClass extends ChangedJavaSuperClass {
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.example;
|
||||
|
||||
public class JavaSubClassOfKotlinSuperClass extends ChangedKotlinSuperClass {
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example
|
||||
|
||||
class KotlinSubClassOfJavaSuperClass : ChangedJavaSuperClass()
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example
|
||||
|
||||
class KotlinSubClassOfKotlinSuperClass : ChangedKotlinSuperClass()
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.example;
|
||||
|
||||
public class UnimpactedJavaClass {
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example
|
||||
|
||||
class UnimpactedKotlinClass
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+10
@@ -0,0 +1,10 @@
|
||||
package com.example;
|
||||
|
||||
public class ChangedSuperClass {
|
||||
|
||||
public String changedField = "";
|
||||
|
||||
public String changedMethod() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.example;
|
||||
|
||||
public class SubClass extends ChangedSuperClass {
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.example;
|
||||
|
||||
public class SubSubClass extends SubClass {
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.example;
|
||||
|
||||
public class UnimpactedClass {
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.example;
|
||||
|
||||
public class ChangedSuperClass {
|
||||
|
||||
public int changedField = 0;
|
||||
|
||||
public void changedMethod() {
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.example;
|
||||
|
||||
public class SubClass extends ChangedSuperClass {
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.example;
|
||||
|
||||
public class SubSubClass extends SubClass {
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.example;
|
||||
|
||||
public class UnimpactedClass {
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.example;
|
||||
|
||||
open class ChangedSuperClass {
|
||||
val changedProperty = ""
|
||||
fun changedFunction() = ""
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example
|
||||
|
||||
open class SubClass : ChangedSuperClass()
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example
|
||||
|
||||
class SubSubClass : SubClass()
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example
|
||||
|
||||
class UnimpactedClass
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.example;
|
||||
|
||||
open class ChangedSuperClass {
|
||||
val changedProperty = 0
|
||||
fun changedFunction() {}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example
|
||||
|
||||
open class SubClass : ChangedSuperClass()
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example
|
||||
|
||||
class SubSubClass : SubClass()
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example
|
||||
|
||||
class UnimpactedClass
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+9
@@ -0,0 +1,9 @@
|
||||
package com.example;
|
||||
|
||||
public class AddedClass {
|
||||
|
||||
public int someField = 0;
|
||||
|
||||
public void someMethod() {
|
||||
}
|
||||
}
|
||||
+18
@@ -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() {
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.example;
|
||||
|
||||
@Deprecated // Changed annotation
|
||||
public class ModifiedClassUnchangedMembers {
|
||||
|
||||
public int unchangedField = 0;
|
||||
|
||||
public void unchangedMethod() {
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.example;
|
||||
|
||||
public class UnchangedClass {
|
||||
|
||||
public int someField = 0;
|
||||
|
||||
public void someMethod() {
|
||||
}
|
||||
}
|
||||
+17
@@ -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() {
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.example;
|
||||
|
||||
public class ModifiedClassUnchangedMembers {
|
||||
|
||||
public int unchangedField = 0;
|
||||
|
||||
public void unchangedMethod() {
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.example;
|
||||
|
||||
public class RemovedClass {
|
||||
|
||||
public int someField = 0;
|
||||
|
||||
public void someMethod() {
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.example;
|
||||
|
||||
public class UnchangedClass {
|
||||
|
||||
public int someField = 0;
|
||||
|
||||
public void someMethod() {
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.example
|
||||
|
||||
class AddedClass {
|
||||
val someProperty = 0
|
||||
fun someFunction() {}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.example
|
||||
|
||||
class ModifiedClassChangedMembers {
|
||||
val unchangedProperty = 0
|
||||
val modifiedProperty = ""
|
||||
val addedProperty = 0
|
||||
fun unchangedFunction() {}
|
||||
fun modifiedFunction() = ""
|
||||
fun addedFunction() {}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.example
|
||||
|
||||
@java.lang.Deprecated // Changed annotation
|
||||
class ModifiedClassUnchangedMembers {
|
||||
val unchangedProperty = 0
|
||||
fun unchangedFunction() {}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.example
|
||||
|
||||
class UnchangedClass {
|
||||
val someProperty = 0
|
||||
fun someFunction() {}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.example
|
||||
|
||||
val unchangedTopLevelProperty = 0
|
||||
val modifiedTopLevelProperty = ""
|
||||
val addedTopLevelProperty = 0
|
||||
|
||||
fun unchangedTopLevelFunction() {}
|
||||
fun modifiedTopLevelFunction() = ""
|
||||
fun addedTopLevelFunction() {}
|
||||
|
||||
fun movedTopLevelFunction() {}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example
|
||||
|
||||
val movedTopLevelProperty = 0
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.example
|
||||
|
||||
class ModifiedClassChangedMembers {
|
||||
val unchangedProperty = 0
|
||||
val modifiedProperty = 0
|
||||
val removedProperty = 0
|
||||
fun unchangedFunction() {}
|
||||
fun modifiedFunction() {}
|
||||
fun removedFunction() {}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.example
|
||||
|
||||
class ModifiedClassUnchangedMembers {
|
||||
val unchangedProperty = 0
|
||||
fun unchangedFunction() {}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.example
|
||||
|
||||
class RemovedClass {
|
||||
val someProperty = 0
|
||||
fun someFunction() {}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.example
|
||||
|
||||
class UnchangedClass {
|
||||
val someProperty = 0
|
||||
fun someFunction() {}
|
||||
}
|
||||
+10
@@ -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() {}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example
|
||||
|
||||
fun movedTopLevelFunction() {}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+58
@@ -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": {}
|
||||
}
|
||||
}
|
||||
+48
@@ -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": {}
|
||||
}
|
||||
}
|
||||
+1164
File diff suppressed because it is too large
Load Diff
+45
@@ -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\u0004XD¢\u0006\u0002\n\u0000R\u0014\u0010\u0005\u001a\u00020\u0004XD¢\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"
|
||||
}
|
||||
]
|
||||
}
|
||||
+55
@@ -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
Reference in New Issue
Block a user