KT-64808 [stubs] Materialize all synthethic declarations in Kotlin Stubs

Here is the reasoning behind that change:

Historically, all the declarations generated by compiler plugins
were marked with `SYNTHESIZED` member kind in Kotlin Metadata
by K1 compiler.

In K1 IDE, descriptors were deserialized directly from the Kotlin
Metadata, and they saw all the generated declarations from it.

In the stubs, however, such declarations were not materialized at all.
It caused no troubles, since K1 IDE relied on descriptors to get the
essential resolution information in completion and other subsystems.
So, the resolution of members from jars processed
by compiler plugins (e.g. `kotlinx-serialization-json`) was mostly fine.

In K2 IDE, however, we use stubs to "deserialize" FIR declarations from
them, to later create `KtSymbol`s upon that FIR.

If we see a library file which was processed by compiler plugins, we
build stubs for it based on the Kotlin Metadata, and then use stubs to
create FIR.
But if stubs do not contain information about the generated
declarations,
then the resulting FIR will also not contain such declarations.

In the end, the K2 IDE would also be blind to such declarations, since
there is no other way to retrieve them from the library jar.

By meterializing all the synthethic declarations in stubs (with some
minor exceptions for data classes), we would avoid such problems,
and the resolve of such declarations from compiler jars in
K2 IDE would become possible.

Important note: currently, the Kotlin Metadata format between K1 and K2
frontends is not 100% the same; most notably, in K2, the generated
declarations are marked as regular declarations,
not as `SYNTHESIZED` ones.

Hence, if you compile a jar with the current version of K2 frontend,
then all the generated declarations would have a regular `DECLARATION`
origin, and there would be no such issues as described above.

There are two notes here:

1. K2 IDE still has to support the jars compiled by the K1 compiler,
so it still makes sense to alter the stubs and make the generated
declarations visible.
2. The issue about the different stub formats has been reported
(see KT-64924), and might be resolved in the future.
So it is possible that K2 frontend's metadata will also start
marking the generated declarations as `SYNTHESIZED`.

^KT-64808 Fixed
This commit is contained in:
Roman Golyshev
2024-01-17 13:08:32 +01:00
committed by teamcity
parent 59b74f10aa
commit 47aa6392bf
4 changed files with 45 additions and 21 deletions
@@ -12,18 +12,14 @@ import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.contracts.description.ContractProviderKey import org.jetbrains.kotlin.contracts.description.ContractProviderKey
import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.SpecialNames
import org.jetbrains.kotlin.psi.psiUtil.quoteIfNeeded import org.jetbrains.kotlin.psi.psiUtil.quoteIfNeeded
import org.jetbrains.kotlin.renderer.DescriptorRenderer import org.jetbrains.kotlin.renderer.DescriptorRenderer
import org.jetbrains.kotlin.renderer.DescriptorRendererModifier import org.jetbrains.kotlin.renderer.DescriptorRendererModifier
import org.jetbrains.kotlin.renderer.DescriptorRendererOptions import org.jetbrains.kotlin.renderer.DescriptorRendererOptions
import org.jetbrains.kotlin.renderer.render import org.jetbrains.kotlin.renderer.render
import org.jetbrains.kotlin.resolve.DataClassDescriptorResolver
import org.jetbrains.kotlin.resolve.DescriptorUtils.isEnumEntry import org.jetbrains.kotlin.resolve.DescriptorUtils.isEnumEntry
import org.jetbrains.kotlin.resolve.descriptorUtil.secondaryConstructors import org.jetbrains.kotlin.resolve.descriptorUtil.secondaryConstructors
import org.jetbrains.kotlin.types.isFlexible import org.jetbrains.kotlin.types.isFlexible
import org.jetbrains.kotlin.util.OperatorNameConventions
import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly
private const val DECOMPILED_CODE_COMMENT = "/* compiled code */" private const val DECOMPILED_CODE_COMMENT = "/* compiled code */"
@@ -44,18 +40,35 @@ fun DescriptorRendererOptions.defaultDecompilerRendererOptions() {
propertyConstantRenderer = { _ -> COMPILED_DEFAULT_INITIALIZER } propertyConstantRenderer = { _ -> COMPILED_DEFAULT_INITIALIZER }
} }
/**
* @see org.jetbrains.kotlin.analysis.decompiler.stub.mustNotBeWrittenToStubs
*/
internal fun CallableMemberDescriptor.mustNotBeWrittenToDecompiledText(): Boolean { internal fun CallableMemberDescriptor.mustNotBeWrittenToDecompiledText(): Boolean {
return when (kind) { return when (kind) {
CallableMemberDescriptor.Kind.DECLARATION, CallableMemberDescriptor.Kind.DELEGATION -> false CallableMemberDescriptor.Kind.DECLARATION, CallableMemberDescriptor.Kind.DELEGATION -> false
CallableMemberDescriptor.Kind.FAKE_OVERRIDE -> true CallableMemberDescriptor.Kind.FAKE_OVERRIDE -> true
CallableMemberDescriptor.Kind.SYNTHESIZED -> { CallableMemberDescriptor.Kind.SYNTHESIZED -> syntheticMemberMustNotBeWrittenToDecompiledText()
// Of all synthesized functions, only `component*` functions are rendered (for historical reasons) }
!DataClassDescriptorResolver.isComponentLike(name) && name !in listOf( }
OperatorNameConventions.EQUALS,
StandardNames.HASHCODE_NAME, private fun CallableMemberDescriptor.syntheticMemberMustNotBeWrittenToDecompiledText(): Boolean {
OperatorNameConventions.TO_STRING val containingClass = containingDeclaration as? ClassDescriptor ?: return false
return when {
containingClass.isData && containingClass.kind != ClassKind.OBJECT -> {
// we want to materialize every synthetic data class function except for the 'copy' (for historical reasons)
name == StandardNames.DATA_CLASS_COPY
}
containingClass.kind == ClassKind.ENUM_CLASS -> {
name in arrayOf(
StandardNames.ENUM_VALUES,
StandardNames.ENUM_ENTRIES,
StandardNames.ENUM_VALUE_OF,
) )
} }
else -> false
} }
} }
@@ -26,12 +26,10 @@ import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.stubs.KotlinPropertyStub import org.jetbrains.kotlin.psi.stubs.KotlinPropertyStub
import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes
import org.jetbrains.kotlin.psi.stubs.impl.* import org.jetbrains.kotlin.psi.stubs.impl.*
import org.jetbrains.kotlin.resolve.DataClassResolver
import org.jetbrains.kotlin.resolve.constants.ClassLiteralValue import org.jetbrains.kotlin.resolve.constants.ClassLiteralValue
import org.jetbrains.kotlin.serialization.deserialization.AnnotatedCallableKind import org.jetbrains.kotlin.serialization.deserialization.AnnotatedCallableKind
import org.jetbrains.kotlin.serialization.deserialization.ProtoContainer import org.jetbrains.kotlin.serialization.deserialization.ProtoContainer
import org.jetbrains.kotlin.serialization.deserialization.getName import org.jetbrains.kotlin.serialization.deserialization.getName
import org.jetbrains.kotlin.util.OperatorNameConventions
import org.jetbrains.kotlin.utils.addIfNotNull import org.jetbrains.kotlin.utils.addIfNotNull
import org.jetbrains.kotlin.utils.addToStdlib.runIf import org.jetbrains.kotlin.utils.addToStdlib.runIf
@@ -55,14 +53,14 @@ fun createDeclarationsStubs(
propertyProtos: List<ProtoBuf.Property>, propertyProtos: List<ProtoBuf.Property>,
) { ) {
for (propertyProto in propertyProtos) { for (propertyProto in propertyProtos) {
if (mustNotBeWrittenToStubs(propertyProto.flags, outerContext.nameResolver.getName(propertyProto.name))) { if (mustNotBeWrittenToStubs(propertyProto.flags, outerContext.nameResolver.getName(propertyProto.name), protoContainer)) {
continue continue
} }
PropertyClsStubBuilder(parentStub, outerContext, protoContainer, propertyProto).build() PropertyClsStubBuilder(parentStub, outerContext, protoContainer, propertyProto).build()
} }
for (functionProto in functionProtos) { for (functionProto in functionProtos) {
if (mustNotBeWrittenToStubs(functionProto.flags, outerContext.nameResolver.getName(functionProto.name))) { if (mustNotBeWrittenToStubs(functionProto.flags, outerContext.nameResolver.getName(functionProto.name), protoContainer)) {
continue continue
} }
@@ -90,15 +88,27 @@ fun createConstructorStub(
ConstructorClsStubBuilder(parentStub, outerContext, protoContainer, constructorProto).build() ConstructorClsStubBuilder(parentStub, outerContext, protoContainer, constructorProto).build()
} }
private fun mustNotBeWrittenToStubs(flags: Int, name: Name): Boolean { /**
* @see org.jetbrains.kotlin.analysis.decompiler.psi.text.mustNotBeWrittenToDecompiledText
*/
private fun mustNotBeWrittenToStubs(flags: Int, name: Name, protoContainer: ProtoContainer): Boolean {
return when (Flags.MEMBER_KIND.get(flags)) { return when (Flags.MEMBER_KIND.get(flags)) {
MemberKind.FAKE_OVERRIDE -> true MemberKind.FAKE_OVERRIDE -> true
//TODO: fix decompiler to use sane criteria //TODO: fix decompiler to use sane criteria
MemberKind.SYNTHESIZED -> !DataClassResolver.isComponentLike(name) && name !in listOf( MemberKind.SYNTHESIZED -> syntheticMemberMustNotBeWrittenToStubs(name, protoContainer)
OperatorNameConventions.EQUALS, else -> false
StandardNames.HASHCODE_NAME, }
OperatorNameConventions.TO_STRING }
)
private fun syntheticMemberMustNotBeWrittenToStubs(name: Name, protoContainer: ProtoContainer): Boolean {
val containingClass = protoContainer as? ProtoContainer.Class ?: return false
return when {
containingClass.isData && containingClass.kind != ProtoBuf.Class.Kind.OBJECT -> {
// we want to materialize every synthetic data class function except for the 'copy' (for historical reasons)
name == StandardNames.DATA_CLASS_COPY
}
else -> false else -> false
} }
} }
@@ -17,7 +17,7 @@ object KotlinStubVersions {
// Binary stub version should be increased if stub format (org.jetbrains.kotlin.psi.stubs.impl) is changed // Binary stub version should be increased if stub format (org.jetbrains.kotlin.psi.stubs.impl) is changed
// or changes are made to the core stub building code (org.jetbrains.kotlin.idea.decompiler.stubBuilder). // or changes are made to the core stub building code (org.jetbrains.kotlin.idea.decompiler.stubBuilder).
// Increasing this version will lead to reindexing of all binary files that are potentially kotlin binaries (including all class files). // Increasing this version will lead to reindexing of all binary files that are potentially kotlin binaries (including all class files).
private const val BINARY_STUB_VERSION = 97 private const val BINARY_STUB_VERSION = 98
// Classfile stub version should be increased if changes are made to classfile stub building subsystem (org.jetbrains.kotlin.idea.decompiler.classFile) // Classfile stub version should be increased if changes are made to classfile stub building subsystem (org.jetbrains.kotlin.idea.decompiler.classFile)
// Increasing this version will lead to reindexing of all classfiles. // Increasing this version will lead to reindexing of all classfiles.
@@ -29,6 +29,7 @@ sealed class ProtoContainer(
val kind: ProtoBuf.Class.Kind = Flags.CLASS_KIND.get(classProto.flags) ?: ProtoBuf.Class.Kind.CLASS val kind: ProtoBuf.Class.Kind = Flags.CLASS_KIND.get(classProto.flags) ?: ProtoBuf.Class.Kind.CLASS
val isInner: Boolean = Flags.IS_INNER.get(classProto.flags) val isInner: Boolean = Flags.IS_INNER.get(classProto.flags)
val isData: Boolean = Flags.IS_DATA.get(classProto.flags)
override fun debugFqName(): FqName = classId.asSingleFqName() override fun debugFqName(): FqName = classId.asSingleFqName()
} }