Handle SHORTEN_IF_ALEADY_IMPORTED case of KtFirReferenceShortener

For the following example, when we run the reference shortener, it
drops `a.b.c` qualifier, because it matches "FOURTH".
```
package a.b.c

fun <T, E, D> foo(a: T, b: E, c: D) = a.hashCode() + b.hashCode() + c.hashCode() // FIRST
fun <E> E.foo() = hashCode() // SECOND

object Receiver {
    fun <T, E, D> foo(a: T, b: E, c: D) = a.hashCode() + b.hashCode() + c.hashCode() // THIRD
    fun foo(a: Int, b: Boolean, c: String) = a.hashCode() + b.hashCode() + c.hashCode() // FOURTH
    fun test(): Int {
        fun foo(a: Int, b: Boolean, c: Int) = a + b.hashCode() + c // FIFTH
        return <expr>a.b.c.foo(1, false, "bar")</expr>
    }
}
```

As shown in the above example, when SHORTEN_IF_ALEADY_IMPORTED option is
given from a user, the reference shortener has to check whether it can
drop the qualifier without changing the referenced symbol and if it is
possible to do that without adding a new import directive, it deletes
the qualifier.

It needs two steps:
 1. Collect all candidate symbols matching the signature e.g., function
    arguments / type arguments
 2. Determine whether the referenced symbol has the highest reference
    priority when we drops the qualifier depending on scopes

This commit uses `AllCandidatesResolver(shorteningContext.analysisSession.useSiteSession).
getAllCandidates( .. fake FIR call/property-access ..)` for step1.
For step2, we use a heuristic based on scopes of candidates. If a
candidate symbol is under the same scope with the target expression, it
has a `FirLocalScope` which has the high priority. So when we have a
candidate under a `FirLocalScope` and the actual referenced symbol is
different from the candidate, we must avoid dropping its qualifier
because the shortening will change its semantics i.e., reference.

The order of scopes depending on their scope types is:
 1. FirLocalScope
 2. FirClassUseSiteMemberScope / FirNestedClassifierScope
 3. FirExplicitSimpleImportingScope
 4. FirPackageMemberScope
 5. others

Note that for "others" the above rule can be wrong. Please update it if
you find other scopes that have a priority higher than the specified
scopes.

One of non-trivial parts is the priority among multiple
FirClassUseSiteMemberScope and FirNestedClassifierScope. They are
basically scopes for class declarations. We decide their priorities
based on the distance of class declaration from the target expression.

Note that we take a strict approach to reject all false positive. For
example, when we are not sure, we don't shorten it to avoid changing its
semantics.

TODO: One corner case is handling receivers. We have to update
```
private fun shortenIfAlreadyImported(
    firQualifiedAccess: FirQualifiedAccess,
    calledSymbol: FirCallableSymbol<*>,
    expressionInScope: KtExpression,
): Boolean
```

The current implementation cannot handle the following example:
```
package foo
class Foo {
    fun test() {
        // It references FIRST. Removing `foo` lets it reference SECOND.
        <caret>foo.myRun {
            42
        }
    }
}
inline fun <R> myRun(block: () -> R): R = block()         // FIRST
inline fun <T, R> T.myRun(block: T.() -> R): R = block()  // SECOND
```

Tests related to TODO:
 - analysis/analysis-api/testData/components/referenceShortener/referenceShortener/receiver2.kt
 - analysis/analysis-api/testData/components/referenceShortener/referenceShortener/receiver3.kt
This commit is contained in:
Jaebaek Seo
2023-01-25 02:17:38 +01:00
committed by teamcity
parent 860dee2cb1
commit a09d0aa1cf
99 changed files with 2265 additions and 45 deletions
@@ -5,23 +5,25 @@
package org.jetbrains.kotlin.analysis.api.fir.components
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import com.intellij.psi.SmartPsiElementPointer
import org.jetbrains.kotlin.analysis.api.components.KtReferenceShortener
import org.jetbrains.kotlin.analysis.api.components.ShortenCommand
import org.jetbrains.kotlin.analysis.api.components.ShortenOption
import org.jetbrains.kotlin.analysis.api.fir.KtFirAnalysisSession
import org.jetbrains.kotlin.analysis.api.fir.components.ElementsToShortenCollector.PartialOrderOfScope.Companion.toPartialOrder
import org.jetbrains.kotlin.analysis.api.fir.utils.addImportToFile
import org.jetbrains.kotlin.analysis.api.fir.utils.computeImportableName
import org.jetbrains.kotlin.analysis.api.lifetime.KtLifetimeToken
import org.jetbrains.kotlin.analysis.api.symbols.KtCallableSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtClassLikeSymbol
import org.jetbrains.kotlin.analysis.api.lifetime.KtLifetimeToken
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.LLFirResolveSession
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.LowLevelFirApiFacadeForResolveOnAir
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getOrBuildFir
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getOrBuildFirOfType
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.resolveToFirSymbol
import org.jetbrains.kotlin.analysis.low.level.api.fir.element.builder.FirTowerContextProvider
import org.jetbrains.kotlin.analysis.low.level.api.fir.resolver.AllCandidatesResolver
import org.jetbrains.kotlin.analysis.utils.printer.parentsOfType
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.fir.*
@@ -29,14 +31,16 @@ import org.jetbrains.kotlin.fir.analysis.checkers.toRegularClassSymbol
import org.jetbrains.kotlin.fir.declarations.*
import org.jetbrains.kotlin.fir.declarations.builder.buildImport
import org.jetbrains.kotlin.fir.declarations.builder.buildResolvedImport
import org.jetbrains.kotlin.fir.expressions.FirErrorResolvedQualifier
import org.jetbrains.kotlin.fir.expressions.FirFunctionCall
import org.jetbrains.kotlin.fir.expressions.FirResolvedQualifier
import org.jetbrains.kotlin.fir.declarations.utils.classId
import org.jetbrains.kotlin.fir.expressions.*
import org.jetbrains.kotlin.fir.expressions.builder.buildFunctionCall
import org.jetbrains.kotlin.fir.expressions.builder.buildPropertyAccessExpression
import org.jetbrains.kotlin.fir.expressions.impl.FirNoReceiverExpression
import org.jetbrains.kotlin.fir.expressions.FirQualifiedAccessExpression
import org.jetbrains.kotlin.fir.references.FirErrorNamedReference
import org.jetbrains.kotlin.fir.references.FirNamedReference
import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference
import org.jetbrains.kotlin.fir.resolve.ScopeSession
import org.jetbrains.kotlin.fir.references.builder.buildSimpleNamedReference
import org.jetbrains.kotlin.fir.resolve.diagnostics.ConeAmbiguityError
import org.jetbrains.kotlin.fir.resolve.diagnostics.ConeUnmatchedTypeArgumentsError
import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider
@@ -48,17 +52,14 @@ import org.jetbrains.kotlin.fir.scopes.getProperties
import org.jetbrains.kotlin.fir.scopes.impl.*
import org.jetbrains.kotlin.fir.scopes.processClassifiersByName
import org.jetbrains.kotlin.fir.symbols.ConeClassLikeLookupTag
import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol
import org.jetbrains.kotlin.fir.symbols.impl.*
import org.jetbrains.kotlin.fir.types.*
import org.jetbrains.kotlin.fir.visitors.FirVisitorVoid
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.parentOrNull
import org.jetbrains.kotlin.name.*
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer
import org.jetbrains.kotlin.psi.psiUtil.getQualifiedExpressionForSelector
import org.jetbrains.kotlin.psi.psiUtil.unwrapNullability
import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.resolve.calls.tower.CandidateApplicability
import org.jetbrains.kotlin.utils.addIfNotNull
internal class KtFirReferenceShortener(
@@ -77,7 +78,9 @@ internal class KtFirReferenceShortener(
val declarationToVisit = file.findSmallestDeclarationContainingSelection(selection)
?: file.withDeclarationsResolvedToBodyResolve()
val firDeclaration = declarationToVisit.getOrBuildFirOfType<FirDeclaration>(firResolveSession)
val firDeclaration = declarationToVisit.getOrBuildFir(firResolveSession) as? FirDeclaration ?: return ShortenCommandImpl(
file.createSmartPointer(), emptyList(), emptyList(), emptyList(), emptyList(),
)
val towerContext =
LowLevelFirApiFacadeForResolveOnAir.onAirGetTowerContextProvider(firResolveSession, declarationToVisit)
@@ -169,6 +172,8 @@ private class FirShorteningContext(val analysisSession: KtFirAnalysisSession) {
private val firSession: FirSession
get() = firResolveSession.useSiteFirSession
class ClassifierCandidate(val scope: FirScope, val availableSymbol: AvailableSymbol<ClassId>)
fun findFirstClassifierInScopesByName(positionScopes: List<FirScope>, targetClassName: Name): AvailableSymbol<ClassId>? {
for (scope in positionScopes) {
val classifierSymbol = scope.findFirstClassifierByName(targetClassName) ?: continue
@@ -180,11 +185,26 @@ private class FirShorteningContext(val analysisSession: KtFirAnalysisSession) {
return null
}
fun findFunctionsInScopes(scopes: List<FirScope>, name: Name): List<AvailableSymbol<FirNamedFunctionSymbol>> {
fun findClassifiersInScopesByName(scopes: List<FirScope>, targetClassName: Name): List<ClassifierCandidate> =
scopes.mapNotNull { scope ->
val classifierSymbol = scope.findFirstClassifierByName(targetClassName) ?: return@mapNotNull null
val classifierLookupTag = classifierSymbol.toLookupTag() as? ConeClassLikeLookupTag ?: return@mapNotNull null
ClassifierCandidate(scope, AvailableSymbol(classifierLookupTag.classId, ImportKind.fromScope(scope)))
}
fun findFunctionsInScopes(scopes: List<FirScope>, name: Name): List<AvailableSymbol<FirFunctionSymbol<*>>> {
return scopes.flatMap { scope ->
val importKind = ImportKind.fromScope(scope)
scope.getFunctions(name).map {
AvailableSymbol(it, importKind)
buildList {
// Collect constructors
scope.findFirstClassifierByName(name)?.let { classifierSymbol ->
val classSymbol = classifierSymbol as? FirClassSymbol ?: return@let null
classSymbol.declarationSymbols.filterIsInstance<FirConstructorSymbol>()
}?.forEach { add(AvailableSymbol(it, importKind)) }
// Collect functions
addAll(scope.getFunctions(name).map { AvailableSymbol(it, importKind) })
}
}
}
@@ -217,7 +237,10 @@ private class FirShorteningContext(val analysisSession: KtFirAnalysisSession) {
): List<FirScope>? {
val towerDataContext = towerContextProvider.getClosestAvailableParentContext(position) ?: return null
val result = buildList {
addAll(towerDataContext.nonLocalTowerDataElements.mapNotNull { it.scope })
addAll(towerDataContext.nonLocalTowerDataElements.mapNotNull {
// We must use `it.getAvailableScope()` instead of `it.scope` to check scopes of companion objects as well.
it.getAvailableScope()
})
addIfNotNull(createFakeImportingScope(newImports))
addAll(towerDataContext.localScopes)
}
@@ -418,18 +441,210 @@ private class ElementsToShortenCollector(
)
}
private inline fun <E> findClassifierElementsToShorten(
/**
* Returns true if the class symbol has a type parameter that is supposed to be provided for its parent class.
*
* Example:
* class Outer<T> {
* inner class Inner // Inner has an implicit type parameter `T`.
* }
*/
private fun FirClassLikeSymbol<*>.hasTypeParameterFromParent(): Boolean = typeParameterSymbols.orEmpty().any {
it.containingDeclarationSymbol != this
}
private fun FirScope.correspondingClassIdIfExists(): ClassId = when (this) {
is FirNestedClassifierScope -> klass.classId
is FirClassUseSiteMemberScope -> classId
else -> error("FirScope `$this` is expected to be one of FirNestedClassifierScope and FirClassUseSiteMemberScope to get ClassId")
}
private fun ClassId.idWithoutCompanion() = if (shortClassName == SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT) outerClassId else this
private fun FirScope.isScopeForClass() = this is FirNestedClassifierScope || this is FirClassUseSiteMemberScope
/**
* Assuming that both this [FirScope] and [another] are [FirNestedClassifierScope] or [FirClassUseSiteMemberScope] and both of them
* are surrounding [from], returns whether this [FirScope] is closer than [another] based on the distance from [from].
*
* If one of this [FirScope] and [another] is not [FirNestedClassifierScope] or [FirClassUseSiteMemberScope], it returns false.
*
* Example:
* class Outer { // scope1 ClassId = Other
* class Inner { // scope2 ClassId = Other.Inner
* fun foo() {
* // Distance to scopes for classes from <element> in the order from the closest:
* // scope2 -> scope3 -> scope1
* <element>
* }
* companion object { // scope3 ClassId = Other.Inner.Companion
* }
* }
* }
*
* This function determines the distance based on [ClassId].
*/
private fun FirScope.isScopeForClassCloserThanAnotherScopeForClass(another: FirScope, from: KtClass): Boolean {
// Make sure both are scopes for classes
if (!isScopeForClass() || !another.isScopeForClass()) return false
if (this == another) return false
val classId = correspondingClassIdIfExists()
val classIdOfAnother = another.correspondingClassIdIfExists()
if (classId == classIdOfAnother) return false
// Find the first ClassId matching inner class. If the first matching one is this scope's ClassId, it means this scope is closer
// than `another`.
val candidates = setOfNotNull(classId, classIdOfAnother, classId.idWithoutCompanion(), classIdOfAnother.idWithoutCompanion())
val closestClassId = findMostInnerClassMatchingId(from, candidates)
return closestClassId == classId || (closestClassId != classIdOfAnother && closestClassId == classId.idWithoutCompanion())
}
/**
* Travels all containing classes of [innerClass] and finds the one matching ClassId with one of [candidates]. Returns the matching
* ClassId. If it does not have a matching ClassId, it returns null.
*/
private fun findMostInnerClassMatchingId(innerClass: KtClass, candidates: Set<ClassId>): ClassId? {
var classInNestedClass: KtClass? = innerClass
while (classInNestedClass != null) {
val containingClassId = classInNestedClass.getClassId()
if (containingClassId in candidates) return containingClassId
classInNestedClass = classInNestedClass.containingClass()
}
return null
}
/**
* An enum class to specify the distance of scopes from an element as a partial order
*
* Example:
* import ... // scope1 FirExplicitSimpleImportingScope - enum entry: ExplicitSimpleImporting
* // scope2 FirPackageMemberScope - enum entry: PackageMember
* class Outer { // scope3 FirClassUseSiteMemberScope - enum entry: ClassUseSite
* class Inner { // scope4 FirClassUseSiteMemberScope or FirNestedClassifierScope - enum entry: ClassUseSite/NestedClassifier
* fun foo() { // scope5 FirLocalScope - enum entry: Local
*
* // Distance to scopes from <element> in the order from the closest:
* // scope5 -> scope4 -> scope6 -> scope3 -> scope1 -> scope2
* <element>
*
* }
* companion object {
* // scope6 FirClassUseSiteMemberScope or FirNestedClassifierScope - enum entry: ClassUseSite/NestedClassifier
* }
* }
* }
*/
private enum class PartialOrderOfScope(
val scopeDistanceLevel: Int // Note: Don't use the built-in ordinal since there are some scopes that are at the same level.
) {
Local(1),
ClassUseSite(2),
NestedClassifier(2),
ExplicitSimpleImporting(3),
PackageMember(4),
Unclassified(5),
;
companion object {
fun FirScope.toPartialOrder(): PartialOrderOfScope {
return when (this) {
is FirLocalScope -> Local
is FirClassUseSiteMemberScope -> ClassUseSite
is FirNestedClassifierScope -> NestedClassifier
is FirExplicitSimpleImportingScope -> ExplicitSimpleImporting
is FirPackageMemberScope -> PackageMember
else -> Unclassified
}
}
}
}
/**
* Returns whether this [FirScope] is a scope wider than [another] based on the above [PartialOrderOfScope] or not.
*/
private fun FirScope.isWiderThan(another: FirScope): Boolean =
toPartialOrder().scopeDistanceLevel > another.toPartialOrder().scopeDistanceLevel
/**
* Assuming that all scopes in this List<FirScope> and [base] are surrounding [from], returns whether an element of
* this List<FirScope> is closer than [base] based on the distance from [from].
*/
private fun List<FirScope>.hasScopeCloserThan(base: FirScope, from: KtElement) = any { scope ->
if (scope.isScopeForClass() && base.isScopeForClass()) {
val classContainingFrom = from.containingClass() ?: return@any false
return@any scope.isScopeForClassCloserThanAnotherScopeForClass(base, classContainingFrom)
}
base.isWiderThan(scope)
}
/**
* Returns true if this [PsiFile] has a [KtImportDirective] whose imported FqName is the same as [classId] but references a different
* symbol.
*/
private fun PsiFile.hasImportDirectiveForDifferentSymbolWithSameName(classId: ClassId): Boolean {
val importDirectivesWithSameImportedFqName = collectDescendantsOfType { importedDirective: KtImportDirective ->
importedDirective.importedFqName?.shortName() == classId.shortClassName
}
return importDirectivesWithSameImportedFqName.isNotEmpty() &&
importDirectivesWithSameImportedFqName.all { it.importedFqName != classId.asSingleFqName() }
}
private fun shortenClassifierIfAlreadyImported(
classId: ClassId,
element: KtElement,
classSymbol: FirClassLikeSymbol<*>,
positionScopes: List<FirScope>,
): Boolean {
// If its parent has a type parameter, we cannot shorten it because it will lose its type parameter.
if (classSymbol.hasTypeParameterFromParent()) return false
val name = classId.shortClassName
val availableClassifiers = shorteningContext.findClassifiersInScopesByName(positionScopes, name)
val matchingAvailableSymbol = availableClassifiers.firstOrNull { it.availableSymbol.symbol == classId }
val scopeForClass = matchingAvailableSymbol?.scope ?: return false
if (availableClassifiers.map { it.scope }.hasScopeCloserThan(scopeForClass, element)) return false
/**
* If we have a property with the same name, avoid dropping qualifiers makes it reference a property with the same name e.g.,
* package my.component
* class foo { .. } // A
* ..
* fun test() {
* val foo = .. // B
* my.component.foo::class.java // If we drop `my.component`, it will reference `B` instead of `A`
* }
*/
if (shorteningContext.findPropertiesInScopes(positionScopes, name).isNotEmpty()) {
val firForElement = element.getOrBuildFir(firResolveSession) as? FirQualifiedAccessExpression
val typeArguments = firForElement?.typeArguments ?: emptyList()
val qualifiedAccessCandidates = findCandidatesForPropertyAccess(classSymbol.annotations, typeArguments, name, element)
if (qualifiedAccessCandidates.mapNotNull { it.candidate.originScope }.hasScopeCloserThan(scopeForClass, element)) return false
}
return !element.containingFile.hasImportDirectiveForDifferentSymbolWithSameName(classId)
}
private inline fun <E : KtElement> findClassifierElementsToShorten(
positionScopes: List<FirScope>,
allClassIds: Sequence<ClassId>,
allQualifiedElements: Sequence<E>,
createElementToShorten: (E, nameToImport: FqName?, importAllInParent: Boolean) -> ElementToShorten,
findFakePackageToShortenFn: (E) -> ElementToShorten?,
): ElementToShorten? {
for ((classId, element) in allClassIds.zip(allQualifiedElements)) {
val option = classShortenOption(shorteningContext.toClassSymbol(classId) ?: return null)
val classSymbol = shorteningContext.toClassSymbol(classId) ?: return null
val option = classShortenOption(classSymbol)
if (option == ShortenOption.DO_NOT_SHORTEN) continue
if (shortenClassifierIfAlreadyImported(classId, element, classSymbol, positionScopes)) {
return createElementToShorten(element, null, false)
}
if (option == ShortenOption.SHORTEN_IF_ALREADY_IMPORTED) continue
// Find class with the same name that's already available in this file.
val availableClassifier = shorteningContext.findFirstClassifierInScopesByName(positionScopes, classId.shortClassName)
@@ -469,13 +684,193 @@ private class ElementsToShortenCollector(
return findFakePackageToShortenFn(allQualifiedElements.last())
}
private fun resolveUnqualifiedAccess(
fullyQualifiedAccess: FirQualifiedAccessExpression,
name: Name,
expressionInScope: KtExpression,
): List<OverloadCandidate> {
val fakeCalleeReference = buildSimpleNamedReference { this.name = name }
val functionCall = fullyQualifiedAccess as? FirFunctionCall
val fakeFirQualifiedAccess: FirQualifiedAccessExpression?
if (functionCall == null) {
fakeFirQualifiedAccess = buildPropertyAccessExpression {
annotations.addAll(fullyQualifiedAccess.annotations)
typeArguments.addAll(fullyQualifiedAccess.typeArguments)
calleeReference = fakeCalleeReference
}
} else {
val callExpression = expressionInScope as? KtCallExpression
fakeFirQualifiedAccess = buildFunctionCall {
annotations.addAll(functionCall.annotations)
/**
* It is important to avoid passing type arguments when they are implicit type arguments.
* For example,
* package a.b.c
* fun <T, E> foo(a: T, b: E) {} // A
* fun test() {
* fun foo(a: Int, b: String) {} // B
* a.b.c.foo(3, "test")
* }
*
* In the above code, we must prevent it from shortening `a.b.c.foo(3, "test")`. However, if we explicitly pass the type
* arguments to the resolver, the resolver considers it has some explicit type arguments. Therefore, it reports that the
* only function matching the signature is A, because B does not have type parameters. However, actually B also matches the
* signature, and we must avoid dropping a.b.c from the call expression.
*/
if (callExpression?.typeArguments?.isNotEmpty() == true) typeArguments.addAll(functionCall.typeArguments)
argumentList = functionCall.argumentList
calleeReference = fakeCalleeReference
}
}
val candidates = AllCandidatesResolver(shorteningContext.analysisSession.useSiteSession).getAllCandidates(
firResolveSession, fakeFirQualifiedAccess, name, expressionInScope
)
return candidates.filter { overloadCandidate ->
overloadCandidate.candidate.currentApplicability == CandidateApplicability.RESOLVED
}
}
private fun findCandidatesForPropertyAccess(
annotations: List<FirAnnotation>,
typeArguments: List<FirTypeProjection>,
name: Name,
elementInScope: KtElement,
): List<OverloadCandidate> {
val fakeCalleeReference = buildSimpleNamedReference { this.name = name }
val fakeFirQualifiedAccess = buildPropertyAccessExpression {
this.annotations.addAll(annotations)
this.typeArguments.addAll(typeArguments)
calleeReference = fakeCalleeReference
}
val candidates = AllCandidatesResolver(shorteningContext.analysisSession.useSiteSession).getAllCandidates(
firResolveSession, fakeFirQualifiedAccess, name, elementInScope
)
return candidates.filter { overloadCandidate ->
overloadCandidate.candidate.currentApplicability == CandidateApplicability.RESOLVED
}
}
private fun List<OverloadCandidate>.findScopeForSymbol(symbol: FirBasedSymbol<*>): FirScope? = firstOrNull {
it.candidate.symbol == symbol
}?.candidate?.originScope
/**
* Returns whether a member of companion is used to initialize the enum entry or not. For example,
* enum class C(val i: Int) {
* ONE(<expr>C.K</expr>) // C.ONE uses C.K for initialization
* ;
* companion object {
* const val K = 1
* }
* }
*/
private fun KtExpression.isCompanionMemberUsedForEnumEntryInit(resolvedSymbol: FirCallableSymbol<*>): Boolean {
val enumEntry = getNonStrictParentOfType<KtEnumEntry>() ?: return false
val firEnumEntry = enumEntry.resolveToFirSymbol(firResolveSession) as? FirEnumEntrySymbol ?: return false
val classNameOfResolvedSymbol = resolvedSymbol.callableId.className ?: return false
return firEnumEntry.callableId.className == classNameOfResolvedSymbol.parent() &&
classNameOfResolvedSymbol.shortName() == SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT
}
/**
* Returns whether it is fine to shorten [firQualifiedAccess] or not.
*
* @param firQualifiedAccess FIR for the shortening target expression
* @param calledSymbol The symbol referenced by the qualified access expression
* @param expressionInScope An expression under the same scope as the shortening target expression
*
* The decision has two steps:
* 1. Collect all candidates matching [firQualifiedAccess]
* - We use `AllCandidatesResolver(shorteningContext.analysisSession.useSiteSession).getAllCandidates( .. fake FIR .. )`. See
* [resolveUnqualifiedAccess] above.
* 2. Check whether the candidate with the highest priority based on the distance to the scope from [expressionInScope] is the same
* as [calledSymbol] ot not
* - We use [hasScopeCloserThan] to determine the distance to the scope
*/
private fun shortenIfAlreadyImported(
firQualifiedAccess: FirQualifiedAccessExpression,
calledSymbol: FirCallableSymbol<*>,
expressionInScope: KtExpression,
): Boolean {
/**
* Avoid shortening reference to enum companion used in enum entry initialization there is no guarantee that the companion object
* was initialized in advance.
* For example, When we shorten the following code:
* enum class C(val i: Int) {
* ONE(<expr>C.K</expr>) // shorten C.K to K
* ;
* companion object {
* const val K = 1
* }
* }
*
* the compiler reports "Variable 'K' must be initialized". This happens because there is no guarantee that the companion object
* was initialized at the time when we use `C.K` for the enum entry `ONE`. To avoid this type of compiler errors, we don't shorten
* the reference if it is a part of the enum companion object, and it is used by the enum entry initialization.
*/
if (expressionInScope.isCompanionMemberUsedForEnumEntryInit(calledSymbol)) return false
val candidates = resolveUnqualifiedAccess(firQualifiedAccess, calledSymbol.name, expressionInScope)
val scopeForQualifiedAccess = candidates.findScopeForSymbol(calledSymbol) ?: return false
if (candidates.mapNotNull { it.candidate.originScope }
.hasScopeCloserThan(scopeForQualifiedAccess, expressionInScope)) return false
val candidatesWithinSamePriorityScopes = candidates.filter { it.candidate.originScope == scopeForQualifiedAccess }
if (candidatesWithinSamePriorityScopes.isEmpty() || candidatesWithinSamePriorityScopes.size == 1) return true
/**
* This is a conservative decision to avoid false positives.
*
* TODO: Figure out the priorities among `candidatesWithinSamePriorityScopes` and determine if [firQualifiedAccess] matches the
* one with the highest priority. At this moment, we have some counter examples that [OverloadCandidate.isInBestCandidates] is true
* and its symbol matches [firQualifiedAccess], but we cannot shorten it.
*
* For example:
* package foo
* class Foo {
* fun test() {
* // It references FIRST. Removing `foo` lets it reference SECOND. However, the one has true for
* // [OverloadCandidate.isInBestCandidates] is FIRST. Therefore, making a decision based on `isInBestCandidates` can
* // cause false positives i.e., shortening changes the referenced symbol.
* <caret>foo.myRun {
* 42
* }
* }
* }
* inline fun <R> myRun(block: () -> R): R = block() // FIRST
* inline fun <T, R> T.myRun(block: T.() -> R): R = block() // SECOND
*/
return false
}
private fun processPropertyReference(resolvedNamedReference: FirResolvedNamedReference) {
val referenceExpression = resolvedNamedReference.psi as? KtNameReferenceExpression ?: return
if (!referenceExpression.textRange.intersects(selection)) return
val qualifiedProperty = referenceExpression.getDotQualifiedExpressionForSelector() ?: return
val callableSymbol = resolvedNamedReference.resolvedSymbol as? FirCallableSymbol<*> ?: return
processCallableQualifiedAccess(callableSymbol, qualifiedProperty, qualifiedProperty, shorteningContext::findPropertiesInScopes)
val option = callableShortenOption(callableSymbol)
if (option == ShortenOption.DO_NOT_SHORTEN) return
val scopes = shorteningContext.findScopesAtPosition(qualifiedProperty, namesToImport, towerContextProvider) ?: return
val availableCallables = shorteningContext.findPropertiesInScopes(scopes, callableSymbol.name)
val firPropertyAccess = qualifiedProperty.getOrBuildFir(firResolveSession) as? FirQualifiedAccessExpression ?: return
if (availableCallables.isNotEmpty() && shortenIfAlreadyImported(firPropertyAccess, callableSymbol, referenceExpression)) {
addElementToShorten(ShortenQualifier(qualifiedProperty))
return
}
if (option == ShortenOption.SHORTEN_IF_ALREADY_IMPORTED) return
processCallableQualifiedAccess(
callableSymbol,
option,
qualifiedProperty,
availableCallables,
)
}
private fun processFunctionCall(functionCall: FirFunctionCall) {
@@ -487,21 +882,34 @@ private class ElementsToShortenCollector(
val calleeReference = functionCall.calleeReference
val calledSymbol = findUnambiguousReferencedCallableId(calleeReference) ?: return
processCallableQualifiedAccess(calledSymbol, qualifiedCallExpression, callExpression, shorteningContext::findFunctionsInScopes)
val option = callableShortenOption(calledSymbol)
if (option == ShortenOption.DO_NOT_SHORTEN) return
val scopes = shorteningContext.findScopesAtPosition(callExpression, namesToImport, towerContextProvider) ?: return
val availableCallables = shorteningContext.findFunctionsInScopes(scopes, calledSymbol.name)
if (availableCallables.isNotEmpty() && shortenIfAlreadyImported(functionCall, calledSymbol, callExpression)) {
addElementToShorten(ShortenQualifier(qualifiedCallExpression))
return
}
if (option == ShortenOption.SHORTEN_IF_ALREADY_IMPORTED) return
processCallableQualifiedAccess(
calledSymbol,
option,
qualifiedCallExpression,
availableCallables,
)
}
private fun processCallableQualifiedAccess(
calledSymbol: FirCallableSymbol<*>,
option: ShortenOption,
qualifiedCallExpression: KtDotQualifiedExpression,
expressionToGetScope: KtExpression,
findCallableInScopes: (List<FirScope>, Name) -> List<AvailableSymbol<FirCallableSymbol<*>>>,
availableCallables: List<AvailableSymbol<FirCallableSymbol<*>>>,
) {
val option = callableShortenOption(calledSymbol)
if (option == ShortenOption.DO_NOT_SHORTEN) return
val scopes = shorteningContext.findScopesAtPosition(expressionToGetScope, namesToImport, towerContextProvider) ?: return
val availableCallables = findCallableInScopes(scopes, calledSymbol.name)
val nameToImport = shorteningContext.convertToImportableName(calledSymbol)
val (matchedCallables, otherCallables) = availableCallables.partition { it.symbol.callableId == calledSymbol.callableId }
@@ -609,11 +1017,11 @@ private class ElementsToShortenCollector(
}
private class ShortenCommandImpl(
val targetFile: SmartPsiElementPointer<KtFile>,
val importsToAdd: List<FqName>,
val starImportsToAdd: List<FqName>,
val typesToShorten: List<SmartPsiElementPointer<KtUserType>>,
val qualifiersToShorten: List<SmartPsiElementPointer<KtDotQualifiedExpression>>,
private val targetFile: SmartPsiElementPointer<KtFile>,
private val importsToAdd: List<FqName>,
private val starImportsToAdd: List<FqName>,
private val typesToShorten: List<SmartPsiElementPointer<KtUserType>>,
private val qualifiersToShorten: List<SmartPsiElementPointer<KtDotQualifiedExpression>>,
) : ShortenCommand {
override fun invokeShortening() {
@@ -643,6 +1051,10 @@ private class ShortenCommandImpl(
}
override val isEmpty: Boolean get() = typesToShorten.isEmpty() && qualifiersToShorten.isEmpty()
override fun getTypesToShorten(): List<SmartPsiElementPointer<KtUserType>> = typesToShorten
override fun getQualifiersToShorten(): List<SmartPsiElementPointer<KtDotQualifiedExpression>> = qualifiersToShorten
}
private fun KtUserType.hasFakeRootPrefix(): Boolean =
@@ -657,4 +1069,4 @@ internal fun KtElement.getDotQualifiedExpressionForSelector(): KtDotQualifiedExp
private fun KtDotQualifiedExpression.deleteQualifier(): KtExpression? {
val selectorExpression = selectorExpression ?: return null
return this.replace(selectorExpression) as KtExpression
}
}
@@ -0,0 +1,318 @@
/*
* Copyright 2010-2023 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.analysis.api.fir.test.cases.generated.cases.references;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.util.KtTestUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.analysis.api.fir.test.configurators.AnalysisApiFirTestConfiguratorFactory;
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfiguratorFactoryData;
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfigurator;
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.TestModuleKind;
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.FrontendKind;
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisSessionMode;
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiMode;
import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.references.AbstractReferenceShortenerTest;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.analysis.api.GenerateAnalysisApiTestsKt}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("analysis/analysis-api/testData/components/referenceShortener/referenceShortener")
@TestDataPath("$PROJECT_ROOT")
public class FirIdeNormalAnalysisSourceModuleReferenceShortenerTestGenerated extends AbstractReferenceShortenerTest {
@NotNull
@Override
public AnalysisApiTestConfigurator getConfigurator() {
return AnalysisApiFirTestConfiguratorFactory.INSTANCE.createConfigurator(
new AnalysisApiTestConfiguratorFactoryData(
FrontendKind.Fir,
TestModuleKind.Source,
AnalysisSessionMode.Normal,
AnalysisApiMode.Ide
)
);
}
@Test
public void testAllFilesPresentInReferenceShortener() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/analysis-api/testData/components/referenceShortener/referenceShortener"), Pattern.compile("^(.+)\\.kt$"), null, true);
}
@Test
@TestMetadata("classScopes.kt")
public void testClassScopes() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/classScopes.kt");
}
@Test
@TestMetadata("classScopes2.kt")
public void testClassScopes2() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/classScopes2.kt");
}
@Test
@TestMetadata("classScopes3.kt")
public void testClassScopes3() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/classScopes3.kt");
}
@Test
@TestMetadata("classType.kt")
public void testClassType() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/classType.kt");
}
@Test
@TestMetadata("classType2.kt")
public void testClassType2() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/classType2.kt");
}
@Test
@TestMetadata("classesWithSameName.kt")
public void testClassesWithSameName() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/classesWithSameName.kt");
}
@Test
@TestMetadata("classesWithSameName2.kt")
public void testClassesWithSameName2() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/classesWithSameName2.kt");
}
@Test
@TestMetadata("classesWithSameName3.kt")
public void testClassesWithSameName3() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/classesWithSameName3.kt");
}
@Test
@TestMetadata("classesWithSameName4.kt")
public void testClassesWithSameName4() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/classesWithSameName4.kt");
}
@Test
@TestMetadata("classesWithSameName5.kt")
public void testClassesWithSameName5() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/classesWithSameName5.kt");
}
@Test
@TestMetadata("classesWithSameName6.kt")
public void testClassesWithSameName6() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/classesWithSameName6.kt");
}
@Test
@TestMetadata("classesWithSameName7.kt")
public void testClassesWithSameName7() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/classesWithSameName7.kt");
}
@Test
@TestMetadata("classesWithSameName8.kt")
public void testClassesWithSameName8() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/classesWithSameName8.kt");
}
@Test
@TestMetadata("classesWithSameName9.kt")
public void testClassesWithSameName9() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/classesWithSameName9.kt");
}
@Test
@TestMetadata("companionClassLiteral.kt")
public void testCompanionClassLiteral() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/companionClassLiteral.kt");
}
@Test
@TestMetadata("companionClassLiteral2.kt")
public void testCompanionClassLiteral2() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/companionClassLiteral2.kt");
}
@Test
@TestMetadata("companionClassLiteral3.kt")
public void testCompanionClassLiteral3() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/companionClassLiteral3.kt");
}
@Test
@TestMetadata("companionQualifier.kt")
public void testCompanionQualifier() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/companionQualifier.kt");
}
@Test
@TestMetadata("companionUsedOutOfClass.kt")
public void testCompanionUsedOutOfClass() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/companionUsedOutOfClass.kt");
}
@Test
@TestMetadata("enumClassCompanionAlreadyImported.kt")
public void testEnumClassCompanionAlreadyImported() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/enumClassCompanionAlreadyImported.kt");
}
@Test
@TestMetadata("enumEntryInitUsesCompanion.kt")
public void testEnumEntryInitUsesCompanion() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/enumEntryInitUsesCompanion.kt");
}
@Test
@TestMetadata("enumEntryInitUsesCompanion2.kt")
public void testEnumEntryInitUsesCompanion2() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/enumEntryInitUsesCompanion2.kt");
}
@Test
@TestMetadata("nestedClass.kt")
public void testNestedClass() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/nestedClass.kt");
}
@Test
@TestMetadata("nestedClass2.kt")
public void testNestedClass2() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/nestedClass2.kt");
}
@Test
@TestMetadata("nestedClass3.kt")
public void testNestedClass3() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/nestedClass3.kt");
}
@Test
@TestMetadata("parameterTypeTopLevelTypeLoses.kt")
public void testParameterTypeTopLevelTypeLoses() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/parameterTypeTopLevelTypeLoses.kt");
}
@Test
@TestMetadata("receiver.kt")
public void testReceiver() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/receiver.kt");
}
@Test
@TestMetadata("receiver2.kt")
public void testReceiver2() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/receiver2.kt");
}
@Test
@TestMetadata("receiver3.kt")
public void testReceiver3() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/receiver3.kt");
}
@Test
@TestMetadata("receiver4.kt")
public void testReceiver4() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/receiver4.kt");
}
@Test
@TestMetadata("referenceInNestedClass.kt")
public void testReferenceInNestedClass() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/referenceInNestedClass.kt");
}
@Test
@TestMetadata("sameNameDifferentParams.kt")
public void testSameNameDifferentParams() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/sameNameDifferentParams.kt");
}
@Test
@TestMetadata("sameNameDifferentReceiver.kt")
public void testSameNameDifferentReceiver() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/sameNameDifferentReceiver.kt");
}
@Test
@TestMetadata("sameTypeNamesWithinSameScopes.kt")
public void testSameTypeNamesWithinSameScopes() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/sameTypeNamesWithinSameScopes.kt");
}
@Test
@TestMetadata("shortenAlreadyImportedClass.kt")
public void testShortenAlreadyImportedClass() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/shortenAlreadyImportedClass.kt");
}
@Test
@TestMetadata("shortenAlreadyImportedClass2.kt")
public void testShortenAlreadyImportedClass2() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/shortenAlreadyImportedClass2.kt");
}
@Test
@TestMetadata("shortenAlreadyImportedFunction.kt")
public void testShortenAlreadyImportedFunction() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/shortenAlreadyImportedFunction.kt");
}
@Test
@TestMetadata("shortenAlreadyImportedFunction2.kt")
public void testShortenAlreadyImportedFunction2() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/shortenAlreadyImportedFunction2.kt");
}
@Test
@TestMetadata("shortenAlreadyImportedFunction3.kt")
public void testShortenAlreadyImportedFunction3() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/shortenAlreadyImportedFunction3.kt");
}
@Test
@TestMetadata("shortenAlreadyImportedFunction4.kt")
public void testShortenAlreadyImportedFunction4() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/shortenAlreadyImportedFunction4.kt");
}
@Test
@TestMetadata("superClass.kt")
public void testSuperClass() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/superClass.kt");
}
@Test
@TestMetadata("typeParams.kt")
public void testTypeParams() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/typeParams.kt");
}
@Test
@TestMetadata("typeParams2.kt")
public void testTypeParams2() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/typeParams2.kt");
}
@Test
@TestMetadata("variable.kt")
public void testVariable() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/variable.kt");
}
@Test
@TestMetadata("variable2.kt")
public void testVariable2() throws Exception {
runTest("analysis/analysis-api/testData/components/referenceShortener/referenceShortener/variable2.kt");
}
}