From 2befd2bffb62ccce439c66e00996f98590123fd5 Mon Sep 17 00:00:00 2001 From: Roman Golyshev Date: Tue, 23 Jan 2024 00:16:08 +0100 Subject: [PATCH] KT-62695 [AA] Implement resolution of extension functions in KDoc The solution is not complete, since resolution of KDocs is not fully specified and is rather tricky. Many more corner cases are to be expected. This solution should at least give some groundwork for the future improvements/fixes of the resolver. ^KT-62695 Fixed --- .../fir/references/KDocReferenceResolver.kt | 150 ++++++++++++++++-- .../kDoc/extensions/callablesWithSameName.txt | 6 +- .../kDoc/extensions/fromBaseClass.txt | 4 +- .../kDoc/extensions/functionOverloads.txt | 7 +- .../generics/genericsCompatibility.txt | 12 +- .../generics/typeWithGenericsReceiver.txt | 4 +- .../kDoc/extensions/topLevelFunction.txt | 4 +- .../kDoc/extensions/topLevelProperty.txt | 4 +- .../kDoc/extensions/typeAliasReceiver.txt | 8 +- .../kDoc/extensions/typeCompatibility.txt | 12 +- 10 files changed, 171 insertions(+), 40 deletions(-) diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/references/KDocReferenceResolver.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/references/KDocReferenceResolver.kt index c49b3340360..8edeb0f8c02 100644 --- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/references/KDocReferenceResolver.kt +++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/references/KDocReferenceResolver.kt @@ -9,11 +9,14 @@ import com.intellij.psi.util.PsiTreeUtil import org.jetbrains.kotlin.analysis.api.KtAnalysisSession import org.jetbrains.kotlin.analysis.api.components.KtScopeContext import org.jetbrains.kotlin.analysis.api.components.KtScopeKind +import org.jetbrains.kotlin.analysis.api.components.buildClassType import org.jetbrains.kotlin.analysis.api.scopes.KtScope import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol import org.jetbrains.kotlin.analysis.api.symbols.KtCallableSymbol import org.jetbrains.kotlin.analysis.api.symbols.* import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithMembers +import org.jetbrains.kotlin.analysis.api.types.KtType +import org.jetbrains.kotlin.analysis.api.types.KtTypeParameterType import org.jetbrains.kotlin.analysis.utils.printer.parentsOfType import org.jetbrains.kotlin.name.CallableId import org.jetbrains.kotlin.name.ClassId @@ -26,9 +29,16 @@ import org.jetbrains.kotlin.analysis.utils.printer.parentOfType import org.jetbrains.kotlin.utils.addIfNotNull internal object KDocReferenceResolver { - private data class ResolveResult(val symbol: KtSymbol) + /** + * [symbol] is the symbol referenced by this resolve result. + * + * [receiverClassReference] is an optional receiver type in + * the case of extension function references (see [getTypeQualifiedExtensions]). + */ + private data class ResolveResult(val symbol: KtSymbol, val receiverClassReference: KtClassLikeSymbol?) - private fun KtSymbol.toResolveResult(): ResolveResult = ResolveResult(this) + private fun KtSymbol.toResolveResult(receiverClassReference: KtClassLikeSymbol? = null): ResolveResult = + ResolveResult(symbol = this, receiverClassReference) /** * Resolves the [selectedFqName] of KDoc @@ -59,9 +69,23 @@ internal object KDocReferenceResolver { check(goBackSteps > 0) { "Selected FqName ($selectedFqName) should be smaller than the whole FqName ($fullFqName)" } - return fullSymbolsResolved.mapNotNullTo(mutableSetOf()) { findParentSymbol(it.symbol, goBackSteps, selectedFqName) } + return fullSymbolsResolved.mapNotNullTo(mutableSetOf()) { findParentSymbol(it, goBackSteps, selectedFqName) } } + /** + * Finds the parent symbol of the given [ResolveResult] by traversing back up the symbol hierarchy a [goBackSteps] steps, + * or until the containing class or object symbol is found. + * + * Knows about the [ResolveResult.receiverClassReference] field and uses it in case it's not empty. + */ + context(KtAnalysisSession) + private fun findParentSymbol(resolveResult: ResolveResult, goBackSteps: Int, selectedFqName: FqName): KtSymbol? { + return if (resolveResult.receiverClassReference != null) { + findParentSymbol(resolveResult.receiverClassReference, goBackSteps - 1, selectedFqName) + } else { + findParentSymbol(resolveResult.symbol, goBackSteps, selectedFqName) + } + } /** * Finds the parent symbol of the given KtSymbol by traversing back up the symbol hierarchy a certain number of steps, @@ -88,9 +112,17 @@ internal object KDocReferenceResolver { context(KtAnalysisSession) private fun resolveKdocFqName(fqName: FqName, contextElement: KtElement): Collection { getExtensionReceiverSymbolByThisQualifier(fqName, contextElement).ifNotEmpty { return this } - getSymbolsFromExistingScopes(fqName, contextElement).ifNotEmpty { return this } + + buildList { + getSymbolsFromScopes(fqName, contextElement).mapTo(this) { it.toResolveResult() } + addAll(getTypeQualifiedExtensions(fqName, contextElement)) + addIfNotNull(getPackageSymbolIfPackageExists(fqName)?.toResolveResult()) + }.ifNotEmpty { return this } + getNonImportedSymbolsByFullyQualifiedName(fqName).map { it.toResolveResult() }.ifNotEmpty { return this } + AdditionalKDocResolutionProvider.resolveKdocFqName(fqName, contextElement).map { it.toResolveResult() }.ifNotEmpty { return this } + return emptyList() } @@ -100,19 +132,12 @@ internal object KDocReferenceResolver { if (fqName.pathSegments().singleOrNull()?.asString() == "this") { if (owner is KtCallableDeclaration && owner.receiverTypeReference != null) { val symbol = owner.getSymbol() as? KtCallableSymbol ?: return emptyList() - return listOfNotNull(symbol.receiverParameter).map { it.toResolveResult() } + return listOfNotNull(symbol.receiverParameter?.toResolveResult()) } } return emptyList() } - context(KtAnalysisSession) - private fun getSymbolsFromExistingScopes(fqName: FqName, contextElement: KtElement): Collection = - buildList { - getSymbolsFromScopes(fqName, contextElement).mapTo(this) { it.toResolveResult() } - addIfNotNull(getPackageSymbolIfPackageExists(fqName)?.toResolveResult()) - } - context(KtAnalysisSession) private fun getSymbolsFromScopes(fqName: FqName, contextElement: KtElement): Collection { getSymbolsFromParentMemberScopes(fqName, contextElement).ifNotEmpty { return this } @@ -237,6 +262,107 @@ internal object KDocReferenceResolver { } } + /** + * Tries to resolve [fqName] into available extension callables (functions or properties) + * prefixed with a suitable extension receiver type (like in `Foo.bar`, or `foo.Foo.bar`). + * + * Relies on the fact that in such references only the last qualifier refers to the + * actual extension callable, and the part before that refers to the receiver type (either fully + * or partially qualified). + * + * For example, `foo.Foo.bar` may only refer to the extension callable `bar` with + * a `foo.Foo` receiver type, and this function will only look for such combinations. + * + * N.B. This function only searches for extension callables qualified by receiver types! + * It does not try to resolve fully qualified or member functions, because they are dealt + * with by the other parts of [KDocReferenceResolver]. + */ + context(KtAnalysisSession) + private fun getTypeQualifiedExtensions(fqName: FqName, contextElement: KtElement): Collection { + if (fqName.isRoot) return emptyList() + val extensionName = fqName.shortName() + + val receiverTypeName = fqName.parent() + if (receiverTypeName.isRoot) return emptyList() + + val possibleExtensions = getExtensionCallableSymbolsByShortName(extensionName, contextElement) + if (possibleExtensions.isEmpty()) return emptyList() + + val possibleReceivers = getReceiverTypeCandidates(receiverTypeName, contextElement) + + // we abandon resolve if there are multiple different classifiers in scope + val receiverClassSymbol = possibleReceivers.singleOrNull() ?: return emptyList() + val receiverType = buildClassType(receiverClassSymbol) + + return possibleExtensions + .filter { it.canBeReferencedAsExtensionOn(receiverType) } + .map { it.toResolveResult(receiverClassReference = receiverClassSymbol) } + } + + context(KtAnalysisSession) + private fun getExtensionCallableSymbolsByShortName(name: Name, contextElement: KtElement): List { + return getSymbolsFromScopes(FqName.topLevel(name), contextElement) + .filterIsInstance() + .filter { it.isExtension } + } + + context(KtAnalysisSession) + private fun getReceiverTypeCandidates(receiverTypeName: FqName, contextElement: KtElement): List { + val possibleReceivers = + getSymbolsFromScopes(receiverTypeName, contextElement).ifEmpty { null } + ?: getNonImportedSymbolsByFullyQualifiedName(receiverTypeName).ifEmpty { null } + ?: emptyList() + + return possibleReceivers.filterIsInstance() + } + + /** + * Returns true if we consider that [this] extension function prefixed with [actualReceiverType] in + * a KDoc reference should be considered as legal and resolved, and false otherwise. + * + * This is **not** an actual type check, it is just an opinionated approximation. + * The main guideline was K1 KDoc resolve. + * + * This check might change in the future, as Dokka team advances with KDoc rules. + */ + context(KtAnalysisSession) + private fun KtCallableSymbol.canBeReferencedAsExtensionOn(actualReceiverType: KtType): Boolean { + val extensionReceiverType = receiverParameter?.type ?: return false + + return extensionReceiverType.isPossiblySuperTypeOf(actualReceiverType) + } + + /** + * Same constraints as in [canBeReferencedAsExtensionOn]. + * + * For a similar function in the `intellij` repository, see `isPossiblySubTypeOf`. + */ + context(KtAnalysisSession) + private fun KtType.isPossiblySuperTypeOf(actualReceiverType: KtType): Boolean { + // Type parameters cannot act as receiver types in KDoc + if (actualReceiverType is KtTypeParameterType) return false + + val expectedType = this + + if (expectedType is KtTypeParameterType) { + return expectedType.symbol.upperBounds.all { it.isPossiblySuperTypeOf(actualReceiverType) } + } + + val receiverExpanded = actualReceiverType.expandedClassSymbol + val expectedExpanded = expectedType.expandedClassSymbol + + // if the underlying classes are equal, we consider the check successful + // despite the possibility of different type bounds + if ( + receiverExpanded != null && + receiverExpanded == expectedExpanded + ) { + return true + } + + return actualReceiverType.isSubTypeOf(expectedType) + } + context(KtAnalysisSession) private fun getNonImportedSymbolsByFullyQualifiedName(fqName: FqName): Collection = buildSet { generateNameInterpretations(fqName).forEach { interpretation -> diff --git a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/callablesWithSameName.txt b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/callablesWithSameName.txt index 7881d87a02e..d49c3de5370 100644 --- a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/callablesWithSameName.txt +++ b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/callablesWithSameName.txt @@ -7,7 +7,9 @@ Caret 2 resolved to: 1: (in test) val test.Foo.ext: kotlin.Int Caret 3 resolved to: -Nothing (Unresolved reference) +0: (in test) fun test.Foo.ext() +1: (in test) val test.Foo.ext: kotlin.Int Caret 4 resolved to: -Nothing (Unresolved reference) +0: (in test) fun test.Foo.ext() +1: (in test) val test.Foo.ext: kotlin.Int \ No newline at end of file diff --git a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/fromBaseClass.txt b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/fromBaseClass.txt index 3e870791d40..bd2ec63423f 100644 --- a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/fromBaseClass.txt +++ b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/fromBaseClass.txt @@ -1,5 +1,5 @@ Caret 1 resolved to: -Nothing (Unresolved reference) +0: (in test.Base) fun test.Receiver.ext() Caret 2 resolved to: -Nothing (Unresolved reference) +0: (in test.Base) fun test.Receiver.ext() \ No newline at end of file diff --git a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/functionOverloads.txt b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/functionOverloads.txt index d63be9c67a7..0869692731f 100644 --- a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/functionOverloads.txt +++ b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/functionOverloads.txt @@ -7,7 +7,10 @@ Caret 2 resolved to: 1: (in test) fun test.Base.function(i: kotlin.Int) Caret 3 resolved to: -0: (in test.Base) fun function() +0: (in test) fun kotlin.Any.function() +1: (in test) fun test.Base.function(i: kotlin.Int) +2: (in test.Base) fun function() Caret 4 resolved to: -0: (in test.Base) fun function() +0: (in test) fun kotlin.Any.function() +1: (in test) fun test.Base.function(i: kotlin.Int) \ No newline at end of file diff --git a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/generics/genericsCompatibility.txt b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/generics/genericsCompatibility.txt index 4b120ae8187..be1f55fde3a 100644 --- a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/generics/genericsCompatibility.txt +++ b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/generics/genericsCompatibility.txt @@ -1,20 +1,20 @@ Caret 1 resolved to: -Nothing (Unresolved reference) +0: (in test) fun T.anyExt() Caret 2 resolved to: -Nothing (Unresolved reference) +0: (in test) fun T.anyExt() Caret 3 resolved to: -Nothing (Unresolved reference) +0: (in test) fun T.anyExt() Caret 4 resolved to: Nothing (Unresolved reference) Caret 5 resolved to: -Nothing (Unresolved reference) +0: (in test) fun T.baseExt() Caret 6 resolved to: -Nothing (Unresolved reference) +0: (in test) fun T.baseExt() Caret 7 resolved to: Nothing (Unresolved reference) @@ -23,4 +23,4 @@ Caret 8 resolved to: Nothing (Unresolved reference) Caret 9 resolved to: -Nothing (Unresolved reference) \ No newline at end of file +0: (in test) fun T.childExt() \ No newline at end of file diff --git a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/generics/typeWithGenericsReceiver.txt b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/generics/typeWithGenericsReceiver.txt index 7bc25a97c4c..b1aa5afafc8 100644 --- a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/generics/typeWithGenericsReceiver.txt +++ b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/generics/typeWithGenericsReceiver.txt @@ -1,5 +1,5 @@ Caret 1 resolved to: -Nothing (Unresolved reference) +0: (in test) fun test.WithGenerics.intExt() Caret 2 resolved to: -Nothing (Unresolved reference) \ No newline at end of file +0: (in test) fun test.WithGenerics.stringExt() \ No newline at end of file diff --git a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/topLevelFunction.txt b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/topLevelFunction.txt index 331cfb0cb3b..8c4472e3754 100644 --- a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/topLevelFunction.txt +++ b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/topLevelFunction.txt @@ -5,7 +5,7 @@ Caret 2 resolved to: 0: (in test) fun test.Foo.extFun() Caret 3 resolved to: -Nothing (Unresolved reference) +0: (in test) fun test.Foo.extFun() Caret 4 resolved to: -Nothing (Unresolved reference) +0: (in test) fun test.Foo.extFun() \ No newline at end of file diff --git a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/topLevelProperty.txt b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/topLevelProperty.txt index 11cc7b8b8c8..7c6863715d5 100644 --- a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/topLevelProperty.txt +++ b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/topLevelProperty.txt @@ -5,7 +5,7 @@ Caret 2 resolved to: 0: (in test) val test.Foo.extProp: kotlin.Int Caret 3 resolved to: -Nothing (Unresolved reference) +0: (in test) val test.Foo.extProp: kotlin.Int Caret 4 resolved to: -Nothing (Unresolved reference) +0: (in test) val test.Foo.extProp: kotlin.Int \ No newline at end of file diff --git a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/typeAliasReceiver.txt b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/typeAliasReceiver.txt index cd97bab9b9e..af37fdb717a 100644 --- a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/typeAliasReceiver.txt +++ b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/typeAliasReceiver.txt @@ -1,14 +1,14 @@ Caret 1 resolved to: -Nothing (Unresolved reference) +0: (in test) fun test.Foo.fooExt() Caret 2 resolved to: -Nothing (Unresolved reference) +0: (in test) fun test.FooAlias.fooAliasExt() Caret 3 resolved to: -Nothing (Unresolved reference) +0: (in test) fun test.FooAlias.fooAliasExt() Caret 4 resolved to: -Nothing (Unresolved reference) +0: (in test) fun kotlin.Any.anyExt() Caret 5 resolved to: Nothing (Unresolved reference) \ No newline at end of file diff --git a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/typeCompatibility.txt b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/typeCompatibility.txt index e97df900220..0680cd4f925 100644 --- a/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/typeCompatibility.txt +++ b/analysis/analysis-api/testData/referenceResolve/kDoc/extensions/typeCompatibility.txt @@ -1,20 +1,20 @@ Caret 1 resolved to: -Nothing (Unresolved reference) +0: (in test) fun kotlin.Any.anyExt() Caret 2 resolved to: -Nothing (Unresolved reference) +0: (in test) fun kotlin.Any.anyExt() Caret 3 resolved to: -Nothing (Unresolved reference) +0: (in test) fun kotlin.Any.anyExt() Caret 4 resolved to: Nothing (Unresolved reference) Caret 5 resolved to: -Nothing (Unresolved reference) +0: (in test) fun test.Base.baseExt() Caret 6 resolved to: -Nothing (Unresolved reference) +0: (in test) fun test.Base.baseExt() Caret 7 resolved to: Nothing (Unresolved reference) @@ -23,4 +23,4 @@ Caret 8 resolved to: Nothing (Unresolved reference) Caret 9 resolved to: -Nothing (Unresolved reference) +0: (in test) fun test.Child.childExt() \ No newline at end of file