Handle changes to inline functions/property accessors with @JvmNames

If we detect a change in an inline function `foo` with @JvmName
`fooJvmName`, we have two options:
   1. Report that function `foo` has changed
   2. Report that method `fooJvmName` has changed

Similarly, if we detect a change in an inline property accessor with
JvmName `getFoo` of property `foo`, we have two options:
   1. Report that property `foo` has changed
   2. Report that property accessor `getFoo` has changed

The compiler is guaranteed to generate `LookupSymbol`s corresponding to
option 1 when referencing inline functions/property accessors, but it is
not guaranteed to generate `LookupSymbol`s corresponding to option 2.
(Currently the compiler seems to support option 2 for *inline*
functions/property accessors, but that may change.)

Therefore, we will choose option 1 as it is cleaner and safer.

^KT-54144 In progress

Small cleanup in IncrementalCompilerRunner

 - Add comment for closing caches
 - Rename providedChangedFiles to changedFiles
 - Tiny clean up in `performWorkBeforeCompilation`
 - Count directories to delete in debug logs

^KT-53015 In progress

Extract KotlinClassInfo to a separate class

to reduce the size of IncrementalJvmCache and prepare for the next
change.

^KT-54144 In progress

Ignore inline functions that are not found in the bytecode

^KT-54144 In progress

Add unit test for handling `@JvmName`s

Test: KotlinOnlyClasspathChangesComputerTest
             #testFunctionsAndPropertyAccessorsWithJvmNames
^KT-54144 Fixed

Update unit tests for handling `@JvmName`s

In a previous commit, we made a behavior change for inline property
accessors: The existing behavior is that if the implementation of an
inline getter has changed, only usages of the getter will be impacted
but not usages of the setter (and vice versa).

After that previous commit, usages of *both* the getter and setter will
now be impacted (i.e., we might compile slightly more files). This is
because a change to either the getter or the setter will now be
considered a change to the property, which will help simplify our change
analysis.

This commit updates the relevant unit tests to reflect the new behavior.

Test: Updated Incremental*TestGenerated.PureKotlin#testInlinePropertyInClass
      and Incremental*TestGenerated.PureKotlin#testInlinePropertyOnTopLevel

^KT-54144 Fixed
This commit is contained in:
Hung Nguyen
2022-10-13 19:32:14 +01:00
committed by nataliya.valtman
parent e1e07d2094
commit cdbbead157
21 changed files with 515 additions and 366 deletions
@@ -16,6 +16,8 @@
package org.jetbrains.kotlin.incremental
import org.jetbrains.kotlin.incremental.DifferenceCalculatorForClass.Companion.getNonPrivateMembers
import org.jetbrains.kotlin.incremental.DifferenceCalculatorForPackageFacade.Companion.getNonPrivateMembers
import org.jetbrains.kotlin.metadata.ProtoBuf
import org.jetbrains.kotlin.metadata.deserialization.NameResolver
import org.jetbrains.kotlin.name.FqName
@@ -166,7 +168,7 @@ class ChangesCollector {
}
private fun PackagePartProtoData.collectAllFromPackage(isRemoved: Boolean) {
val memberNames = getNonPrivateMemberNames(includeInlineAccessors = true)
val memberNames = getNonPrivateMembers()
if (isRemoved) {
collectRemovedMembers(packageFqName, memberNames)
} else {
@@ -178,14 +180,14 @@ class ChangesCollector {
val classFqName = nameResolver.getClassId(proto.fqName).asSingleFqName()
if (proto.isCompanionObject) {
val memberNames = getNonPrivateMemberNames(includeInlineAccessors = true)
val memberNames = getNonPrivateMembers()
val collectMember = if (isRemoved) this@ChangesCollector::collectRemovedMember else this@ChangesCollector::collectChangedMember
collectMember(classFqName.parent(), classFqName.shortName().asString())
memberNames.forEach { collectMember(classFqName, it) }
} else {
if (!isRemoved && collectAllMembersForNewClass) {
val memberNames = getNonPrivateMemberNames(includeInlineAccessors = true)
val memberNames = getNonPrivateMembers()
memberNames.forEach { this@ChangesCollector.collectChangedMember(classFqName, it) }
}
@@ -23,27 +23,20 @@ import com.intellij.util.io.EnumeratorStringDescriptor
import gnu.trove.THashSet
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.build.GeneratedJvmClass
import org.jetbrains.kotlin.incremental.DifferenceCalculatorForClass.Companion.getNonPrivateMembers
import org.jetbrains.kotlin.incremental.DifferenceCalculatorForPackageFacade.Companion.getNonPrivateMembers
import org.jetbrains.kotlin.incremental.storage.*
import org.jetbrains.kotlin.inline.inlineAccessors
import org.jetbrains.kotlin.inline.inlineFunctionsAndAccessors
import org.jetbrains.kotlin.inline.InlineFunction
import org.jetbrains.kotlin.inline.InlineFunctionOrAccessor
import org.jetbrains.kotlin.inline.InlinePropertyAccessor
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCache
import org.jetbrains.kotlin.load.kotlin.incremental.components.JvmPackagePartProto
import org.jetbrains.kotlin.metadata.ProtoBuf
import org.jetbrains.kotlin.metadata.jvm.deserialization.BitEncoding
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmMemberSignature
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil
import org.jetbrains.kotlin.metadata.jvm.deserialization.ModuleMapping
import org.jetbrains.kotlin.metadata.jvm.serialization.JvmStringTable
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.org.objectweb.asm.*
import org.jetbrains.org.objectweb.asm.ClassReader.SKIP_CODE
import org.jetbrains.org.objectweb.asm.ClassReader.SKIP_DEBUG
import java.io.File
import java.security.MessageDigest
@@ -439,7 +432,7 @@ open class IncrementalJvmCache(
// todo: reuse code with InlineFunctionsMap?
private inner class ConstantsMap(storageFile: File) :
BasicStringMap<LinkedHashMap<String, Any>>(storageFile, LinkedHashMapExternalizer(StringExternalizer, ConstantExternalizer)) {
BasicStringMap<Map<String, Any>>(storageFile, MapExternalizer(StringExternalizer, ConstantValueExternalizer)) {
operator fun contains(className: JvmClassName): Boolean =
className.internalName in storage
@@ -487,7 +480,7 @@ open class IncrementalJvmCache(
storage.remove(className.internalName)
}
override fun dumpValue(value: LinkedHashMap<String, Any>): String =
override fun dumpValue(value: Map<String, Any>): String =
value.dumpMap(Any::toString)
}
@@ -567,7 +560,10 @@ open class IncrementalJvmCache(
}
private inner class InlineFunctionsMap(storageFile: File) :
BasicStringMap<LinkedHashMap<String, Long>>(storageFile, LinkedHashMapExternalizer(StringExternalizer, LongExternalizer)) {
BasicStringMap<Map<InlineFunctionOrAccessor, Long>>(
storageFile,
MapExternalizer(InlineFunctionOrAccessorExternalizer, LongExternalizer)
) {
@Synchronized
fun process(kotlinClassInfo: KotlinClassInfo, changesCollector: ChangesCollector) {
@@ -581,34 +577,36 @@ open class IncrementalJvmCache(
storage.remove(key)
}
for (methodSignature in oldMap.keys + newMap.keys) {
val inlineFunctionOrAccessorName = functionNameBySignature(methodSignature)
// Note: If we detect a change in an inline property accessor (e.g., `getFoo`), we have two options:
// 1. Report that property `foo` has changed (and ignore `getFoo`)
// 2. Report that property accessor `getFoo` has changed (and ignore `foo`)
// Both options are possible (the compiler generates `LookupSymbol`s for both `foo` and `getFoo` when `getFoo` is inlined).
// Currently, we choose option 2 as it is simpler (i.e., even though inline property accessors are not immediate members of
// a class/package, we treat them like immediate members).
changesCollector.collectMemberIfValueWasChanged(
kotlinClassInfo.scopeFqName(),
inlineFunctionOrAccessorName,
oldMap[methodSignature],
newMap[methodSignature]
)
// Note: If we detect a change in an inline function `foo` with @JvmName `fooJvmName`, we have two options:
// 1. Report that function `foo` has changed
// 2. Report that method `fooJvmName` has changed
//
// Similarly, if we detect a change in an inline property accessor with JvmName `getFoo` of property `foo`, we have two options:
// 1. Report that property `foo` has changed
// 2. Report that property accessor `getFoo` has changed
//
// The compiler is guaranteed to generate `LookupSymbol`s corresponding to option 1 when referencing inline functions/property
// accessors, but it is not guaranteed to generate `LookupSymbol`s corresponding to option 2. (Currently the compiler seems to
// support option 2 for *inline* functions/property accessors, but that may change.)
//
// In the following, we will choose option 1 as it is cleaner and safer.
val scope = kotlinClassInfo.scopeFqName()
(oldMap.keys + newMap.keys).forEach {
val name = when (it) {
is InlineFunction -> it.kotlinFunctionName
is InlinePropertyAccessor -> it.propertyName
}
changesCollector.collectMemberIfValueWasChanged(scope, name, oldMap[it], newMap[it])
}
}
// TODO get name in better way instead of using substringBefore
private fun functionNameBySignature(signature: String): String =
signature.substringBefore("(")
@Synchronized
fun remove(className: JvmClassName) {
storage.remove(className.internalName)
}
override fun dumpValue(value: LinkedHashMap<String, Long>): String =
value.dumpMap { java.lang.Long.toHexString(it) }
override fun dumpValue(value: Map<InlineFunctionOrAccessor, Long>): String =
value.mapKeys { it.key.jvmMethodSignature.asString() }.dumpMap { java.lang.Long.toHexString(it) }
}
private fun KotlinClassInfo.scopeFqName() = when (classKind) {
@@ -669,182 +667,3 @@ fun <K : Comparable<K>, V> Map<K, V>.dumpMap(dumpValue: (V) -> String): String =
@TestOnly
fun <T : Comparable<T>> Collection<T>.dumpCollection(): String =
"[${sorted().joinToString(", ", transform = Any::toString)}]"
/**
* @param includeInlineAccessors If `true`, also include inline property accessors in the returned list. This is necessary because property
* accessors are not immediate members of a class/package, and in some cases we treat them like immediate members (see
* [org.jetbrains.kotlin.incremental.IncrementalJvmCache.InlineFunctionsMap.process]). Note that here we deal with *inline* property
* accessors only as [org.jetbrains.kotlin.incremental.IncrementalJvmCache.InlineFunctionsMap.process] is written only to handle *inline*
* property accessors (and functions).
*/
fun ProtoData.getNonPrivateMemberNames(includeInlineAccessors: Boolean): List<String> {
return when (this) {
is ClassProtoData -> {
getNonPrivateMembers() + (if (includeInlineAccessors) {
inlineAccessors(proto.propertyList, nameResolver, excludePrivateAccessors = true).map { it.name }
} else emptyList())
}
is PackagePartProtoData -> {
getNonPrivateMembers() + (if (includeInlineAccessors) {
inlineAccessors(proto.propertyList, nameResolver, excludePrivateAccessors = true).map { it.name }
} else emptyList())
}
}
}
/**
* Minimal information about a Kotlin class to compute recompilation-triggering changes during an incremental run of the `KotlinCompile`
* task (see [IncrementalJvmCache.saveClassToCache]).
*
* It's important that this class contain only the minimal required information, as it will be part of the classpath snapshot of the
* `KotlinCompile` task and the task needs to support compile avoidance. For example, this class should contain public method signatures,
* and should not contain private method signatures, or method implementations.
*/
class KotlinClassInfo constructor(
val classId: ClassId,
val classKind: KotlinClassHeader.Kind,
val classHeaderData: Array<String>, // Can be empty
val classHeaderStrings: Array<String>, // Can be empty
val multifileClassName: String?, // Not null iff classKind == KotlinClassHeader.Kind.MULTIFILE_CLASS_PART
val constantsMap: LinkedHashMap<String, Any>,
val inlineFunctionsAndAccessorsMap: LinkedHashMap<String, Long>
) {
val className: JvmClassName by lazy { JvmClassName.byClassId(classId) }
val protoMapValue: ProtoMapValue by lazy {
ProtoMapValue(
isPackageFacade = classKind != KotlinClassHeader.Kind.CLASS,
BitEncoding.decodeBytes(classHeaderData),
classHeaderStrings
)
}
/**
* The [ProtoData] of this class.
*
* 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.
*/
val protoData: ProtoData by lazy {
check(classKind != KotlinClassHeader.Kind.MULTIFILE_CLASS) {
"Proto data is not available for KotlinClassHeader.Kind.MULTIFILE_CLASS: $classId"
}
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 {
fun createFrom(kotlinClass: LocalFileKotlinClass): KotlinClassInfo {
return createFrom(kotlinClass.classId, kotlinClass.classHeader, kotlinClass.fileContents)
}
fun createFrom(classId: ClassId, classHeader: KotlinClassHeader, classContents: ByteArray): KotlinClassInfo {
val (constants, inlineFunctionsAndAccessors) = getConstantsAndInlineFunctionsOrAccessors(classHeader, classContents)
return KotlinClassInfo(
classId,
classHeader.kind,
classHeader.data ?: emptyArray(),
classHeader.strings ?: emptyArray(),
classHeader.multifileClassName,
constants.mapKeysTo(LinkedHashMap()) { it.key.name },
inlineFunctionsAndAccessors.mapKeysTo(LinkedHashMap()) { it.key.asString() },
)
}
}
}
/**
* Parses the class file only once to get both constants and inline functions/property accessors. This is faster than getting them
* separately in two passes.
*/
private fun getConstantsAndInlineFunctionsOrAccessors(
classHeader: KotlinClassHeader,
classContents: ByteArray
): Pair<Map<JvmMemberSignature.Field, Any>, Map<JvmMemberSignature.Method, Long>> {
val constantsClassVisitor = ConstantsClassVisitor()
val inlineFunctionsAndAccessors = inlineFunctionsAndAccessors(classHeader)
return if (inlineFunctionsAndAccessors.isEmpty()) {
// parsingOptions = (SKIP_CODE, SKIP_DEBUG) as method bodies and debug info are not important for constants
ClassReader(classContents).accept(constantsClassVisitor, SKIP_CODE or SKIP_DEBUG)
Pair(constantsClassVisitor.getResult(), emptyMap())
} else {
val inlineFunctionsAndAccessorsClassVisitor =
InlineFunctionsAndAccessorsClassVisitor(inlineFunctionsAndAccessors.toSet(), constantsClassVisitor)
// parsingOptions must not include (SKIP_CODE, SKIP_DEBUG) as method bodies and debug info (e.g., line numbers) are important for
// inline functions/accessors
ClassReader(classContents).accept(inlineFunctionsAndAccessorsClassVisitor, 0)
Pair(constantsClassVisitor.getResult(), inlineFunctionsAndAccessorsClassVisitor.getResult())
}
}
private class ConstantsClassVisitor : ClassVisitor(Opcodes.API_VERSION) {
private val result = mutableMapOf<JvmMemberSignature.Field, Any>()
override fun visitField(access: Int, name: String, desc: String, signature: String?, value: Any?): FieldVisitor? {
if (access and Opcodes.ACC_PRIVATE == Opcodes.ACC_PRIVATE) return null
val staticFinal = Opcodes.ACC_STATIC or Opcodes.ACC_FINAL
if (value != null && access and staticFinal == staticFinal) {
result[JvmMemberSignature.Field(name, desc)] = value
}
return null
}
fun getResult() = result
}
private class InlineFunctionsAndAccessorsClassVisitor(
private val inlineFunctionsAndAccessors: Set<JvmMemberSignature.Method>,
cv: ConstantsClassVisitor // Note: cv must not override `visitMethod` (it will not be called with the current implementation below)
) : ClassVisitor(Opcodes.API_VERSION, cv) {
private val result = mutableMapOf<JvmMemberSignature.Method, Long>()
private var classVersion: Int? = null
override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array<out String>?) {
super.visit(version, access, name, signature, superName, interfaces)
classVersion = version
}
override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
if (access and Opcodes.ACC_PRIVATE == Opcodes.ACC_PRIVATE) return null
val method = JvmMemberSignature.Method(name, desc)
if (method !in inlineFunctionsAndAccessors) return null
val classWriter = ClassWriter(0)
// The `version` and `name` parameters are important (see KT-38857), the others can be null.
classWriter.visit(/* version */ classVersion!!, /* access */ 0, /* name */ "ClassWithOneMethod", null, null, null)
return object : MethodVisitor(Opcodes.API_VERSION, classWriter.visitMethod(access, name, desc, signature, exceptions)) {
override fun visitEnd() {
result[method] = classWriter.toByteArray().md5()
}
}
}
fun getResult() = result
}
@@ -0,0 +1,206 @@
/*
* 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
import com.intellij.util.io.DataExternalizer
import org.jetbrains.kotlin.incremental.storage.*
import org.jetbrains.kotlin.inline.InlineFunctionOrAccessor
import org.jetbrains.kotlin.inline.inlineFunctionsAndAccessors
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
import org.jetbrains.kotlin.metadata.jvm.deserialization.BitEncoding
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmMemberSignature
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.org.objectweb.asm.*
/**
* Minimal information about a Kotlin class to compute recompilation-triggering changes during an incremental run of the `KotlinCompile`
* task (see [IncrementalJvmCache.saveClassToCache]).
*
* It's important that this class contain only the minimal required information, as it will be part of the classpath snapshot of the
* `KotlinCompile` task and the task needs to support compile avoidance. For example, this class should contain public method signatures,
* and should not contain private method signatures, or method implementations.
*/
class KotlinClassInfo constructor(
val classId: ClassId,
val classKind: KotlinClassHeader.Kind,
val classHeaderData: Array<String>, // Can be empty
val classHeaderStrings: Array<String>, // Can be empty
val multifileClassName: String?, // Not null iff classKind == KotlinClassHeader.Kind.MULTIFILE_CLASS_PART
val constantsMap: Map<String, Any>,
val inlineFunctionsAndAccessorsMap: Map<InlineFunctionOrAccessor, Long>
) {
val className: JvmClassName by lazy { JvmClassName.byClassId(classId) }
val protoMapValue: ProtoMapValue by lazy {
ProtoMapValue(
isPackageFacade = classKind != KotlinClassHeader.Kind.CLASS,
BitEncoding.decodeBytes(classHeaderData),
classHeaderStrings
)
}
/**
* The [ProtoData] of this class.
*
* 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.
*/
val protoData: ProtoData by lazy {
check(classKind != KotlinClassHeader.Kind.MULTIFILE_CLASS) {
"Proto data is not available for KotlinClassHeader.Kind.MULTIFILE_CLASS: $classId"
}
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 {
fun createFrom(kotlinClass: LocalFileKotlinClass): KotlinClassInfo {
return createFrom(kotlinClass.classId, kotlinClass.classHeader, kotlinClass.fileContents)
}
fun createFrom(classId: ClassId, classHeader: KotlinClassHeader, classContents: ByteArray): KotlinClassInfo {
val (constants, inlineFunctionsAndAccessors) = getConstantsAndInlineFunctionsOrAccessors(classHeader, classContents)
return KotlinClassInfo(
classId,
classHeader.kind,
classHeader.data ?: emptyArray(),
classHeader.strings ?: emptyArray(),
classHeader.multifileClassName,
constants.mapKeys { it.key.name },
inlineFunctionsAndAccessors
)
}
}
}
/**
* Parses the class file only once to get both constants and inline functions/property accessors. This is faster than getting them
* separately in two passes.
*/
private fun getConstantsAndInlineFunctionsOrAccessors(
classHeader: KotlinClassHeader,
classContents: ByteArray
): Pair<Map<JvmMemberSignature.Field, Any>, Map<InlineFunctionOrAccessor, Long>> {
val constantsClassVisitor = ConstantsClassVisitor()
val inlineFunctionsAndAccessors = inlineFunctionsAndAccessors(classHeader, excludePrivateMembers = true)
return if (inlineFunctionsAndAccessors.isEmpty()) {
// Handle this case differently to improve performance
// parsingOptions = (SKIP_CODE, SKIP_DEBUG) as method bodies and debug info are not important for constants
ClassReader(classContents).accept(constantsClassVisitor, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG)
Pair(constantsClassVisitor.getResult(), emptyMap())
} else {
val inlineFunctionsAndAccessorsClassVisitor = InlineFunctionsAndAccessorsClassVisitor(
inlineFunctionsAndAccessors.map { it.jvmMethodSignature }.toSet(),
constantsClassVisitor
)
// parsingOptions must not include (SKIP_CODE, SKIP_DEBUG) as method bodies and debug info (e.g., line numbers) are important for
// inline functions/accessors
ClassReader(classContents).accept(inlineFunctionsAndAccessorsClassVisitor, 0)
val constantsMap = constantsClassVisitor.getResult()
val methodHashesMap = inlineFunctionsAndAccessorsClassVisitor.getResult()
val inlineFunctionsAndAccessorsMap = inlineFunctionsAndAccessors.mapNotNull { inline ->
// Note that internal/private inline functions may be removed from the bytecode if code shrinker is used. For example,
// `kotlin-reflect-1.7.20.jar` contains `/kotlin/reflect/jvm/internal/UtilKt.class` in which the internal inline function
// `reflectionCall` appears in the Kotlin metadata (also in the source file), but not in the bytecode.
// When that happens (i.e., when the map lookup below returns null), we will ignore the method. It is safe to ignore because the
// method is not declared in the bytecode and therefore can't be referenced.
methodHashesMap[inline.jvmMethodSignature]?.let { inline to it }
}.toMap()
Pair(constantsMap, inlineFunctionsAndAccessorsMap)
}
}
private class ConstantsClassVisitor : ClassVisitor(Opcodes.API_VERSION) {
private val result = mutableMapOf<JvmMemberSignature.Field, Any>()
override fun visitField(access: Int, name: String, desc: String, signature: String?, value: Any?): FieldVisitor? {
if (access and Opcodes.ACC_PRIVATE == Opcodes.ACC_PRIVATE) return null
val staticFinal = Opcodes.ACC_STATIC or Opcodes.ACC_FINAL
if (value != null && access and staticFinal == staticFinal) {
result[JvmMemberSignature.Field(name, desc)] = value
}
return null
}
fun getResult() = result
}
private class InlineFunctionsAndAccessorsClassVisitor(
private val inlineFunctionsAndAccessors: Set<JvmMemberSignature.Method>,
cv: ConstantsClassVisitor // Note: cv must not override `visitMethod` (it will not be called with the current implementation below)
) : ClassVisitor(Opcodes.API_VERSION, cv) {
private val result = mutableMapOf<JvmMemberSignature.Method, Long>()
private var classVersion: Int? = null
override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array<out String>?) {
super.visit(version, access, name, signature, superName, interfaces)
classVersion = version
}
override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
// Note: Do not filter out private methods here because a *public* inline function may actually have a *private* corresponding JVM
// method in the bytecode (see `InlineOnlyKt.isInlineOnlyPrivateInBytecode`).
// Just filter the methods based on the given `inlineFunctionsAndAccessors` set.
val method = JvmMemberSignature.Method(name, desc)
if (method !in inlineFunctionsAndAccessors) return null
val classWriter = ClassWriter(0)
// The `version` and `name` parameters are important (see KT-38857), the others can be null.
classWriter.visit(/* version */ classVersion!!, /* access */ 0, /* name */ "ClassWithOneMethod", null, null, null)
return object : MethodVisitor(Opcodes.API_VERSION, classWriter.visitMethod(access, name, desc, signature, exceptions)) {
override fun visitEnd() {
result[method] = classWriter.toByteArray().md5()
}
}
}
fun getResult() = result
}
/**
* [DataExternalizer] for the value of a Kotlin constant.
*
* The constants' values are provided by ASM (see the javadoc of [ConstantsClassVisitor.visitField]), so their types can only be the
* following: Integer, Long, Float, Double, String. (Boolean constants have Integer (0, 1) values in ASM.)
*/
object ConstantValueExternalizer : DataExternalizer<Any> by DelegateDataExternalizer(
listOf(
java.lang.Integer::class.java,
java.lang.Long::class.java,
java.lang.Float::class.java,
java.lang.Double::class.java,
java.lang.String::class.java
),
listOf(IntExternalizer, LongExternalizer, FloatExternalizer, DoubleExternalizer, StringExternalizer)
)
@@ -22,6 +22,10 @@ import com.intellij.util.io.DataExternalizer
import com.intellij.util.io.EnumeratorStringDescriptor
import com.intellij.util.io.IOUtil
import com.intellij.util.io.KeyDescriptor
import org.jetbrains.kotlin.inline.InlineFunction
import org.jetbrains.kotlin.inline.InlineFunctionOrAccessor
import org.jetbrains.kotlin.inline.InlinePropertyAccessor
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmMemberSignature
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
@@ -165,53 +169,6 @@ object StringToLongMapExternalizer : StringMapExternalizer<Long>() {
}
}
/** [DataExternalizer] for a Kotlin constant. */
object ConstantExternalizer : DataExternalizer<Any> {
override fun save(output: DataOutput, value: Any) {
when (value) {
is Int -> {
output.writeByte(Kind.INT.ordinal)
output.writeInt(value)
}
is Float -> {
output.writeByte(Kind.FLOAT.ordinal)
output.writeFloat(value)
}
is Long -> {
output.writeByte(Kind.LONG.ordinal)
output.writeLong(value)
}
is Double -> {
output.writeByte(Kind.DOUBLE.ordinal)
output.writeDouble(value)
}
is String -> {
output.writeByte(Kind.STRING.ordinal)
output.writeString(value)
}
else -> throw IllegalStateException("Unexpected constant class: ${value::class.java}")
}
}
override fun read(input: DataInput): Any {
return when (Kind.values()[input.readByte().toInt()]) {
Kind.INT -> input.readInt()
Kind.FLOAT -> input.readFloat()
Kind.LONG -> input.readLong()
Kind.DOUBLE -> input.readDouble()
Kind.STRING -> input.readString()
}
}
// The constants' values are provided by ASM, so their types can only be the following.
// See https://asm.ow2.io/javadoc/org/objectweb/asm/ClassVisitor.html#visitField(int,java.lang.String,java.lang.String,java.lang.String,java.lang.Object)
// (Note: Boolean constants have Integer (0, 1) values in ASM.)
private enum class Kind {
INT, FLOAT, LONG, DOUBLE, STRING
}
}
fun <T> DataExternalizer<T>.saveToFile(file: File, value: T) {
return DataOutputStream(FileOutputStream(file).buffered()).use {
save(it, value)
@@ -248,6 +205,16 @@ object LongExternalizer : DataExternalizer<Long> {
override fun read(input: DataInput): Long = input.readLong()
}
object FloatExternalizer : DataExternalizer<Float> {
override fun save(output: DataOutput, value: Float) = output.writeFloat(value)
override fun read(input: DataInput): Float = input.readFloat()
}
object DoubleExternalizer : DataExternalizer<Double> {
override fun save(output: DataOutput, value: Double) = output.writeDouble(value)
override fun read(input: DataInput): Double = input.readDouble()
}
object StringExternalizer : DataExternalizer<String> {
override fun save(output: DataOutput, value: String) = IOUtil.writeString(value, output)
override fun read(input: DataInput): String = IOUtil.readString(input)
@@ -268,6 +235,33 @@ object PathStringDescriptor : EnumeratorStringDescriptor() {
}
}
/** [DataExternalizer] that delegates to another [DataExternalizer] depending on the type of the object to externalize. */
class DelegateDataExternalizer<T>(
val types: List<Class<out T>>,
val typesExternalizers: List<DataExternalizer<out T>>
) : DataExternalizer<T> {
init {
check(types.size == typesExternalizers.size)
check(types.size < Byte.MAX_VALUE) // We will writeByte(index), so we need lastIndex (types.size - 1) <= Byte.MAX_VALUE
}
override fun save(output: DataOutput, objectToExternalize: T) {
val type = types.single { it.isAssignableFrom(objectToExternalize!!::class.java) }
val typeIndex = types.indexOf(type)
output.writeByte(typeIndex)
@Suppress("UNCHECKED_CAST")
(typesExternalizers[typeIndex] as DataExternalizer<T>).save(output, objectToExternalize)
}
override fun read(input: DataInput): T {
val typeIndex = input.readByte().toInt()
@Suppress("UNCHECKED_CAST")
return typesExternalizers[typeIndex].read(input) as T
}
}
/**
* [DataExternalizer] for a [Collection].
*
@@ -401,3 +395,53 @@ class LinkedHashMapExternalizer<K, V>(
keyExternalizer: DataExternalizer<K>,
valueExternalizer: DataExternalizer<V>
) : MapExternalizer<K, V, LinkedHashMap<K, V>>(keyExternalizer, valueExternalizer, { size -> LinkedHashMap(size) })
object JvmMethodSignatureExternalizer : DataExternalizer<JvmMemberSignature.Method> {
override fun save(output: DataOutput, method: JvmMemberSignature.Method) {
StringExternalizer.save(output, method.name)
StringExternalizer.save(output, method.desc)
}
override fun read(input: DataInput): JvmMemberSignature.Method {
return JvmMemberSignature.Method(
name = StringExternalizer.read(input),
desc = StringExternalizer.read(input)
)
}
}
object InlineFunctionOrAccessorExternalizer : DataExternalizer<InlineFunctionOrAccessor> by DelegateDataExternalizer(
types = listOf(InlineFunction::class.java, InlinePropertyAccessor::class.java),
typesExternalizers = listOf(InlineFunctionExternalizer, InlinePropertyAccessorExternalizer)
)
private object InlineFunctionExternalizer : DataExternalizer<InlineFunction> {
override fun save(output: DataOutput, function: InlineFunction) {
JvmMethodSignatureExternalizer.save(output, function.jvmMethodSignature)
StringExternalizer.save(output, function.kotlinFunctionName)
}
override fun read(input: DataInput): InlineFunction {
return InlineFunction(
jvmMethodSignature = JvmMethodSignatureExternalizer.read(input),
kotlinFunctionName = StringExternalizer.read(input)
)
}
}
private object InlinePropertyAccessorExternalizer : DataExternalizer<InlinePropertyAccessor> {
override fun save(output: DataOutput, accessor: InlinePropertyAccessor) {
JvmMethodSignatureExternalizer.save(output, accessor.jvmMethodSignature)
StringExternalizer.save(output, accessor.propertyName)
}
override fun read(input: DataInput): InlinePropertyAccessor {
return InlinePropertyAccessor(
jvmMethodSignature = JvmMethodSignatureExternalizer.read(input),
propertyName = StringExternalizer.read(input)
)
}
}