org.jetbrains.kotlin.incremental.protoDifferenceUtils.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of kotlin-compiler-embeddable Show documentation
Show all versions of kotlin-compiler-embeddable Show documentation
the Kotlin compiler embeddable
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.incremental
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.incremental.ProtoCompareGenerated.ProtoBufClassKind
import org.jetbrains.kotlin.incremental.ProtoCompareGenerated.ProtoBufPackageKind
import org.jetbrains.kotlin.incremental.storage.ProtoMapValue
import org.jetbrains.kotlin.metadata.ProtoBuf
import org.jetbrains.kotlin.metadata.ProtoBuf.Type
import org.jetbrains.kotlin.metadata.deserialization.Flags
import org.jetbrains.kotlin.metadata.deserialization.NameResolver
import org.jetbrains.kotlin.metadata.deserialization.TypeTable
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.protobuf.MessageLite
import org.jetbrains.kotlin.serialization.deserialization.ProtoEnumFlags
import org.jetbrains.kotlin.serialization.deserialization.descriptorVisibility
import org.jetbrains.kotlin.serialization.deserialization.getClassId
data class Difference(
val isClassAffected: Boolean = false,
val areSubclassesAffected: Boolean = false,
val changedMembersNames: Set = emptySet(),
val changedSupertypes: Set = emptySet()
)
sealed class ProtoData
data class ClassProtoData(val proto: ProtoBuf.Class, val nameResolver: NameResolver) : ProtoData()
data class PackagePartProtoData(val proto: ProtoBuf.Package, val nameResolver: NameResolver, val packageFqName: FqName) : ProtoData()
fun ProtoMapValue.toProtoData(packageFqName: FqName): ProtoData =
if (isPackageFacade) {
val (nameResolver, packageProto) = JvmProtoBufUtil.readPackageDataFrom(bytes, strings)
PackagePartProtoData(packageProto, nameResolver, packageFqName)
} else {
val (nameResolver, classProto) = JvmProtoBufUtil.readClassDataFrom(bytes, strings)
ClassProtoData(classProto, nameResolver)
}
internal val MessageLite.isPrivate: Boolean
get() {
val visibility = when (this) {
is ProtoBuf.Constructor -> Flags.VISIBILITY.get(flags)
is ProtoBuf.Function -> Flags.VISIBILITY.get(flags)
is ProtoBuf.Property -> Flags.VISIBILITY.get(flags)
is ProtoBuf.TypeAlias -> Flags.VISIBILITY.get(flags)
is ProtoBuf.EnumEntry -> return false // EnumEntry doesn't have a visibility flag
else -> error("Unknown message: $this")
}
return DescriptorVisibilities.isPrivate(ProtoEnumFlags.descriptorVisibility(visibility))
}
private fun MessageLite.name(nameResolver: NameResolver): String {
return when (this) {
is ProtoBuf.Constructor -> ""
is ProtoBuf.Function -> nameResolver.getString(name)
is ProtoBuf.Property -> nameResolver.getString(name)
is ProtoBuf.TypeAlias -> nameResolver.getString(name)
is ProtoBuf.EnumEntry -> nameResolver.getString(name)
else -> error("Unknown message: $this")
}
}
internal fun List.names(nameResolver: NameResolver): List = map { it.name(nameResolver) }
abstract class DifferenceCalculator {
protected abstract val compareObject: ProtoCompareGenerated
abstract fun difference(): Difference
protected fun calcDifferenceForMembers(oldList: List, newList: List): Collection {
val result = hashSetOf()
val oldMap =
oldList.groupBy {
it.getHashCode(
compareObject::oldGetIndexOfString,
compareObject::oldGetIndexOfClassId,
compareObject::oldGetTypeById
)
}
val newMap =
newList.groupBy {
it.getHashCode(
compareObject::newGetIndexOfString,
compareObject::newGetIndexOfClassId,
compareObject::newGetTypeById
)
}
val hashes = oldMap.keys + newMap.keys
for (hash in hashes) {
val oldMembers = oldMap[hash]
val newMembers = newMap[hash]
val differentMembers = when {
newMembers == null -> oldMembers!!.names(compareObject.oldNameResolver)
oldMembers == null -> newMembers.names(compareObject.newNameResolver)
else -> calcDifferenceForEqualHashes(oldMembers, newMembers)
}
result.addAll(differentMembers)
}
return result
}
private fun calcDifferenceForEqualHashes(
oldList: List,
newList: List
): Collection {
val result = hashSetOf()
val newSet = HashSet(newList)
oldList.forEach { oldMember ->
val newMember = newSet.firstOrNull { compareObject.checkEquals(oldMember, it) }
if (newMember != null) {
newSet.remove(newMember)
} else {
result.add(oldMember.name(compareObject.oldNameResolver))
}
}
newSet.forEach { newMember ->
result.add(newMember.name(compareObject.newNameResolver))
}
return result
}
protected fun calcDifferenceForNames(
oldList: List,
newList: List
): Collection {
val oldNames = oldList.map { compareObject.oldNameResolver.getString(it) }.toSet()
val newNames = newList.map { compareObject.newNameResolver.getString(it) }.toSet()
return oldNames.union(newNames) - oldNames.intersect(newNames)
}
private fun MessageLite.getHashCode(stringIndexes: (Int) -> Int, fqNameIndexes: (Int) -> Int, typeTable: (Int) -> ProtoBuf.Type): Int {
return when (this) {
is ProtoBuf.Constructor -> hashCode(stringIndexes, fqNameIndexes, typeTable)
is ProtoBuf.Function -> hashCode(stringIndexes, fqNameIndexes, typeTable)
is ProtoBuf.Property -> hashCode(stringIndexes, fqNameIndexes, typeTable)
is ProtoBuf.TypeAlias -> hashCode(stringIndexes, fqNameIndexes, typeTable)
else -> error("Unknown message: $this")
}
}
private fun ProtoCompareGenerated.checkEquals(old: MessageLite, new: MessageLite): Boolean {
return when {
old is ProtoBuf.Constructor && new is ProtoBuf.Constructor -> checkEquals(old, new)
old is ProtoBuf.Function && new is ProtoBuf.Function -> checkEquals(old, new)
old is ProtoBuf.Property && new is ProtoBuf.Property -> checkEquals(old, new)
old is ProtoBuf.TypeAlias && new is ProtoBuf.TypeAlias -> checkEquals(old, new)
else -> error("Unknown message: $this")
}
}
}
class DifferenceCalculatorForClass(
private val oldData: ClassProtoData,
private val newData: ClassProtoData
) : DifferenceCalculator() {
override val compareObject = ProtoCompareGenerated(
oldNameResolver = oldData.nameResolver,
newNameResolver = newData.nameResolver,
oldTypeTable = oldData.proto.typeTableOrNull,
newTypeTable = newData.proto.typeTableOrNull
)
override fun difference(): Difference {
val (oldProto, oldNameResolver) = oldData
val (newProto, newNameResolver) = newData
val diff = compareObject.difference(oldProto, newProto)
var isClassAffected = false
var areSubclassesAffected = false
val changedSupertypes = HashSet()
val names = hashSetOf()
val classIsSealed = newProto.isSealed && oldProto.isSealed
fun Int.oldToNames() = names.add(oldNameResolver.getString(this))
fun Int.newToNames() = names.add(newNameResolver.getString(this))
fun calcDifferenceForNonPrivateMembers(members: (ProtoBuf.Class) -> List): Collection {
val oldMembers = members(oldProto).filterNot { it.isPrivate }
val newMembers = members(newProto).filterNot { it.isPrivate }
return calcDifferenceForMembers(oldMembers, newMembers)
}
for (kind in diff) {
when (kind!!) {
ProtoBufClassKind.COMPANION_OBJECT_NAME -> {
if (oldProto.hasCompanionObjectName()) oldProto.companionObjectName.oldToNames()
if (newProto.hasCompanionObjectName()) newProto.companionObjectName.newToNames()
isClassAffected = true
}
ProtoBufClassKind.NESTED_CLASS_NAME_LIST -> {
if (classIsSealed) {
// when class is sealed, adding an implementation can break exhaustive when expressions
// the workaround is to recompile all class usages
isClassAffected = true
}
names.addAll(calcDifferenceForNames(oldProto.nestedClassNameList, newProto.nestedClassNameList))
}
ProtoBufClassKind.CONSTRUCTOR_LIST -> {
val differentNonPrivateConstructors = calcDifferenceForNonPrivateMembers(ProtoBuf.Class::getConstructorList)
isClassAffected = isClassAffected || differentNonPrivateConstructors.isNotEmpty()
}
ProtoBufClassKind.FUNCTION_LIST ->
names.addAll(calcDifferenceForNonPrivateMembers(ProtoBuf.Class::getFunctionList))
ProtoBufClassKind.PROPERTY_LIST ->
names.addAll(calcDifferenceForNonPrivateMembers(ProtoBuf.Class::getPropertyList))
ProtoBufClassKind.TYPE_ALIAS_LIST ->
names.addAll(calcDifferenceForNonPrivateMembers(ProtoBuf.Class::getTypeAliasList))
ProtoBufClassKind.ENUM_ENTRY_LIST -> {
isClassAffected = true
}
ProtoBufClassKind.SEALED_SUBCLASS_FQ_NAME_LIST -> {
isClassAffected = true
// Subclasses are considered to be affected to fix the case where
// an implementation is added to an nth-level (n > 1) sealed class.
// In case of the following hierarchy:
// Base <- Intermediate <- Impl
// the change of the SEALED_SUBCLASS_FQ_NAME_LIST will be detected in the Intermediate,
// but there can be usages, that should be rebuilt, without direct references to the Intermediate:
// when (x as Base) { is Impl -> ... }
areSubclassesAffected = true
}
ProtoBufClassKind.VERSION_REQUIREMENT_LIST,
ProtoBufClassKind.VERSION_REQUIREMENT_TABLE -> {
// TODO
}
ProtoBufClassKind.FLAGS,
ProtoBufClassKind.FQ_NAME,
ProtoBufClassKind.TYPE_PARAMETER_LIST,
ProtoBufClassKind.JS_EXT_CLASS_ANNOTATION_LIST -> {
isClassAffected = true
areSubclassesAffected = true
}
ProtoBufClassKind.SUPERTYPE_LIST,
ProtoBufClassKind.SUPERTYPE_ID_LIST -> {
isClassAffected = true
areSubclassesAffected = true
val oldTypeTable = oldProto.typeTableOrNull?.let { TypeTable(it) }
val newTypeTable = newProto.typeTableOrNull?.let { TypeTable(it) }
fun getSupertypes(supertypeList: List, nameResolver: NameResolver): List {
return supertypeList.map { nameResolver.getClassId(it.className).asSingleFqName() }
}
fun getSupertypesById(supertypeIdList: List, typeTable: TypeTable?, nameResolver: NameResolver): List {
return supertypeIdList.mapNotNull { id ->
typeTable?.get(id)?.className?.let {
nameResolver.getClassId(it).asSingleFqName()
}
}
}
val oldSupertypes = getSupertypes(oldProto.supertypeList, oldNameResolver)
val newSupertypes = getSupertypes(newProto.supertypeList, newNameResolver)
val oldSupertypesById = getSupertypesById(
oldProto.supertypeIdList,
oldTypeTable,
oldNameResolver
)
val newSupertypesById = getSupertypesById(
newProto.supertypeIdList,
newTypeTable,
newNameResolver
)
val changed = (oldSupertypes union newSupertypes) subtract (oldSupertypes intersect newSupertypes)
val changedById = (oldSupertypesById union newSupertypesById) subtract (oldSupertypesById intersect newSupertypesById)
val elements: Set = changed + changedById
changedSupertypes.addAll(elements)
}
ProtoBufClassKind.JVM_EXT_CLASS_MODULE_NAME,
ProtoBufClassKind.JS_EXT_CLASS_CONTAINING_FILE_ID -> {
// TODO
}
ProtoBufClassKind.JVM_EXT_CLASS_LOCAL_VARIABLE_LIST -> {
// Not affected, local variables are not accessible outside of a file
}
ProtoBufClassKind.JAVA_EXT_IS_PACKAGE_PRIVATE_CLASS -> {
isClassAffected = true
areSubclassesAffected = true
}
ProtoBufClassKind.BUILT_INS_EXT_CLASS_ANNOTATION_LIST -> {
isClassAffected = true
}
ProtoBufClassKind.JVM_EXT_ANONYMOUS_OBJECT_ORIGIN_NAME -> {
// Not affected, this extension is not used in the compiler
}
ProtoBufClassKind.KLIB_EXT_CLASS_ANNOTATION_LIST -> {
isClassAffected = true
areSubclassesAffected = true
}
ProtoBufClassKind.JVM_EXT_JVM_CLASS_FLAGS -> {
isClassAffected = true
areSubclassesAffected = true
}
ProtoBufClassKind.INLINE_CLASS_UNDERLYING_PROPERTY_NAME,
ProtoBufClassKind.INLINE_CLASS_UNDERLYING_TYPE,
ProtoBufClassKind.INLINE_CLASS_UNDERLYING_TYPE_ID,
ProtoBufClassKind.MULTI_FIELD_VALUE_CLASS_UNDERLYING_NAME_LIST,
ProtoBufClassKind.MULTI_FIELD_VALUE_CLASS_UNDERLYING_TYPE_LIST,
ProtoBufClassKind.MULTI_FIELD_VALUE_CLASS_UNDERLYING_TYPE_ID_LIST -> {
isClassAffected = true
}
ProtoBufClassKind.CONTEXT_RECEIVER_TYPE_LIST,
ProtoBufClassKind.CONTEXT_RECEIVER_TYPE_ID_LIST -> {
isClassAffected = true
areSubclassesAffected = true
}
}
}
return Difference(isClassAffected, areSubclassesAffected, names, changedSupertypes)
}
companion object {
fun ClassProtoData.getNonPrivateMembers(): List {
val membersResolvers: List<(ProtoBuf.Class) -> List> = listOf(
// This list must match the logic in `DifferenceCalculatorForClass.difference`
// TODO: Consider adding COMPANION_OBJECT_NAME and NESTED_CLASS_NAME_LIST as they are also members of a class (see
// `DifferenceCalculatorForClass.difference`)
ProtoBuf.Class::getConstructorList,
ProtoBuf.Class::getFunctionList,
ProtoBuf.Class::getPropertyList,
ProtoBuf.Class::getTypeAliasList,
ProtoBuf.Class::getEnumEntryList
)
return membersResolvers.flatMap { membersResolver ->
membersResolver(proto).filterNot { it.isPrivate }.names(nameResolver)
}
}
}
}
class DifferenceCalculatorForPackageFacade(
private val oldData: PackagePartProtoData,
private val newData: PackagePartProtoData
) : DifferenceCalculator() {
override val compareObject = ProtoCompareGenerated(
oldNameResolver = oldData.nameResolver,
newNameResolver = newData.nameResolver,
oldTypeTable = oldData.proto.typeTableOrNull,
newTypeTable = newData.proto.typeTableOrNull
)
override fun difference(): Difference {
val oldProto = oldData.proto
val newProto = newData.proto
val diff = compareObject.difference(oldProto, newProto)
val names = hashSetOf()
fun calcDifferenceForNonPrivateMembers(members: (ProtoBuf.Package) -> List): Collection {
val oldMembers = members(oldProto).filterNot { it.isPrivate }
val newMembers = members(newProto).filterNot { it.isPrivate }
return calcDifferenceForMembers(oldMembers, newMembers)
}
for (kind in diff) {
@Suppress("UNUSED_VARIABLE") // To make this 'when' exhaustive
val unused: Any = when (kind!!) {
ProtoBufPackageKind.FUNCTION_LIST ->
names.addAll(calcDifferenceForNonPrivateMembers(ProtoBuf.Package::getFunctionList))
ProtoBufPackageKind.PROPERTY_LIST ->
names.addAll(calcDifferenceForNonPrivateMembers(ProtoBuf.Package::getPropertyList))
ProtoBufPackageKind.TYPE_ALIAS_LIST ->
names.addAll(calcDifferenceForNonPrivateMembers(ProtoBuf.Package::getTypeAliasList))
ProtoBufPackageKind.VERSION_REQUIREMENT_TABLE,
ProtoBufPackageKind.JVM_EXT_PACKAGE_MODULE_NAME,
ProtoBufPackageKind.JS_EXT_PACKAGE_FQ_NAME -> {
// TODO
}
ProtoBufPackageKind.JVM_EXT_PACKAGE_LOCAL_VARIABLE_LIST -> {
// Not affected, local variables are not accessible outside of a file
}
ProtoBufPackageKind.BUILT_INS_EXT_PACKAGE_FQ_NAME -> {
// Not affected
}
ProtoBufPackageKind.KLIB_EXT_PACKAGE_FQ_NAME -> {
// Not affected
}
}
}
return Difference(changedMembersNames = names)
}
companion object {
fun PackagePartProtoData.getNonPrivateMembers(): List {
val membersResolvers: List<(ProtoBuf.Package) -> List> = listOf(
// This list must match the logic in `DifferenceCalculatorForPackageFacade.difference`
ProtoBuf.Package::getFunctionList,
ProtoBuf.Package::getPropertyList,
ProtoBuf.Package::getTypeAliasList
)
return membersResolvers.flatMap { membersResolver ->
membersResolver(proto).filterNot { it.isPrivate }.names(nameResolver)
}
}
}
}
private val ProtoBuf.Class.isSealed: Boolean
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?
get() = if (hasTypeTable()) typeTable else null
val ProtoBuf.Package.typeTableOrNull: ProtoBuf.TypeTable?
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 {
return proto.propertyList
.filter { Flags.IS_CONST.get(it.flags) }
.map { nameResolver.getString(it.name) }
}