All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jetbrains.kotlin.incremental.protoDifferenceUtils.kt Maven / Gradle / Ivy

There is a newer version: 2.1.0
Show newest version
/*
 * 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) }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy