Fix navigation in IDE to synthetic methods in compiled Kotlin

For some synthetic methods from compiled Kotlin classes, it is
not possible to find the PsiElement, when navigating to declaration
in the IDE.

For other methods it works like this:
1. There is a SourceElement with a psi inside
2. Or for K1, the element is searched in the decompiled text
   (see ByDescriptorIndexer),
3. Or for K2, the element is searched by Stub taken from metadata
   (see FirDeserializedDeclarationSourceProvider)

However, these approaches do not apply to synthetic methods which have
no SourceElement and are not written into decompiled text and metadata.

These are methods values, valueOf, entries from enum and
copy, equals, hashCode, toString from data classes.

Therefore, it was decided to handle their cases separately
at the resolve stage.

Tests (in idea repository):
- org.jetbrains.kotlin.idea.resolve.ReferenceResolveWithLibTestGenerated#testDataClassSyntheticMethods
- org.jetbrains.kotlin.idea.resolve.ReferenceResolveWithLibTestGenerated#testEnumSyntheticMethods
- org.jetbrains.kotlin.idea.fir.resolve.FirReferenceResolveWithLibTestGenerated#testDataClassSyntheticMethods
- org.jetbrains.kotlin.idea.fir.resolve.FirReferenceResolveWithLibTestGenerated#testEnumSyntheticMethods

^KTIJ-24413 Fixed
This commit is contained in:
Roman Efremov
2023-02-09 13:22:20 +01:00
committed by Space Team
parent f640a7be2e
commit d120d83d60
4 changed files with 61 additions and 18 deletions
@@ -12,10 +12,15 @@ import org.jetbrains.kotlin.analysis.low.level.api.fir.api.KtDeclarationAndFirDe
import org.jetbrains.kotlin.analysis.low.level.api.fir.project.structure.llFirModuleData import org.jetbrains.kotlin.analysis.low.level.api.fir.project.structure.llFirModuleData
import org.jetbrains.kotlin.analysis.project.structure.KtBuiltinsModule import org.jetbrains.kotlin.analysis.project.structure.KtBuiltinsModule
import org.jetbrains.kotlin.analysis.providers.createDeclarationProvider import org.jetbrains.kotlin.analysis.providers.createDeclarationProvider
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.fir.FirElement import org.jetbrains.kotlin.fir.FirElement
import org.jetbrains.kotlin.fir.containingClassLookupTag import org.jetbrains.kotlin.fir.containingClassLookupTag
import org.jetbrains.kotlin.fir.declarations.* import org.jetbrains.kotlin.fir.declarations.*
import org.jetbrains.kotlin.fir.declarations.utils.isData
import org.jetbrains.kotlin.fir.declarations.utils.isEnumClass
import org.jetbrains.kotlin.fir.resolve.getContainingClass
import org.jetbrains.kotlin.fir.scopes.impl.delegatedWrapperData import org.jetbrains.kotlin.fir.scopes.impl.delegatedWrapperData
import org.jetbrains.kotlin.fir.symbols.impl.FirCallableSymbol
import org.jetbrains.kotlin.fir.unwrapFakeOverrides import org.jetbrains.kotlin.fir.unwrapFakeOverrides
import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.FqName
@@ -25,6 +30,7 @@ import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtFunction import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtNamedFunction import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.util.OperatorNameConventions
//todo introduce LibraryModificationTracker based cache? //todo introduce LibraryModificationTracker based cache?
internal object FirDeserializedDeclarationSourceProvider { internal object FirDeserializedDeclarationSourceProvider {
@@ -40,6 +46,34 @@ internal object FirDeserializedDeclarationSourceProvider {
} }
} }
fun findClassPsiForGeneratedMembers(fir: FirElement, project: Project): PsiElement? {
if (fir !is FirCallableDeclaration) {
return null
}
val containingClass = fir.getContainingClass(fir.moduleData.session) ?: return null
if (isGeneratedMemberNotWrittenToMetadata(fir.symbol, containingClass)) {
return provideSourceForClass(containingClass, project)
}
return null
}
private fun isGeneratedMemberNotWrittenToMetadata(symbol: FirCallableSymbol<*>, containingClass: FirRegularClass): Boolean {
return when {
containingClass.isEnumClass -> symbol.name in setOf(
StandardNames.ENUM_VALUES,
StandardNames.ENUM_VALUE_OF,
StandardNames.ENUM_ENTRIES,
)
containingClass.isData -> symbol.name in setOf(
OperatorNameConventions.EQUALS,
OperatorNameConventions.TO_STRING,
StandardNames.HASHCODE_NAME,
StandardNames.DATA_CLASS_COPY,
)
else -> false
}
}
private fun provideSourceForFunction( private fun provideSourceForFunction(
function: FirSimpleFunction, function: FirSimpleFunction,
project: Project project: Project
@@ -51,6 +51,9 @@ fun FirElement.findPsi(session: FirSession): PsiElement? =
* For data classes & enums generated members like `copy` `componentN`, `values` it will return corresponding enum/data class * For data classes & enums generated members like `copy` `componentN`, `values` it will return corresponding enum/data class
* Otherwise, behaves the same way as [findPsi] returns exact PSI declaration corresponding to passed [FirDeclaration] * Otherwise, behaves the same way as [findPsi] returns exact PSI declaration corresponding to passed [FirDeclaration]
*/ */
fun FirDeclaration.findReferencePsi(): PsiElement? = fun FirDeclaration.findReferencePsi(): PsiElement? {
psi psi?.let { return it }
?: FirDeserializedDeclarationSourceProvider.findPsi(this, (moduleData.session as LLFirSession).project) val project = (moduleData.session as LLFirSession).project
return FirDeserializedDeclarationSourceProvider.findPsi(this, project)
?: FirDeserializedDeclarationSourceProvider.findClassPsiForGeneratedMembers(this, project)
}
@@ -52,6 +52,12 @@ object ByDescriptorIndexer : DecompiledTextIndexer<String> {
return classOrObject?.primaryConstructor ?: classOrObject return classOrObject?.primaryConstructor ?: classOrObject
} }
if ((original as? CallableMemberDescriptor)?.mustNotBeWrittenToDecompiledText() == true &&
original.containingDeclaration is ClassDescriptor
) {
return getDeclarationForDescriptor(original.containingDeclaration, file)
}
val descriptorKey = original.toStringKey() val descriptorKey = original.toStringKey()
if (!file.isContentsLoaded && original is MemberDescriptor) { if (!file.isContentsLoaded && original is MemberDescriptor) {
@@ -34,6 +34,20 @@ fun DescriptorRendererOptions.defaultDecompilerRendererOptions() {
parameterNamesInFunctionalTypes = false // to support parameters names in decompiled text we need to load annotation arguments parameterNamesInFunctionalTypes = false // to support parameters names in decompiled text we need to load annotation arguments
} }
internal fun CallableMemberDescriptor.mustNotBeWrittenToDecompiledText(): Boolean {
return when (kind) {
CallableMemberDescriptor.Kind.DECLARATION -> false
CallableMemberDescriptor.Kind.FAKE_OVERRIDE,
CallableMemberDescriptor.Kind.DELEGATION -> true
CallableMemberDescriptor.Kind.SYNTHESIZED -> {
// Of all synthesized functions, only `component*` functions are rendered (for historical reasons)
!DataClassDescriptorResolver.isComponentLike(name)
}
}
}
fun buildDecompiledText( fun buildDecompiledText(
packageFqName: FqName, packageFqName: FqName,
descriptors: List<DeclarationDescriptor>, descriptors: List<DeclarationDescriptor>,
@@ -56,20 +70,6 @@ fun buildDecompiledText(
textIndex.addToIndex(descriptor, TextRange(startOffset, endOffset)) textIndex.addToIndex(descriptor, TextRange(startOffset, endOffset))
} }
fun CallableMemberDescriptor.isConsideredSynthetic(): Boolean {
return when (kind) {
CallableMemberDescriptor.Kind.DECLARATION -> false
CallableMemberDescriptor.Kind.FAKE_OVERRIDE,
CallableMemberDescriptor.Kind.DELEGATION -> true
CallableMemberDescriptor.Kind.SYNTHESIZED -> {
// Of all synthesized functions, only `component*` functions are rendered (for historical reasons)
!DataClassDescriptorResolver.isComponentLike(name)
}
}
}
fun appendDescriptor(descriptor: DeclarationDescriptor, indent: String, lastEnumEntry: Boolean? = null) { fun appendDescriptor(descriptor: DeclarationDescriptor, indent: String, lastEnumEntry: Boolean? = null) {
val startOffset = builder.length val startOffset = builder.length
if (isEnumEntry(descriptor)) { if (isEnumEntry(descriptor)) {
@@ -144,7 +144,7 @@ fun buildDecompiledText(
if (member == companionObject) { if (member == companionObject) {
continue continue
} }
if (member is CallableMemberDescriptor && member.isConsideredSynthetic()) { if (member is CallableMemberDescriptor && member.mustNotBeWrittenToDecompiledText()) {
continue continue
} }
newlineExceptFirst() newlineExceptFirst()