New IC: Add constants-in-companion-objects impact computation
When computing impacted symbols of changed symbols, previously we considered only the supertypes-inheritors type of impact, which is the most common type. This commit adds the constants-in-companion-objects type of impact to address KT-53266. We've also cleaned up impact computation to make it easier to add new types of impact in the future. ^KT-53266 In progress
This commit is contained in:
committed by
Alexander Likhachev
parent
3f0a93d6dd
commit
2ad047340f
@@ -17,7 +17,6 @@
|
|||||||
package org.jetbrains.kotlin.incremental
|
package org.jetbrains.kotlin.incremental
|
||||||
|
|
||||||
import org.jetbrains.kotlin.metadata.ProtoBuf
|
import org.jetbrains.kotlin.metadata.ProtoBuf
|
||||||
import org.jetbrains.kotlin.metadata.deserialization.Flags
|
|
||||||
import org.jetbrains.kotlin.metadata.deserialization.NameResolver
|
import org.jetbrains.kotlin.metadata.deserialization.NameResolver
|
||||||
import org.jetbrains.kotlin.name.FqName
|
import org.jetbrains.kotlin.name.FqName
|
||||||
import org.jetbrains.kotlin.protobuf.MessageLite
|
import org.jetbrains.kotlin.protobuf.MessageLite
|
||||||
@@ -203,9 +202,8 @@ class ChangesCollector {
|
|||||||
|
|
||||||
private fun ClassProtoData.collectAllFromClass(isRemoved: Boolean, isAdded: Boolean, collectAllMembersForNewClass: Boolean = false) {
|
private fun ClassProtoData.collectAllFromClass(isRemoved: Boolean, isAdded: Boolean, collectAllMembersForNewClass: Boolean = false) {
|
||||||
val classFqName = nameResolver.getClassId(proto.fqName).asSingleFqName()
|
val classFqName = nameResolver.getClassId(proto.fqName).asSingleFqName()
|
||||||
val kind = Flags.CLASS_KIND.get(proto.flags)
|
|
||||||
|
|
||||||
if (kind == ProtoBuf.Class.Kind.COMPANION_OBJECT) {
|
if (proto.isCompanionObject) {
|
||||||
val memberNames = getNonPrivateMemberNames()
|
val memberNames = getNonPrivateMemberNames()
|
||||||
|
|
||||||
val collectMember = if (isRemoved) this@ChangesCollector::collectRemovedMember else this@ChangesCollector::collectChangedMember
|
val collectMember = if (isRemoved) this@ChangesCollector::collectRemovedMember else this@ChangesCollector::collectChangedMember
|
||||||
|
|||||||
@@ -455,27 +455,27 @@ open class IncrementalJvmCache(
|
|||||||
val allConstants = oldMap.keys + newMap.keys
|
val allConstants = oldMap.keys + newMap.keys
|
||||||
if (allConstants.isEmpty()) return
|
if (allConstants.isEmpty()) return
|
||||||
|
|
||||||
// If a constant is defined in a companion object, it will be found in the constantsMap of the containing class, not the
|
val scope = kotlinClassInfo.scopeFqName()
|
||||||
// companion object's class, so we will need to correct its scope.
|
|
||||||
// (See https://youtrack.jetbrains.com/issue/KT-44741#focus=Comments-27-5659564.0-0 for more details.)
|
|
||||||
// Note: This only applies to a *constant* defined in a *companion object* (it's not an issue for inline functions, or top-level
|
|
||||||
// constants, or constants in non-companion objects).
|
|
||||||
val companionObjectClassId = if (kotlinClassInfo.classKind == KotlinClassHeader.Kind.CLASS) {
|
|
||||||
val protoData = kotlinClassInfo.protoData as ClassProtoData
|
|
||||||
if (protoData.proto.hasCompanionObjectName()) {
|
|
||||||
val companionObjectName = Name.identifier(protoData.nameResolver.getString(protoData.proto.companionObjectName))
|
|
||||||
kotlinClassInfo.classId.createNestedClassId(companionObjectName)
|
|
||||||
} else null
|
|
||||||
} else null
|
|
||||||
val scope = companionObjectClassId?.asSingleFqName() ?: kotlinClassInfo.scopeFqName()
|
|
||||||
|
|
||||||
// Here we assume that the old and new classes have the same KotlinClassHeader.Kind, so that the scopes of the old and new
|
|
||||||
// constants are the same and their values can be compared.
|
|
||||||
// If the class kinds are different, the changes will be detected when comparing protos (in that case, the changes collected
|
|
||||||
// here will be a subset of those changes).
|
|
||||||
for (const in allConstants) {
|
for (const in allConstants) {
|
||||||
changesCollector.collectMemberIfValueWasChanged(scope, const, oldMap[const], newMap[const])
|
changesCollector.collectMemberIfValueWasChanged(scope, const, oldMap[const], newMap[const])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a constant is defined in a companion object of class A, its name and type will be found in the Kotlin metadata of
|
||||||
|
// `A$Companion.class`, but its value will only be found in the Java bytecode code of `A.class` (see
|
||||||
|
// `org.jetbrains.kotlin.incremental.classpathDiff.ConstantsInCompanionObjectImpact` for more details).
|
||||||
|
// Therefore, if the value of `CONSTANT` in `A.class` has changed, we will report that `A.CONSTANT` has changed in the code
|
||||||
|
// above, and report that `A.Companion.CONSTANT` is impacted in the code below.
|
||||||
|
kotlinClassInfo.companionObject?.let { companionObjectClassId ->
|
||||||
|
// Note that `companionObjectClassId` is the companion object of the current class. Here we assume that the previous class
|
||||||
|
// also has a companion object with the same name. If that is not the case, that change will be detected when comparing
|
||||||
|
// protos, and the report below will be imprecise/redundant, but it's okay to over-approximate the result.
|
||||||
|
val companionObjectFqName = companionObjectClassId.asSingleFqName()
|
||||||
|
for (const in allConstants) {
|
||||||
|
changesCollector.collectMemberIfValueWasChanged(
|
||||||
|
scope = companionObjectFqName, name = const, oldMap[const], newMap[const]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@@ -688,7 +688,7 @@ class KotlinClassInfo constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the [ProtoData] of this class.
|
* The [ProtoData] of this class.
|
||||||
*
|
*
|
||||||
* NOTE: The caller needs to ensure `classKind != KotlinClassHeader.Kind.MULTIFILE_CLASS` first, as the compiler doesn't write proto
|
* NOTE: The caller needs to ensure `classKind != KotlinClassHeader.Kind.MULTIFILE_CLASS` first, as the compiler doesn't write proto
|
||||||
* data to [KotlinClassHeader.Kind.MULTIFILE_CLASS] classes.
|
* data to [KotlinClassHeader.Kind.MULTIFILE_CLASS] classes.
|
||||||
@@ -700,6 +700,25 @@ class KotlinClassInfo constructor(
|
|||||||
protoMapValue.toProtoData(classId.packageFqName)
|
protoMapValue.toProtoData(classId.packageFqName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Name of the companion object of this class (default is "Companion") iff this class HAS a companion object, or null otherwise. */
|
||||||
|
val companionObject: ClassId? by lazy {
|
||||||
|
if (classKind == KotlinClassHeader.Kind.CLASS) {
|
||||||
|
(protoData as ClassProtoData).getCompanionObjectName()?.let {
|
||||||
|
classId.createNestedClassId(Name.identifier(it))
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** List of constants defined in this class iff this class IS a companion object, or null otherwise. The list could be empty. */
|
||||||
|
val constantsInCompanionObject: List<String>? by lazy {
|
||||||
|
if (classKind == KotlinClassHeader.Kind.CLASS) {
|
||||||
|
val classProtoData = protoData as ClassProtoData
|
||||||
|
if (classProtoData.proto.isCompanionObject) {
|
||||||
|
classProtoData.getConstants()
|
||||||
|
} else null
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun createFrom(kotlinClass: LocalFileKotlinClass): KotlinClassInfo {
|
fun createFrom(kotlinClass: LocalFileKotlinClass): KotlinClassInfo {
|
||||||
|
|||||||
@@ -367,8 +367,23 @@ class DifferenceCalculatorForPackageFacade(
|
|||||||
private val ProtoBuf.Class.isSealed: Boolean
|
private val ProtoBuf.Class.isSealed: Boolean
|
||||||
get() = ProtoBuf.Modality.SEALED == Flags.MODALITY.get(flags)
|
get() = ProtoBuf.Modality.SEALED == Flags.MODALITY.get(flags)
|
||||||
|
|
||||||
|
internal val ProtoBuf.Class.isCompanionObject: Boolean
|
||||||
|
get() = ProtoBuf.Class.Kind.COMPANION_OBJECT == Flags.CLASS_KIND.get(flags)
|
||||||
|
|
||||||
val ProtoBuf.Class.typeTableOrNull: ProtoBuf.TypeTable?
|
val ProtoBuf.Class.typeTableOrNull: ProtoBuf.TypeTable?
|
||||||
get() = if (hasTypeTable()) typeTable else null
|
get() = if (hasTypeTable()) typeTable else null
|
||||||
|
|
||||||
val ProtoBuf.Package.typeTableOrNull: ProtoBuf.TypeTable?
|
val ProtoBuf.Package.typeTableOrNull: ProtoBuf.TypeTable?
|
||||||
get() = if (hasTypeTable()) typeTable else null
|
get() = if (hasTypeTable()) typeTable else null
|
||||||
|
|
||||||
|
internal fun ClassProtoData.getCompanionObjectName(): String? {
|
||||||
|
return if (proto.hasCompanionObjectName()) {
|
||||||
|
nameResolver.getString(proto.companionObjectName)
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun ClassProtoData.getConstants(): List<String> {
|
||||||
|
return proto.propertyList
|
||||||
|
.filter { Flags.IS_CONST.get(it.flags) }
|
||||||
|
.map { nameResolver.getString(it.name) }
|
||||||
|
}
|
||||||
|
|||||||
+113
-124
@@ -12,13 +12,15 @@ import org.jetbrains.kotlin.build.report.metrics.BuildMetricsReporter
|
|||||||
import org.jetbrains.kotlin.build.report.metrics.BuildTime
|
import org.jetbrains.kotlin.build.report.metrics.BuildTime
|
||||||
import org.jetbrains.kotlin.build.report.metrics.measure
|
import org.jetbrains.kotlin.build.report.metrics.measure
|
||||||
import org.jetbrains.kotlin.incremental.*
|
import org.jetbrains.kotlin.incremental.*
|
||||||
|
import org.jetbrains.kotlin.incremental.classpathDiff.BreadthFirstSearch.findReachableNodes
|
||||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinker.shrinkClasspath
|
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinker.shrinkClasspath
|
||||||
import org.jetbrains.kotlin.incremental.classpathDiff.ImpactAnalysis.computeImpactedSetInclusive
|
import org.jetbrains.kotlin.incremental.classpathDiff.ImpactedSymbolsComputer.computeImpactedSymbols
|
||||||
import org.jetbrains.kotlin.incremental.storage.FileToCanonicalPathConverter
|
import org.jetbrains.kotlin.incremental.storage.FileToCanonicalPathConverter
|
||||||
import org.jetbrains.kotlin.incremental.storage.ListExternalizer
|
import org.jetbrains.kotlin.incremental.storage.ListExternalizer
|
||||||
import org.jetbrains.kotlin.incremental.storage.loadFromFile
|
import org.jetbrains.kotlin.incremental.storage.loadFromFile
|
||||||
import org.jetbrains.kotlin.name.ClassId
|
import org.jetbrains.kotlin.name.ClassId
|
||||||
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
|
import org.jetbrains.kotlin.name.FqName
|
||||||
|
import org.jetbrains.kotlin.name.Name
|
||||||
import org.jetbrains.kotlin.resolve.sam.SAM_LOOKUP_NAME
|
import org.jetbrains.kotlin.resolve.sam.SAM_LOOKUP_NAME
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -110,14 +112,16 @@ object ClasspathChangesComputer {
|
|||||||
val changedAndImpactedSet = reporter.measure(BuildTime.COMPUTE_IMPACTED_SET) {
|
val changedAndImpactedSet = reporter.measure(BuildTime.COMPUTE_IMPACTED_SET) {
|
||||||
// Note that changes may contain added symbols (they can also impact recompilation -- see examples in JavaClassChangesComputer).
|
// Note that changes may contain added symbols (they can also impact recompilation -- see examples in JavaClassChangesComputer).
|
||||||
// So ideally, the result should be:
|
// So ideally, the result should be:
|
||||||
// computeImpactedSetInclusive(changes = changesOnPreviousClasspath, allClasses = classesOnPreviousClasspath) +
|
// computeImpactedSymbols(changes = changesOnPreviousClasspath, allClasses = classesOnPreviousClasspath) +
|
||||||
// computeImpactedSetInclusive(changes = changesOnCurrentClasspath, allClasses = classesOnCurrentClasspath)
|
// computeImpactedSymbols(changes = changesOnCurrentClasspath, allClasses = classesOnCurrentClasspath)
|
||||||
// However, here we only have the combined changes on both the previous and current classpath, and because it's okay to
|
// However, here we only have the combined changes on both the previous and current classpath, and because it's okay to
|
||||||
// over-approximate the result, we will modify the above computation into:
|
// over-approximate the result, we will modify the above computation into:
|
||||||
// computeImpactedSetInclusive(changes, allClasses = classesOnPreviousClasspath + classesOnCurrentClasspath)
|
// computeImpactedSet(
|
||||||
// Note: `allClasses` may contain overlapping ClassIds, but it won't be an issue. Also, we will replace
|
// changes = changesOnPreviousAndCurrentClasspath,
|
||||||
// classesOnCurrentClasspath with changedClassesOnCurrentClasspath to avoid listing unchanged classes twice.
|
// allClasses = classesOnPreviousClasspath + classesOnCurrentClasspath)
|
||||||
computeImpactedSetInclusive(
|
// Note: We will replace `classesOnCurrentClasspath` with `changedClassesOnCurrentClasspath` to avoid listing unchanged classes
|
||||||
|
// twice. `allClasses` may contain overlapping ClassIds of modified classes, but it won't be an issue.
|
||||||
|
computeImpactedSymbols(
|
||||||
changes = changedSet,
|
changes = changedSet,
|
||||||
allClasses = (previousClassSnapshots.asSequence() + changedCurrentClasses.asSequence()).asIterable()
|
allClasses = (previousClassSnapshots.asSequence() + changedCurrentClasses.asSequence()).asIterable()
|
||||||
)
|
)
|
||||||
@@ -240,158 +244,143 @@ object ClasspathChangesComputer {
|
|||||||
// classes, and symbols in removed classes.
|
// classes, and symbols in removed classes.
|
||||||
incrementalJvmCache.clearCacheForRemovedClasses(changesCollector)
|
incrementalJvmCache.clearCacheForRemovedClasses(changesCollector)
|
||||||
|
|
||||||
// Normalize the changes and clean up
|
// Get the changes and clean up
|
||||||
val dirtyData = changesCollector.getDirtyData(listOf(incrementalJvmCache), DoNothingICReporter)
|
val dirtyData = changesCollector.getDirtyData(listOf(incrementalJvmCache), DoNothingICReporter)
|
||||||
workingDir.deleteRecursively()
|
workingDir.deleteRecursively()
|
||||||
|
|
||||||
return dirtyData.toProgramSymbols(currentClassSnapshots, previousClassSnapshots)
|
// Normalize the changes (convert DirtyData to `ProgramSymbol`s)
|
||||||
}
|
// Note:
|
||||||
|
// - DirtyData may contain added symbols (they can also impact recompilation -- see examples in JavaClassChangesComputer).
|
||||||
private fun DirtyData.toProgramSymbols(
|
// Therefore, we need to consider classes on both the previous and current classpath when converting DirtyData.
|
||||||
currentClassSnapshots: List<AccessibleClassSnapshot>,
|
// - We have removed unchanged classes earlier in computeChangedAndImpactedSet method, so here we only have changed classes.
|
||||||
previousClassSnapshots: List<AccessibleClassSnapshot>
|
// - `changedClasses` may contain overlapping ClassIds of modified classes, but it won't be an issue.
|
||||||
): ProgramSymbolSet {
|
val changedClasses = (previousClassSnapshots.asSequence() + currentClassSnapshots.asSequence()).asIterable()
|
||||||
// Note that dirtyLookupSymbols may contain added symbols (they can also impact recompilation -- see examples in
|
return dirtyData.toProgramSymbols(changedClasses)
|
||||||
// JavaClassChangesComputer). Therefore, we need to consider classes on both the previous and current classpath. The combined list
|
|
||||||
// may contain overlapping ClassIds, but it won't be an issue.
|
|
||||||
val allClasses = (previousClassSnapshots.asSequence() + currentClassSnapshots.asSequence()).asIterable()
|
|
||||||
return dirtyLookupSymbols.toProgramSymbolSet(allClasses).also {
|
|
||||||
checkDirtyDataNormalization(this, it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether [DirtyData.toProgramSymbols] completed without any inconsistencies.
|
* Converts this [DirtyData] to [ProgramSymbol]s.
|
||||||
*
|
*
|
||||||
* Specifically, [DirtyData] consists of:
|
* Specifically, [DirtyData] consists of:
|
||||||
* - dirtyLookupSymbols (Collection<LookupSymbol)
|
* - dirtyLookupSymbols (Collection<LookupSymbol)
|
||||||
* - dirtyClassesFqNames (Collection<FqName>)
|
* - dirtyClassesFqNames (Collection<FqName>)
|
||||||
* - dirtyClassesFqNamesForceRecompile (Collection<FqName>)
|
* - dirtyClassesFqNamesForceRecompile (Collection<FqName>)
|
||||||
*
|
*
|
||||||
* In [DirtyData.toProgramSymbols], we converted only dirtyLookupSymbols to [ProgramSymbol]s as dirtyLookupSymbols should contain all
|
* First, we will convert `dirtyLookupSymbols` to [ProgramSymbol]s as `dirtyLookupSymbols` should contain all the changes.
|
||||||
* the changes.
|
|
||||||
*
|
*
|
||||||
* In the following, we'll check that:
|
* Then, we will check that:
|
||||||
* 1. There are no items in dirtyLookupSymbols that have not yet been converted to [ProgramSymbol]s.
|
* 1. There are no items in `dirtyLookupSymbols` that have not yet been converted to [ProgramSymbol]s.
|
||||||
* 2. dirtyClassesFqNames and dirtyClassesFqNamesForceRecompile do not contain new information that can't be derived from
|
* 2. `dirtyClassesFqNames` and `dirtyClassesFqNamesForceRecompile` must not contain new information that can't be derived from
|
||||||
* dirtyLookupSymbols.
|
* `dirtyLookupSymbols`.
|
||||||
*/
|
*/
|
||||||
private fun checkDirtyDataNormalization(dirtyData: DirtyData, programSymbols: ProgramSymbolSet) {
|
private fun DirtyData.toProgramSymbols(changedClasses: Iterable<AccessibleClassSnapshot>): ProgramSymbolSet {
|
||||||
val changes = programSymbols.toChangesEither()
|
val changedProgramSymbols = dirtyLookupSymbols.toProgramSymbolSet(changedClasses)
|
||||||
|
|
||||||
val unmatchedLookupSymbols = dirtyData.dirtyLookupSymbols.toMutableSet().also {
|
// Check whether there is any info in this DirtyData that has not yet been converted to `changedProgramSymbols`
|
||||||
it.removeAll(changes.lookupSymbols.toSet())
|
val (changedLookupSymbols, changedFqNames) = changedProgramSymbols.toChangesEither().let {
|
||||||
|
it.lookupSymbols.toSet() to it.fqNames.toSet()
|
||||||
}
|
}
|
||||||
val unmatchedFqNames = (dirtyData.dirtyClassesFqNames).toMutableSet().also {
|
val unmatchedLookupSymbols = this.dirtyLookupSymbols.toMutableSet().also {
|
||||||
it.removeAll(changes.fqNames.toSet())
|
it.removeAll(changedLookupSymbols)
|
||||||
|
}
|
||||||
|
val unmatchedFqNames = this.dirtyClassesFqNames.toMutableSet().also {
|
||||||
|
it.addAll(this.dirtyClassesFqNamesForceRecompile)
|
||||||
|
it.removeAll(changedFqNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some LookupSymbols (reported by IncrementalJvmCache) are invalid. Examples:
|
/* When `unmatchedLookupSymbols` or `unmatchedFqNames` is not empty, there are two cases:
|
||||||
// - LookupSymbol(name=<SAM-CONSTRUCTOR>, scope=com.example) (detected by
|
* 1. The unmatched LookupSymbols/FqNames are redundant. This is not ideal but because it does not cause incremental compilation
|
||||||
// KotlinOnlyClasspathChangesComputerTest.testTopLevelMembers): SAM-CONSTRUCTOR should have a class scope not a package scope.
|
* to be incorrect, we can fix these issues later if they are not easy to fix immediately.
|
||||||
// - LookupSymbol(name=BarUseABKt, scope=bar) (detected by
|
* 2. The unmatched LookupSymbols/FqNames are valid changes. Since they are required for incremental compilation to be correct, we
|
||||||
// IncrementalCompilationClasspathSnapshotJvmMultiProjectIT.testMoveFunctionFromLibToApp): The name of a LookupSymbol should not
|
* must fix these issues immediately.
|
||||||
// end with "Kt" (unless there is a Kotlin class (CLASS kind) whose name ends with "Kt", which is almost never the case).
|
* In the following, we'll list the known issues for case 1 (and it must be case 1 only).
|
||||||
// Ignore these for now.
|
* TODO: We'll fix these issues later.
|
||||||
// TODO: Fix them later
|
*/
|
||||||
if (unmatchedLookupSymbols.isNotEmpty()) {
|
|
||||||
unmatchedLookupSymbols.removeAll { it.name == SAM_LOOKUP_NAME.asString() || it.name.endsWith("Kt") }
|
|
||||||
}
|
|
||||||
if (unmatchedFqNames.isNotEmpty()) {
|
|
||||||
unmatchedFqNames.removeAll { it.asString().endsWith("Kt") }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Known issue 1: DirtyData reported by IncrementalJvmCache may include both a class and class member (e.g.,
|
||||||
|
// LookupSymbol("com.example", "A") and LookupSymbol("com.example.A", "someProperty")). When the class LookupSymbol is present, the
|
||||||
|
// class member LookupSymbol is redundant. When converting DirtyData to ProgramSymbols, we remove redundant class member
|
||||||
|
// `ProgramSymbol`s, so here we will find that LookupSymbol("com.example.A", "someProperty") is not yet matched. Ignore these
|
||||||
|
// `LookupSymbol`s for now.
|
||||||
|
val classesFqNames = changedProgramSymbols.classes.mapTo(mutableSetOf()) { it.asSingleFqName() }
|
||||||
|
unmatchedLookupSymbols.removeAll { FqName(it.scope) in classesFqNames }
|
||||||
|
|
||||||
|
// Known issue 2: If class A has a companion object containing a constant `CONSTANT`, and if the value of `CONSTANT` has changed,
|
||||||
|
// then only `A.class` will change, not `A.Companion.class` (see `ConstantsInCompanionObjectImpact`). Since we distinguish between
|
||||||
|
// changed symbols and impacted symbols, we should detect that:
|
||||||
|
// - A.CONSTANT has changed
|
||||||
|
// - A.Companion.CONSTANT is unchanged but impacted (this detection happens after the step here)
|
||||||
|
//
|
||||||
|
// However, currently IncrementalJvmCache will report that both `A.CONSTANT` and `A.Companion.CONSTANT` have changed (see
|
||||||
|
// `IncrementalJvmCache.ConstantsMap.process`) as it needs to work with both the old IC and the new IC (in the old IC, changed
|
||||||
|
// symbols and impacted symbols are not clearly separated).
|
||||||
|
//
|
||||||
|
// With the new IC, when converting DirtyData to ProgramSymbols (this method), because we consider only changed classes and
|
||||||
|
// `A.Companion.class` is unchanged, we will not convert `A.Companion.CONSTANT`. Therefore, `A.Companion.CONSTANT` is unmatched,
|
||||||
|
// and we'll need to ignore it here.
|
||||||
|
//
|
||||||
|
// Note: Once we are able to remove this workaround, we can remove RegularKotlinClassSnapshot.companionObjectName as this is the only
|
||||||
|
// usage of that property.
|
||||||
|
val companionObjectFqNames = changedClasses.mapNotNullTo(mutableSetOf()) { clazz ->
|
||||||
|
(clazz as? RegularKotlinClassSnapshot)?.companionObjectName?.let { it ->
|
||||||
|
clazz.classId.createNestedClassId(Name.identifier(it)).asSingleFqName()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unmatchedLookupSymbols.removeAll { FqName(it.scope) in companionObjectFqNames }
|
||||||
|
unmatchedFqNames.removeAll(companionObjectFqNames)
|
||||||
|
|
||||||
|
// Known issue 3: LookupSymbol(name=<SAM-CONSTRUCTOR>, scope=com.example) reported by IncrementalJvmCache is invalid (detected by
|
||||||
|
// KotlinOnlyClasspathChangesComputerTest.testTopLevelMembers): SAM-CONSTRUCTOR should have a class scope, not a package scope.
|
||||||
|
unmatchedLookupSymbols.removeAll { it.name == SAM_LOOKUP_NAME.asString() && FqName(it.scope) !in classesFqNames }
|
||||||
|
|
||||||
|
// Known issue 4: LookupSymbol(name=BarUseABKt, scope=bar) reported by IncrementalJvmCache is invalid (detected by
|
||||||
|
// IncrementalCompilationClasspathSnapshotJvmMultiProjectIT.testMoveFunctionFromLibToApp): The name of a LookupSymbol should not
|
||||||
|
// end with "Kt" (unless there is a Kotlin class (CLASS kind) whose name ends with "Kt", which is almost never the case).
|
||||||
|
unmatchedLookupSymbols.removeAll { it.name.endsWith("Kt") }
|
||||||
|
unmatchedFqNames.removeAll { it.asString().endsWith("Kt") }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* End of known issues, throw an Exception.
|
||||||
|
*/
|
||||||
check(unmatchedLookupSymbols.isEmpty()) {
|
check(unmatchedLookupSymbols.isEmpty()) {
|
||||||
"The following LookupSymbols are not yet converted to ProgramSymbols: ${unmatchedLookupSymbols.joinToString(", ")}"
|
"The following LookupSymbols are not yet converted to ProgramSymbols: ${unmatchedLookupSymbols.joinToString(", ")}"
|
||||||
}
|
}
|
||||||
check(unmatchedFqNames.isEmpty()) {
|
check(unmatchedFqNames.isEmpty()) {
|
||||||
"The following FqNames are not found in DirtyData.dirtyLookupSymbols: ${unmatchedFqNames.joinToString(", ")}.\n" +
|
"The following FqNames can't be derived from DirtyData.dirtyLookupSymbols: ${unmatchedFqNames.joinToString(", ")}.\n" +
|
||||||
"DirtyData = $dirtyData"
|
"DirtyData = $this"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return changedProgramSymbols
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object ImpactAnalysis {
|
private object ImpactedSymbolsComputer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes the set of [ProgramSymbol]s that are impacted by the given changes.
|
* Computes the set of [ProgramSymbol]s that are *transitively* impacted by the given set of [ProgramSymbol]s. For example, if a
|
||||||
|
* superclass has changed/been impacted, its subclasses will be impacted.
|
||||||
*
|
*
|
||||||
* For example, if a superclass has changed, any of its subclasses will be impacted even if it has not changed because unchanged source
|
* The returned set is *inclusive* (it contains the given set + the directly/transitively impacted ones).
|
||||||
* files in the previous compilation that depended on the subclasses will need to be recompiled.
|
|
||||||
*
|
|
||||||
* The returned set includes the given changes plus the impacted ones.
|
|
||||||
*/
|
*/
|
||||||
fun computeImpactedSetInclusive(changes: ProgramSymbolSet, allClasses: Iterable<AccessibleClassSnapshot>): ProgramSymbolSet {
|
fun computeImpactedSymbols(changes: ProgramSymbolSet, allClasses: Iterable<AccessibleClassSnapshot>): ProgramSymbolSet {
|
||||||
val classIdToSubclasses = getClassIdToSubclassesMap(allClasses)
|
val impactedSymbolsResolver = AllImpacts.getResolver(allClasses)
|
||||||
val impactedClassesResolver = { classId: ClassId -> classIdToSubclasses[classId] ?: emptySet() }
|
return ProgramSymbolSet.Collector().apply {
|
||||||
|
// Add impacted classes
|
||||||
|
val impactedClasses = findReachableNodes(changes.classes, impactedSymbolsResolver::getImpactedClasses)
|
||||||
|
addClasses(impactedClasses)
|
||||||
|
|
||||||
return ProgramSymbolSet.Collector().run {
|
// Add impacted class members
|
||||||
addClasses(findImpactedClassesInclusive(changes.classes, impactedClassesResolver))
|
val classMembers = changes.classMembers.map { ClassMembers(it.key, it.value) }
|
||||||
|
val impactedClassMembers = findReachableNodes(classMembers, impactedSymbolsResolver::getImpactedClassMembers)
|
||||||
for ((classId, memberNames) in changes.classMembers) {
|
impactedClassMembers.forEach {
|
||||||
findImpactedClassesInclusive(setOf(classId), impactedClassesResolver).forEach { impactedClassId ->
|
addClassMembers(it.classId, it.memberNames)
|
||||||
addClassMembers(impactedClassId, memberNames)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for ((packageFqName, memberNames) in changes.packageMembers) {
|
// Package members are currently not impacted, so we just copy the original set over
|
||||||
|
changes.packageMembers.forEach { (packageFqName, memberNames) ->
|
||||||
addPackageMembers(packageFqName, memberNames)
|
addPackageMembers(packageFqName, memberNames)
|
||||||
}
|
}
|
||||||
|
}.getResult()
|
||||||
getResult()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getClassIdToSubclassesMap(allClasses: Iterable<AccessibleClassSnapshot>): Map<ClassId, Set<ClassId>> {
|
|
||||||
val classIds: Set<ClassId> = allClasses.mapTo(mutableSetOf()) { it.classId } // Use Set for presence check
|
|
||||||
val classNameToClassId = classIds.associateBy { JvmClassName.byClassId(it) }
|
|
||||||
val classNameToClassIdResolver = { className: JvmClassName -> classNameToClassId[className] }
|
|
||||||
|
|
||||||
val classIdToSubclasses = mutableMapOf<ClassId, MutableSet<ClassId>>()
|
|
||||||
allClasses.forEach { classSnapshot ->
|
|
||||||
// No need to collect supertypes outside the given set of classes (e.g., "java/lang/Object")
|
|
||||||
classSnapshot.getSupertypes(classNameToClassIdResolver).intersect(classIds).forEach { supertype ->
|
|
||||||
classIdToSubclasses.getOrPut(supertype) { mutableSetOf() }.add(classSnapshot.classId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return classIdToSubclasses
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds directly and transitively impacted classes of the given classes. The return set includes both the given classes and the
|
|
||||||
* impacted classes.
|
|
||||||
*/
|
|
||||||
fun findImpactedClassesInclusive(classIds: Set<ClassId>, impactedClassesResolver: (ClassId) -> Set<ClassId>): Set<ClassId> {
|
|
||||||
// Standard Breadth-First Search
|
|
||||||
val visitedAndToVisitClasses = classIds.toMutableSet()
|
|
||||||
val classesToVisit = ArrayDeque(classIds)
|
|
||||||
|
|
||||||
while (classesToVisit.isNotEmpty()) {
|
|
||||||
val classToVisit = classesToVisit.removeFirst()
|
|
||||||
val nextClassesToVisit = impactedClassesResolver.invoke(classToVisit) - visitedAndToVisitClasses
|
|
||||||
visitedAndToVisitClasses.addAll(nextClassesToVisit)
|
|
||||||
classesToVisit.addAll(nextClassesToVisit)
|
|
||||||
}
|
|
||||||
return visitedAndToVisitClasses
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the [ClassId]s of the supertypes of this class (could be empty in some cases).
|
|
||||||
*
|
|
||||||
* @param classIdResolver Resolves the [ClassId] from the [JvmClassName] of a supertype. It may return null if the supertype is outside the
|
|
||||||
* considered set of classes (e.g., "java/lang/Object"). Those supertypes do not need to be included in the returned result.
|
|
||||||
*/
|
|
||||||
internal fun AccessibleClassSnapshot.getSupertypes(classIdResolver: (JvmClassName) -> ClassId?): Set<ClassId> {
|
|
||||||
return when (this) {
|
|
||||||
is RegularKotlinClassSnapshot -> supertypes.mapNotNullTo(mutableSetOf()) { classIdResolver.invoke(it) }
|
|
||||||
is PackageFacadeKotlinClassSnapshot, is MultifileClassKotlinClassSnapshot -> {
|
|
||||||
// These classes may have supertypes (e.g., kotlin/collections/ArraysKt (MULTIFILE_CLASS) extends
|
|
||||||
// kotlin/collections/ArraysKt___ArraysKt (MULTIFILE_CLASS_PART)), but we don't have to use that info during impact analysis
|
|
||||||
// because those inheritors and supertypes should have the same package names, and in package facades only the package names and
|
|
||||||
// member names matter.
|
|
||||||
emptySet()
|
|
||||||
}
|
|
||||||
is JavaClassSnapshot -> supertypes.mapNotNullTo(mutableSetOf()) { classIdResolver.invoke(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -62,7 +62,14 @@ class RegularKotlinClassSnapshot(
|
|||||||
override val classId: ClassId,
|
override val classId: ClassId,
|
||||||
override val classAbiHash: Long,
|
override val classAbiHash: Long,
|
||||||
override val classMemberLevelSnapshot: KotlinClassInfo?,
|
override val classMemberLevelSnapshot: KotlinClassInfo?,
|
||||||
val supertypes: List<JvmClassName>
|
val supertypes: List<JvmClassName>,
|
||||||
|
|
||||||
|
/** Name of the companion object of this class (default is "Companion") iff this class HAS a companion object, or null otherwise. */
|
||||||
|
val companionObjectName: String?,
|
||||||
|
|
||||||
|
/** List of constants defined in this class iff this class IS a companion object, or null otherwise. The list could be empty. */
|
||||||
|
val constantsInCompanionObject: List<String>?
|
||||||
|
|
||||||
) : KotlinClassSnapshot()
|
) : KotlinClassSnapshot()
|
||||||
|
|
||||||
/** [KotlinClassSnapshot] where class kind == [FILE_FACADE] or [MULTIFILE_CLASS_PART]. */
|
/** [KotlinClassSnapshot] where class kind == [FILE_FACADE] or [MULTIFILE_CLASS_PART]. */
|
||||||
|
|||||||
+5
-1
@@ -120,6 +120,8 @@ private object RegularKotlinClassSnapshotExternalizer : DataExternalizer<Regular
|
|||||||
LongExternalizer.save(output, snapshot.classAbiHash)
|
LongExternalizer.save(output, snapshot.classAbiHash)
|
||||||
NullableValueExternalizer(KotlinClassInfoExternalizer).save(output, snapshot.classMemberLevelSnapshot)
|
NullableValueExternalizer(KotlinClassInfoExternalizer).save(output, snapshot.classMemberLevelSnapshot)
|
||||||
ListExternalizer(JvmClassNameExternalizer).save(output, snapshot.supertypes)
|
ListExternalizer(JvmClassNameExternalizer).save(output, snapshot.supertypes)
|
||||||
|
NullableValueExternalizer(StringExternalizer).save(output, snapshot.companionObjectName)
|
||||||
|
NullableValueExternalizer(ListExternalizer(StringExternalizer)).save(output, snapshot.constantsInCompanionObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun read(input: DataInput): RegularKotlinClassSnapshot {
|
override fun read(input: DataInput): RegularKotlinClassSnapshot {
|
||||||
@@ -128,7 +130,9 @@ private object RegularKotlinClassSnapshotExternalizer : DataExternalizer<Regular
|
|||||||
classId = ClassIdExternalizerWithInterning.read(input),
|
classId = ClassIdExternalizerWithInterning.read(input),
|
||||||
classAbiHash = LongExternalizer.read(input),
|
classAbiHash = LongExternalizer.read(input),
|
||||||
classMemberLevelSnapshot = NullableValueExternalizer(KotlinClassInfoExternalizer).read(input),
|
classMemberLevelSnapshot = NullableValueExternalizer(KotlinClassInfoExternalizer).read(input),
|
||||||
supertypes = ListExternalizer(JvmClassNameExternalizerWithInterning).read(input)
|
supertypes = ListExternalizer(JvmClassNameExternalizerWithInterning).read(input),
|
||||||
|
companionObjectName = NullableValueExternalizer(StringExternalizer).read(input),
|
||||||
|
constantsInCompanionObject = NullableValueExternalizer(ListExternalizer(StringExternalizer)).read(input)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-22
@@ -17,6 +17,7 @@ import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotEnable
|
|||||||
import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotEnabled.NotAvailableForNonIncrementalRun
|
import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotEnabled.NotAvailableForNonIncrementalRun
|
||||||
import org.jetbrains.kotlin.incremental.LookupStorage
|
import org.jetbrains.kotlin.incremental.LookupStorage
|
||||||
import org.jetbrains.kotlin.incremental.LookupSymbol
|
import org.jetbrains.kotlin.incremental.LookupSymbol
|
||||||
|
import org.jetbrains.kotlin.incremental.classpathDiff.BreadthFirstSearch.findReachableNodes
|
||||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinker.shrinkClasses
|
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinker.shrinkClasses
|
||||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinker.shrinkClasspath
|
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinker.shrinkClasspath
|
||||||
import org.jetbrains.kotlin.incremental.storage.ListExternalizer
|
import org.jetbrains.kotlin.incremental.storage.ListExternalizer
|
||||||
@@ -24,7 +25,6 @@ import org.jetbrains.kotlin.incremental.storage.LookupSymbolKey
|
|||||||
import org.jetbrains.kotlin.incremental.storage.loadFromFile
|
import org.jetbrains.kotlin.incremental.storage.loadFromFile
|
||||||
import org.jetbrains.kotlin.incremental.storage.saveToFile
|
import org.jetbrains.kotlin.incremental.storage.saveToFile
|
||||||
import org.jetbrains.kotlin.name.ClassId
|
import org.jetbrains.kotlin.name.ClassId
|
||||||
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
|
|
||||||
|
|
||||||
object ClasspathSnapshotShrinker {
|
object ClasspathSnapshotShrinker {
|
||||||
|
|
||||||
@@ -42,7 +42,12 @@ object ClasspathSnapshotShrinker {
|
|||||||
return shrinkClasses(allClasses, lookupSymbols, metrics)
|
return shrinkClasses(allClasses, lookupSymbols, metrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shrinks the given classes by retaining only classes that are referenced by the given lookup symbols. */
|
/**
|
||||||
|
* Shrinks the given classes by retaining only classes that are referenced by the given lookup symbols.
|
||||||
|
*
|
||||||
|
* Note: We need to retain both directly and transitively referenced classes to compute the impact of classpath changes correctly (see
|
||||||
|
* [ClasspathChangesComputer.computeChangedAndImpactedSet]).
|
||||||
|
*/
|
||||||
fun shrinkClasses(
|
fun shrinkClasses(
|
||||||
allClasses: List<AccessibleClassSnapshot>,
|
allClasses: List<AccessibleClassSnapshot>,
|
||||||
lookupSymbols: Collection<LookupSymbolKey>,
|
lookupSymbols: Collection<LookupSymbolKey>,
|
||||||
@@ -57,7 +62,7 @@ object ClasspathSnapshotShrinker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds classes that are referenced by the given lookup symbols.
|
* Finds classes that are *directly* referenced by the given lookup symbols.
|
||||||
*
|
*
|
||||||
* Note: It's okay to over-approximate the result.
|
* Note: It's okay to over-approximate the result.
|
||||||
*/
|
*/
|
||||||
@@ -90,31 +95,19 @@ object ClasspathSnapshotShrinker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds classes that are transitively referenced. For example, if a subclass is referenced, its supertypes will potentially be
|
* Finds classes that are *transitively* referenced from the given classes. For example, if a subclass is referenced, its supertypes
|
||||||
* referenced.
|
* will be transitively referenced.
|
||||||
*
|
*
|
||||||
* The returned list includes the given referenced classes plus the transitively referenced ones.
|
* The returned list is *inclusive* (it contains the given list + the transitively referenced ones).
|
||||||
*/
|
*/
|
||||||
private fun findTransitivelyReferencedClasses(
|
private fun findTransitivelyReferencedClasses(
|
||||||
allClasses: List<AccessibleClassSnapshot>,
|
allClasses: List<AccessibleClassSnapshot>,
|
||||||
referencedClasses: List<AccessibleClassSnapshot>
|
referencedClasses: List<AccessibleClassSnapshot>
|
||||||
): List<AccessibleClassSnapshot> {
|
): List<AccessibleClassSnapshot> {
|
||||||
val classIdToClassSnapshot = allClasses.associateBy { it.classId }
|
val referencedClassIds = referencedClasses.map { it.classId }
|
||||||
val classIds: Set<ClassId> = classIdToClassSnapshot.keys // Use Set for presence check
|
val impactingClassesResolver = AllImpacts.getReverseResolver(allClasses)
|
||||||
val classNameToClassId = classIds.associateBy { JvmClassName.byClassId(it) }
|
val transitivelyReferencedClassIds: Set<ClassId> = /* Must be a Set for the presence check below */
|
||||||
val classNameToClassIdResolver = { className: JvmClassName -> classNameToClassId[className] }
|
findReachableNodes(referencedClassIds, impactingClassesResolver::getImpactingClasses)
|
||||||
|
|
||||||
val supertypesResolver = { classId: ClassId ->
|
|
||||||
// No need to collect supertypes outside the given set of classes (e.g., "java/lang/Object")
|
|
||||||
@Suppress("SimpleRedundantLet")
|
|
||||||
classIdToClassSnapshot[classId]?.let {
|
|
||||||
it.getSupertypes(classNameToClassIdResolver).intersect(classIds)
|
|
||||||
} ?: emptySet()
|
|
||||||
}
|
|
||||||
|
|
||||||
val referencedClassIds = referencedClasses.mapTo(mutableSetOf()) { it.classId }
|
|
||||||
val transitivelyReferencedClassIds: Set<ClassId> = /* Use Set for presence check */
|
|
||||||
ImpactAnalysis.findImpactedClassesInclusive(referencedClassIds, supertypesResolver)
|
|
||||||
|
|
||||||
return allClasses.filter { it.classId in transitivelyReferencedClassIds }
|
return allClasses.filter { it.classId in transitivelyReferencedClassIds }
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -89,7 +89,12 @@ object ClassSnapshotter {
|
|||||||
val classMemberLevelSnapshot = kotlinClassInfo.takeIf { granularity == CLASS_MEMBER_LEVEL }
|
val classMemberLevelSnapshot = kotlinClassInfo.takeIf { granularity == CLASS_MEMBER_LEVEL }
|
||||||
|
|
||||||
return when (kotlinClassInfo.classKind) {
|
return when (kotlinClassInfo.classKind) {
|
||||||
CLASS -> RegularKotlinClassSnapshot(classId, classAbiHash, classMemberLevelSnapshot, classFile.classInfo.supertypes)
|
CLASS -> RegularKotlinClassSnapshot(
|
||||||
|
classId, classAbiHash, classMemberLevelSnapshot,
|
||||||
|
supertypes = classFile.classInfo.supertypes,
|
||||||
|
companionObjectName = kotlinClassInfo.companionObject?.shortClassName?.identifier,
|
||||||
|
constantsInCompanionObject = kotlinClassInfo.constantsInCompanionObject
|
||||||
|
)
|
||||||
FILE_FACADE, MULTIFILE_CLASS_PART -> PackageFacadeKotlinClassSnapshot(
|
FILE_FACADE, MULTIFILE_CLASS_PART -> PackageFacadeKotlinClassSnapshot(
|
||||||
classId, classAbiHash, classMemberLevelSnapshot,
|
classId, classAbiHash, classMemberLevelSnapshot,
|
||||||
packageMemberNames = (kotlinClassInfo.protoData as PackagePartProtoData).getNonPrivateMemberNames()
|
packageMemberNames = (kotlinClassInfo.protoData as PackagePartProtoData).getNonPrivateMemberNames()
|
||||||
|
|||||||
+240
@@ -0,0 +1,240 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2022 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 java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A common interface for all types of impact among classes. For example, if class B extends class A, then class A impacts class B, because
|
||||||
|
* if class A has changed, a source file that references class B will need to be recompiled (even though class B has not changed).
|
||||||
|
*/
|
||||||
|
internal sealed interface Impact {
|
||||||
|
|
||||||
|
/** Provides an [ImpactedSymbolsResolver] to compute the set of [ProgramSymbol]s impacted by a given set of [ProgramSymbol]s. */
|
||||||
|
fun getResolver(allClasses: Iterable<AccessibleClassSnapshot>): ImpactedSymbolsResolver
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an [ImpactingClassesResolver] to compute the set of classes impacting a given set of classes (the reverse of [getResolver]).
|
||||||
|
*/
|
||||||
|
fun getReverseResolver(allClasses: Iterable<AccessibleClassSnapshot>): ImpactingClassesResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the set of [ProgramSymbol]s that are *directly* impacted by a given set of [ProgramSymbol]s.
|
||||||
|
*
|
||||||
|
* The returned set is *inclusive* (it contains the given set + the directly impacted ones).
|
||||||
|
*
|
||||||
|
* This is typically used when computing classpath changes: If class A has changed, and it impacts class B, then a source file that
|
||||||
|
* references class B will need to be recompiled (even though class B has not changed).
|
||||||
|
*/
|
||||||
|
internal interface ImpactedSymbolsResolver {
|
||||||
|
fun getImpactedClasses(classId: ClassId): Set<ClassId>
|
||||||
|
fun getImpactedClassMembers(classMembers: ClassMembers): Set<ClassMembers>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the set of classes *directly* impacting a given set of classes.
|
||||||
|
*
|
||||||
|
* The returned set is *inclusive* (it contains the given set + the directly impacting ones).
|
||||||
|
*
|
||||||
|
* This is typically used when shrinking classpath snapshots: If class A impacts class B, and class B is referenced by a source file, then
|
||||||
|
* class A will need to be retained in the shrunk classpath snapshot because the classpath changes computation will need to see class A
|
||||||
|
* (see [ImpactedSymbolsResolver]).
|
||||||
|
*/
|
||||||
|
internal interface ImpactingClassesResolver {
|
||||||
|
fun getImpactingClasses(classId: ClassId): Set<ClassId>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A composite [Impact] containing all possible concrete impacts. Currently, the types of impact include:
|
||||||
|
* 1. [SupertypesInheritorsImpact]
|
||||||
|
* 2. [ConstantsInCompanionObjectsImpact]
|
||||||
|
*/
|
||||||
|
internal object AllImpacts : Impact {
|
||||||
|
|
||||||
|
private val allImpacts = listOf(SupertypesInheritorsImpact, ConstantsInCompanionObjectsImpact)
|
||||||
|
|
||||||
|
override fun getResolver(allClasses: Iterable<AccessibleClassSnapshot>): ImpactedSymbolsResolver {
|
||||||
|
val resolvers = allImpacts.map { it.getResolver(allClasses) }
|
||||||
|
return object : ImpactedSymbolsResolver {
|
||||||
|
override fun getImpactedClasses(classId: ClassId): Set<ClassId> {
|
||||||
|
return resolvers.flatMapTo(mutableSetOf()) { it.getImpactedClasses(classId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getImpactedClassMembers(classMembers: ClassMembers): Set<ClassMembers> {
|
||||||
|
return resolvers.flatMapTo(mutableSetOf()) { it.getImpactedClassMembers(classMembers) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getReverseResolver(allClasses: Iterable<AccessibleClassSnapshot>): ImpactingClassesResolver {
|
||||||
|
val reverseResolvers = allImpacts.map { it.getReverseResolver(allClasses) }
|
||||||
|
return object : ImpactingClassesResolver {
|
||||||
|
override fun getImpactingClasses(classId: ClassId): Set<ClassId> {
|
||||||
|
return reverseResolvers.flatMapTo(mutableSetOf()) { it.getImpactingClasses(classId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the impact between supertypes and inheritors: If a superclass/interface has changed, its subclasses/sub-interfaces will be
|
||||||
|
* impacted.
|
||||||
|
*/
|
||||||
|
private object SupertypesInheritorsImpact : Impact {
|
||||||
|
|
||||||
|
override fun getResolver(allClasses: Iterable<AccessibleClassSnapshot>): ImpactedSymbolsResolver {
|
||||||
|
val classIdToSubclasses: Map<ClassId, Set<ClassId>> = getClassIdToSubclassesMap(allClasses)
|
||||||
|
return object : ImpactedSymbolsResolver {
|
||||||
|
override fun getImpactedClasses(classId: ClassId): Set<ClassId> {
|
||||||
|
return classIdToSubclasses[classId] ?: emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getImpactedClassMembers(classMembers: ClassMembers): Set<ClassMembers> {
|
||||||
|
return classIdToSubclasses[classMembers.classId]?.let { subclasses ->
|
||||||
|
subclasses.mapTo(mutableSetOf()) { subclass ->
|
||||||
|
ClassMembers(subclass, classMembers.memberNames)
|
||||||
|
}
|
||||||
|
} ?: emptySet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getReverseResolver(allClasses: Iterable<AccessibleClassSnapshot>): ImpactingClassesResolver {
|
||||||
|
val classIdToSupertypesMap: Map<ClassId, Set<ClassId>> = getClassIdToSupertypesMap(allClasses)
|
||||||
|
return object : ImpactingClassesResolver {
|
||||||
|
override fun getImpactingClasses(classId: ClassId): Set<ClassId> {
|
||||||
|
return classIdToSupertypesMap[classId] ?: emptySet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getClassIdToSubclassesMap(allClasses: Iterable<AccessibleClassSnapshot>): Map<ClassId, Set<ClassId>> {
|
||||||
|
val classIdToSubclasses = mutableMapOf<ClassId, MutableSet<ClassId>>()
|
||||||
|
getClassIdToSupertypesMap(allClasses).forEach { (classId, supertypes) ->
|
||||||
|
supertypes.forEach { supertype ->
|
||||||
|
classIdToSubclasses.getOrPut(supertype) { mutableSetOf() }.add(classId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return classIdToSubclasses
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getClassIdToSupertypesMap(allClasses: Iterable<AccessibleClassSnapshot>): Map<ClassId, Set<ClassId>> {
|
||||||
|
val classNameToClassId = allClasses.associate { JvmClassName.byClassId(it.classId) to it.classId }
|
||||||
|
return allClasses.mapNotNull { clazz ->
|
||||||
|
// Find supertypes that are within `allClasses`, we don't care about those outside `allClasses` (e.g., `java/lang/Object`)
|
||||||
|
val supertypes = when (clazz) {
|
||||||
|
is RegularKotlinClassSnapshot -> clazz.supertypes.mapNotNullTo(mutableSetOf()) { classNameToClassId[it] }
|
||||||
|
is PackageFacadeKotlinClassSnapshot, is MultifileClassKotlinClassSnapshot -> {
|
||||||
|
// These classes may have supertypes (e.g., kotlin/collections/ArraysKt (MULTIFILE_CLASS) extends
|
||||||
|
// kotlin/collections/ArraysKt___ArraysKt (MULTIFILE_CLASS_PART)), but we don't have to use that info during impact
|
||||||
|
// analysis because those inheritors and supertypes should have the same package names, and in package facades only the
|
||||||
|
// package names and member names matter.
|
||||||
|
emptySet()
|
||||||
|
}
|
||||||
|
is JavaClassSnapshot -> clazz.supertypes.mapNotNullTo(mutableSetOf()) { classNameToClassId[it] }
|
||||||
|
}
|
||||||
|
if (supertypes.isNotEmpty()) {
|
||||||
|
clazz.classId to supertypes
|
||||||
|
} else null
|
||||||
|
}.toMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the impact between a class and its companion object when the companion object defines some constants.
|
||||||
|
*
|
||||||
|
* Consider the following source file:
|
||||||
|
* class A {
|
||||||
|
* companion object {
|
||||||
|
* const val CONSTANT = 1
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* This source file will compile into 2 .class files:
|
||||||
|
* - `A.Companion.class`'s Kotlin metadata describes the name and type of `CONSTANT` but not its value. Its Java bytecode does not define
|
||||||
|
* the constant.
|
||||||
|
* - `A.class`'s Kotlin metadata does not contain `CONSTANT`. However, its Java bytecode defines the constant as follows:
|
||||||
|
* public static final int CONSTANT = 1;
|
||||||
|
*
|
||||||
|
* Therefore, if the value of the constant has changed in the source file, we will only see a change in the Java bytecode of `A.class`, not
|
||||||
|
* in `A.Companion.class` or in the Kotlin metadata of either class.
|
||||||
|
*
|
||||||
|
* Hence, we will need to detect that `A.CONSTANT` impacts `A.Companion.CONSTANT` because if a source file references
|
||||||
|
* `A.Companion.CONSTANT`, it will need to be recompiled when `A.CONSTANT`'s value in `A.class` has changed (even though `A.Companion.class`
|
||||||
|
* has not changed).
|
||||||
|
*
|
||||||
|
* Note: This corner case only applies to *constants' values* defined in *companion objects* (it does not apply to constants' names and
|
||||||
|
* types, or top-level constants, or constants in non-companion objects, or inline functions).
|
||||||
|
*/
|
||||||
|
private object ConstantsInCompanionObjectsImpact : Impact {
|
||||||
|
|
||||||
|
override fun getResolver(allClasses: Iterable<AccessibleClassSnapshot>): ImpactedSymbolsResolver {
|
||||||
|
val companionObjectToConstants: Map<ClassId, List<String>> = allClasses.mapNotNull { clazz ->
|
||||||
|
(clazz as? RegularKotlinClassSnapshot)?.constantsInCompanionObject?.let { constants ->
|
||||||
|
// We only care about companion objects that define some constants
|
||||||
|
if (constants.isNotEmpty()) {
|
||||||
|
clazz.classId to constants
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
}.toMap()
|
||||||
|
val classToCompanionObject: Map<ClassId, ClassId> = companionObjectToConstants.keys.associateBy { it.parentClassId!! }
|
||||||
|
|
||||||
|
return object : ImpactedSymbolsResolver {
|
||||||
|
override fun getImpactedClasses(classId: ClassId): Set<ClassId> {
|
||||||
|
return setOfNotNull(classToCompanionObject[classId])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getImpactedClassMembers(classMembers: ClassMembers): Set<ClassMembers> {
|
||||||
|
return classToCompanionObject[classMembers.classId]?.let { companionObject ->
|
||||||
|
val constantsInCompanionObject = companionObjectToConstants[companionObject]!!
|
||||||
|
val impactedConstants = classMembers.memberNames.intersect(constantsInCompanionObject.toSet())
|
||||||
|
setOf(ClassMembers(companionObject, impactedConstants))
|
||||||
|
} ?: emptySet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getReverseResolver(allClasses: Iterable<AccessibleClassSnapshot>): ImpactingClassesResolver {
|
||||||
|
val companionObjects: Set<ClassId> = allClasses.mapNotNullTo(mutableSetOf()) { clazz ->
|
||||||
|
(clazz as? RegularKotlinClassSnapshot)?.constantsInCompanionObject?.let { constants ->
|
||||||
|
// We only care about companion objects that define some constants
|
||||||
|
if (constants.isNotEmpty()) clazz.classId else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return object : ImpactingClassesResolver {
|
||||||
|
override fun getImpactingClasses(classId: ClassId): Set<ClassId> {
|
||||||
|
return if (classId in companionObjects) {
|
||||||
|
setOf(classId.parentClassId!!)
|
||||||
|
} else emptySet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object BreadthFirstSearch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the set of nodes that are *transitively* reachable from the given set of nodes.
|
||||||
|
*
|
||||||
|
* The returned set is *inclusive* (it contains the given set + the directly/transitively reachable ones).
|
||||||
|
*/
|
||||||
|
fun <T> findReachableNodes(nodes: Iterable<T>, edgesProvider: (T) -> Iterable<T>): Set<T> {
|
||||||
|
val visitedAndToVisitNodes = nodes.toMutableSet()
|
||||||
|
val nodesToVisit = ArrayDeque(nodes.toSet())
|
||||||
|
|
||||||
|
while (nodesToVisit.isNotEmpty()) {
|
||||||
|
val nodeToVisit = nodesToVisit.removeFirst()
|
||||||
|
val nextNodesToVisit = edgesProvider.invoke(nodeToVisit) - visitedAndToVisitNodes
|
||||||
|
visitedAndToVisitNodes.addAll(nextNodesToVisit)
|
||||||
|
nodesToVisit.addAll(nextNodesToVisit)
|
||||||
|
}
|
||||||
|
return visitedAndToVisitNodes
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -21,6 +21,9 @@ data class ClassMember(val classId: ClassId, val memberName: String) : ProgramSy
|
|||||||
|
|
||||||
data class PackageMember(val packageFqName: FqName, val memberName: String) : ProgramSymbol()
|
data class PackageMember(val packageFqName: FqName, val memberName: String) : ProgramSymbol()
|
||||||
|
|
||||||
|
/** Compact representation for set of [ClassMember]s having the same [ClassId]. */
|
||||||
|
data class ClassMembers(val classId: ClassId, val memberNames: Set<String>)
|
||||||
|
|
||||||
/** Compact representation for a set of [ProgramSymbol]s. */
|
/** Compact representation for a set of [ProgramSymbol]s. */
|
||||||
class ProgramSymbolSet private constructor(
|
class ProgramSymbolSet private constructor(
|
||||||
|
|
||||||
|
|||||||
+3
@@ -217,6 +217,9 @@ class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
|
|||||||
val changes = computeClasspathChanges(File(testDataDir, "KotlinOnly/testConstantsAndInlineFunctions/src"), tmpDir)
|
val changes = computeClasspathChanges(File(testDataDir, "KotlinOnly/testConstantsAndInlineFunctions/src"), tmpDir)
|
||||||
Changes(
|
Changes(
|
||||||
lookupSymbols = setOf(
|
lookupSymbols = setOf(
|
||||||
|
LookupSymbol(name = "constantChangedType", scope = "com.example.SomeClass"),
|
||||||
|
LookupSymbol(name = "constantChangedValue", scope = "com.example.SomeClass"),
|
||||||
|
|
||||||
LookupSymbol(name = "constantChangedType", scope = "com.example.SomeClass.CompanionObject"),
|
LookupSymbol(name = "constantChangedType", scope = "com.example.SomeClass.CompanionObject"),
|
||||||
LookupSymbol(name = "constantChangedValue", scope = "com.example.SomeClass.CompanionObject"),
|
LookupSymbol(name = "constantChangedValue", scope = "com.example.SomeClass.CompanionObject"),
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -12,6 +12,8 @@ import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommo
|
|||||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.CompileUtil.compile
|
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.CompileUtil.compile
|
||||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.CompileUtil.compileAll
|
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.CompileUtil.compileAll
|
||||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.SourceFile.KotlinSourceFile
|
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.SourceFile.KotlinSourceFile
|
||||||
|
import org.jetbrains.kotlin.incremental.storage.fromByteArray
|
||||||
|
import org.jetbrains.kotlin.incremental.storage.toByteArray
|
||||||
import org.jetbrains.kotlin.test.KotlinTestUtils
|
import org.jetbrains.kotlin.test.KotlinTestUtils
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.rules.TemporaryFolder
|
import org.junit.rules.TemporaryFolder
|
||||||
@@ -25,7 +27,11 @@ abstract class ClasspathSnapshotTestCommon {
|
|||||||
|
|
||||||
// Use Gson to compare objects
|
// Use Gson to compare objects
|
||||||
private val gson by lazy { GsonBuilder().setPrettyPrinting().create() }
|
private val gson by lazy { GsonBuilder().setPrettyPrinting().create() }
|
||||||
protected fun Any.toGson(): String = gson.toJson(this)
|
protected fun ClassSnapshot.toGson(): String = gson.toJson(
|
||||||
|
// Serialize and deserialize the object to unset lazy properties' values as they are not essential and can add noise when comparing
|
||||||
|
// objects
|
||||||
|
ClassSnapshotExternalizer.fromByteArray(ClassSnapshotExternalizer.toByteArray(this))
|
||||||
|
)
|
||||||
|
|
||||||
sealed class SourceFile(val baseDir: File, relativePath: String) {
|
sealed class SourceFile(val baseDir: File, relativePath: String) {
|
||||||
val unixStyleRelativePath: String
|
val unixStyleRelativePath: String
|
||||||
|
|||||||
+8
@@ -56,6 +56,14 @@
|
|||||||
"protoData$delegate": {
|
"protoData$delegate": {
|
||||||
"initializer": {},
|
"initializer": {},
|
||||||
"_value": {}
|
"_value": {}
|
||||||
|
},
|
||||||
|
"companionObject$delegate": {
|
||||||
|
"initializer": {},
|
||||||
|
"_value": {}
|
||||||
|
},
|
||||||
|
"constantsInCompanionObject$delegate": {
|
||||||
|
"initializer": {},
|
||||||
|
"_value": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"supertypes": [
|
"supertypes": [
|
||||||
|
|||||||
Reference in New Issue
Block a user