FIR IDE: Add fine-grained control to KtReferenceShortener

This change makes it possible to control how references to a symbol should be shortened.
This commit is contained in:
Tianyu Geng
2021-05-27 15:25:45 -07:00
committed by TeamCityServer
parent 8ac2a48eaf
commit 726d141589
10 changed files with 346 additions and 163 deletions
@@ -1,3 +1,4 @@
// FIR_COMPARISON
open class B {
open var someVar: String = ""
}
@@ -1,3 +1,4 @@
// FIR_COMPARISON
open class B {
open var someVar: String = ""
}
@@ -6,17 +6,77 @@
package org.jetbrains.kotlin.idea.frontend.api.components
import com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtCallableSymbol
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtClassLikeSymbol
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtEnumEntrySymbol
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtFile
enum class ShortenOption {
/** Skip shortening references to this symbol. */
DO_NOT_SHORTEN,
/** Only shorten references to this symbol if it's already imported in the file. Otherwise, leave it as it is. */
SHORTEN_IF_ALREADY_IMPORTED,
/** Shorten references to this symbol and import it into the file. */
SHORTEN_AND_IMPORT,
/** Shorten references to this symbol and import this symbol and all its sibling symbols with star import on the parent. */
SHORTEN_AND_STAR_IMPORT
}
abstract class KtReferenceShortener : KtAnalysisSessionComponent() {
abstract fun collectShortenings(file: KtFile, selection: TextRange): ShortenCommand
abstract fun collectShortenings(
file: KtFile,
selection: TextRange,
classShortenOption: (KtClassLikeSymbol) -> ShortenOption,
callableShortenOption: (KtCallableSymbol) -> ShortenOption
): ShortenCommand
}
interface KtReferenceShortenerMixIn : KtAnalysisSessionMixIn {
fun collectPossibleReferenceShortenings(file: KtFile, selection: TextRange): ShortenCommand =
analysisSession.referenceShortener.collectShortenings(file, selection)
companion object {
private val defaultClassShortenOption: (KtClassLikeSymbol) -> ShortenOption = {
if (it.classIdIfNonLocal?.isNestedClass == true) {
ShortenOption.SHORTEN_IF_ALREADY_IMPORTED
} else {
ShortenOption.SHORTEN_AND_IMPORT
}
}
private val defaultCallableShortenOption: (KtCallableSymbol) -> ShortenOption = { symbol ->
if (symbol is KtEnumEntrySymbol) ShortenOption.DO_NOT_SHORTEN
else ShortenOption.SHORTEN_AND_IMPORT
}
}
/**
* Collects possible references to shorten. By default, it shortens a fully-qualified members to the outermost class and does not
* shorten enum entries.
*/
fun collectPossibleReferenceShortenings(
file: KtFile,
selection: TextRange = file.textRange,
classShortenOption: (KtClassLikeSymbol) -> ShortenOption = defaultClassShortenOption,
callableShortenOption: (KtCallableSymbol) -> ShortenOption = defaultCallableShortenOption
): ShortenCommand =
analysisSession.referenceShortener.collectShortenings(file, selection, classShortenOption, callableShortenOption)
fun collectPossibleReferenceShorteningsInElement(
element: KtElement,
classShortenOption: (KtClassLikeSymbol) -> ShortenOption = defaultClassShortenOption,
callableShortenOption: (KtCallableSymbol) -> ShortenOption = defaultCallableShortenOption
): ShortenCommand =
analysisSession.referenceShortener.collectShortenings(
element.containingKtFile,
element.textRange,
classShortenOption,
callableShortenOption
)
}
interface ShortenCommand {
fun invokeShortening()
val isEmpty: Boolean
}
@@ -82,10 +82,14 @@ abstract class KtNamedClassOrObjectSymbol : KtClassOrObjectSymbol(),
}
enum class KtClassKind {
CLASS, ENUM_CLASS, ENUM_ENTRY, ANNOTATION_CLASS, OBJECT, COMPANION_OBJECT, INTERFACE, ANONYMOUS_OBJECT
}
CLASS,
ENUM_CLASS,
ENUM_ENTRY,
ANNOTATION_CLASS,
OBJECT,
COMPANION_OBJECT,
INTERFACE,
ANONYMOUS_OBJECT;
val KtClassKind.isObject
get() = this == KtClassKind.OBJECT
|| this == KtClassKind.COMPANION_OBJECT
|| this == KtClassKind.ANONYMOUS_OBJECT
val isObject: Boolean get() = this == OBJECT || this == COMPANION_OBJECT || this == ANONYMOUS_OBJECT
}
@@ -9,17 +9,16 @@ import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.util.TextRange
import com.intellij.psi.SmartPsiElementPointer
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.fir.FirElement
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.ROOT_PREFIX_FOR_IDE_RESOLUTION_MODE
import org.jetbrains.kotlin.fir.*
import org.jetbrains.kotlin.fir.analysis.checkers.toRegularClass
import org.jetbrains.kotlin.fir.declarations.*
import org.jetbrains.kotlin.fir.declarations.FirDeclarationOrigin
import org.jetbrains.kotlin.fir.declarations.FirRegularClass
import org.jetbrains.kotlin.fir.declarations.FirResolvedImport
import org.jetbrains.kotlin.fir.declarations.builder.buildImport
import org.jetbrains.kotlin.fir.declarations.builder.buildResolvedImport
import org.jetbrains.kotlin.fir.expressions.FirFunctionCall
import org.jetbrains.kotlin.fir.expressions.FirResolvedQualifier
import org.jetbrains.kotlin.fir.expressions.impl.FirNoReceiverExpression
import org.jetbrains.kotlin.fir.psi
import org.jetbrains.kotlin.fir.references.FirErrorNamedReference
import org.jetbrains.kotlin.fir.references.FirNamedReference
import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference
@@ -30,11 +29,8 @@ import org.jetbrains.kotlin.fir.resolve.transformers.resolveToPackageOrClass
import org.jetbrains.kotlin.fir.scopes.FirScope
import org.jetbrains.kotlin.fir.scopes.getFunctions
import org.jetbrains.kotlin.fir.scopes.getProperties
import org.jetbrains.kotlin.fir.scopes.impl.FirAbstractStarImportingScope
import org.jetbrains.kotlin.fir.scopes.impl.FirExplicitSimpleImportingScope
import org.jetbrains.kotlin.fir.scopes.impl.FirPackageMemberScope
import org.jetbrains.kotlin.fir.scopes.impl.*
import org.jetbrains.kotlin.fir.scopes.processClassifiersByName
import org.jetbrains.kotlin.name.CallableId
import org.jetbrains.kotlin.fir.symbols.ConeClassLikeLookupTag
import org.jetbrains.kotlin.fir.symbols.impl.*
import org.jetbrains.kotlin.fir.types.FirResolvedTypeRef
@@ -47,16 +43,19 @@ import org.jetbrains.kotlin.idea.fir.low.level.api.api.LowLevelFirApiFacadeForRe
import org.jetbrains.kotlin.idea.fir.low.level.api.api.getOrBuildFir
import org.jetbrains.kotlin.idea.fir.low.level.api.element.builder.FirTowerContextProvider
import org.jetbrains.kotlin.idea.fir.low.level.api.util.parentsOfType
import org.jetbrains.kotlin.idea.frontend.api.tokens.ValidityToken
import org.jetbrains.kotlin.idea.frontend.api.components.KtReferenceShortener
import org.jetbrains.kotlin.idea.frontend.api.components.ShortenCommand
import org.jetbrains.kotlin.idea.frontend.api.components.ShortenOption
import org.jetbrains.kotlin.idea.frontend.api.fir.KtFirAnalysisSession
import org.jetbrains.kotlin.idea.frontend.api.fir.utils.addImportToFile
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtCallableSymbol
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtClassLikeSymbol
import org.jetbrains.kotlin.idea.frontend.api.tokens.ValidityToken
import org.jetbrains.kotlin.name.*
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.*
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.utils.addIfNotNull
internal class KtFirReferenceShortener(
@@ -66,7 +65,12 @@ internal class KtFirReferenceShortener(
) : KtReferenceShortener(), KtFirAnalysisSessionComponent {
private val context = FirShorteningContext(firResolveState)
override fun collectShortenings(file: KtFile, selection: TextRange): ShortenCommand {
override fun collectShortenings(
file: KtFile,
selection: TextRange,
classShortenOption: (KtClassLikeSymbol) -> ShortenOption,
callableShortenOption: (KtCallableSymbol) -> ShortenOption
): ShortenCommand {
val declarationToVisit = file.findSmallestDeclarationContainingSelection(selection)
?: file.withDeclarationsResolvedToBodyResolve()
@@ -75,12 +79,19 @@ internal class KtFirReferenceShortener(
val towerContext =
LowLevelFirApiFacadeForResolveOnAir.onAirGetTowerContextProvider(firResolveState, declarationToVisit)
val collector = ElementsToShortenCollector(context, towerContext)
//TODO: collect all usages of available symbols in the file and prevent importing symbols that could introduce name clashes, which
// may alter the meaning of existing code.
val collector = ElementsToShortenCollector(
context,
towerContext,
classShortenOption = { classShortenOption(analysisSession.firSymbolBuilder.buildSymbol(it.fir) as KtClassLikeSymbol) },
callableShortenOption = { callableShortenOption(analysisSession.firSymbolBuilder.buildSymbol(it.fir) as KtCallableSymbol) })
firDeclaration.accept(collector)
return ShortenCommandImpl(
file,
collector.namesToImport.distinct(),
collector.namesToImportWithStar.distinct(),
collector.typesToShorten.distinct().map { it.createSmartPointer() },
collector.qualifiersToShorten.distinct().map { it.createSmartPointer() }
)
@@ -100,33 +111,84 @@ private fun KtFile.findSmallestDeclarationContainingSelection(selection: TextRan
?.parentsOfType<KtDeclaration>(withSelf = true)
?.firstOrNull { selection in it.textRange }
private data class AvailableClassifier(val classId: ClassId, val isFromStarOrPackageImport: Boolean)
/**
* How a symbol is imported. The order of the enum entry represents the priority of imports. If a symbol is available from multiple kinds of
* imports, the symbol from "smaller" kind is used. For example, an explicitly imported symbol can overwrite a star-imported symbol.
*/
private enum class ImportKind {
/** The symbol is available from the local scope and hence cannot be imported or overwritten. */
LOCAL,
/** Explicitly imported by user. */
EXPLICIT,
/** Explicitly imported by Kotlin default. For example, `kotlin.String`. */
DEFAULT_EXPLICIT,
/** Implicitly imported from package. */
PACKAGE,
/** Star imported (star import) by user. */
STAR,
/** Star imported (star import) by Kotlin default. */
DEFAULT_STAR;
infix fun hasHigherPriorityThan(that: ImportKind): Boolean = this < that
val canBeOverwrittenByExplicitImport: Boolean get() = DEFAULT_EXPLICIT hasHigherPriorityThan this
companion object {
fun fromScope(scope: FirScope): ImportKind {
return when (scope) {
is FirDefaultStarImportingScope -> DEFAULT_STAR
is FirAbstractStarImportingScope -> STAR
is FirPackageMemberScope -> PACKAGE
is FirDefaultSimpleImportingScope -> DEFAULT_EXPLICIT
is FirAbstractSimpleImportingScope -> EXPLICIT
else -> LOCAL
}
}
}
}
private data class AvailableSymbol<out T>(
val symbol: T,
val importKind: ImportKind,
)
private class FirShorteningContext(val firResolveState: FirModuleResolveState) {
private val firSession: FirSession
get() = firResolveState.rootModuleSession
fun findFirstClassifierInScopesByName(positionScopes: List<FirScope>, targetClassName: Name): AvailableClassifier? {
fun findFirstClassifierInScopesByName(positionScopes: List<FirScope>, targetClassName: Name): AvailableSymbol<ClassId>? {
for (scope in positionScopes) {
val classifierSymbol = scope.findFirstClassifierByName(targetClassName) ?: continue
val classifierLookupTag = classifierSymbol.toLookupTag() as? ConeClassLikeLookupTag ?: continue
return AvailableClassifier(
classifierLookupTag.classId,
isFromStarOrPackageImport = scope is FirAbstractStarImportingScope || scope is FirPackageMemberScope
)
return AvailableSymbol(classifierLookupTag.classId, ImportKind.fromScope(scope))
}
return null
}
fun findFunctionsInScopes(scopes: List<FirScope>, name: Name): List<FirNamedFunctionSymbol> {
return scopes.flatMap { it.getFunctions(name) }
fun findFunctionsInScopes(scopes: List<FirScope>, name: Name): List<AvailableSymbol<FirNamedFunctionSymbol>> {
return scopes.flatMap { scope ->
val importKind = ImportKind.fromScope(scope)
scope.getFunctions(name).map {
AvailableSymbol(it, importKind)
}
}
}
fun findPropertiesInScopes(scopes: List<FirScope>, name: Name): List<FirVariableSymbol<*>> {
return scopes.flatMap { it.getProperties(name) }
fun findPropertiesInScopes(scopes: List<FirScope>, name: Name): List<AvailableSymbol<FirVariableSymbol<*>>> {
return scopes.flatMap { scope ->
val importKind = ImportKind.fromScope(scope)
scope.getProperties(name).map {
AvailableSymbol(it, importKind)
}
}
}
private fun FirScope.findFirstClassifierByName(name: Name): FirClassifierSymbol<*>? {
@@ -181,21 +243,56 @@ private class FirShorteningContext(val firResolveState: FirModuleResolveState) {
fun getRegularClass(typeRef: FirTypeRef): FirRegularClass? {
return typeRef.toRegularClass(firSession)
}
fun toClassSymbol(classId: ClassId) =
firSession.symbolProvider.getClassLikeSymbolByFqName(classId)
fun convertToImportableName(callableId: CallableId): FqName? {
val classId = callableId.classId
return if (classId == null) {
callableId.packageName.child(callableId.callableName)
} else {
// Java static members, enums, and object members can be imported
val containingClass = firSession.symbolProvider.getClassLikeSymbolByFqName(classId)?.fir as? FirRegularClass ?: return null
if (containingClass.origin == FirDeclarationOrigin.Java ||
containingClass.classKind == ClassKind.ENUM_CLASS ||
containingClass.classKind == ClassKind.OBJECT
) {
callableId.asSingleFqName()
} else {
null
}
}
}
}
private sealed class ElementToShorten
private class ShortenType(val element: KtUserType, val nameToImport: FqName? = null) : ElementToShorten()
private class ShortenQualifier(val element: KtDotQualifiedExpression, val nameToImport: FqName? = null) : ElementToShorten() {
fun withElement(newElement: KtDotQualifiedExpression) = ShortenQualifier(newElement, nameToImport)
private sealed class ElementToShorten {
abstract val nameToImport: FqName?
abstract val importAllInParent: Boolean
}
private class ShortenType(
val element: KtUserType,
override val nameToImport: FqName? = null,
override val importAllInParent: Boolean = false
) : ElementToShorten()
private class ShortenQualifier(
val element: KtDotQualifiedExpression,
override val nameToImport: FqName? = null,
override val importAllInParent: Boolean = false
) : ElementToShorten()
private class ElementsToShortenCollector(
private val shorteningContext: FirShorteningContext,
private val towerContextProvider: FirTowerContextProvider
private val towerContextProvider: FirTowerContextProvider,
private val classShortenOption: (FirClassLikeSymbol<*>) -> ShortenOption,
private val callableShortenOption: (FirCallableSymbol<*>) -> ShortenOption,
) :
FirVisitorVoid() {
val namesToImport: MutableList<FqName> = mutableListOf()
val namesToImportWithStar: MutableList<FqName> = mutableListOf()
val typesToShorten: MutableList<KtUserType> = mutableListOf()
val qualifiersToShorten: MutableList<KtDotQualifiedExpression> = mutableListOf()
@@ -239,35 +336,18 @@ private class ElementsToShortenCollector(
findTypeToShorten(wholeClassifierId, wholeTypeElement)?.let(::addElementToShorten)
}
private fun findTypeToShorten(wholeClassifierId: ClassId, wholeTypeElement: KtUserType): ShortenType? {
private fun findTypeToShorten(wholeClassifierId: ClassId, wholeTypeElement: KtUserType): ElementToShorten? {
val positionScopes = shorteningContext.findScopesAtPosition(wholeTypeElement, namesToImport, towerContextProvider) ?: return null
val allClassIds = wholeClassifierId.outerClassesWithSelf
val allTypeElements = wholeTypeElement.qualifiersWithSelf
val positionScopes = shorteningContext.findScopesAtPosition(wholeTypeElement, namesToImport, towerContextProvider) ?: return null
for ((classId, typeElement) in allClassIds.zip(allTypeElements)) {
// if qualifier is null, then this type have no package and thus cannot be shortened
if (typeElement.qualifier == null) return null
val firstFoundClass = shorteningContext.findFirstClassifierInScopesByName(positionScopes, classId.shortClassName)?.classId
if (firstFoundClass == classId) {
return ShortenType(typeElement)
}
}
// none class matched
val (mostTopLevelClassId, mostTopLevelTypeElement) = allClassIds.zip(allTypeElements).last()
val availableClassifier = shorteningContext.findFirstClassifierInScopesByName(positionScopes, mostTopLevelClassId.shortClassName)
check(availableClassifier?.classId != mostTopLevelClassId) {
"If this condition were true, we would have exited from the loop on the last iteration. ClassId = $mostTopLevelClassId"
}
return if (availableClassifier == null || availableClassifier.isFromStarOrPackageImport) {
ShortenType(mostTopLevelTypeElement, mostTopLevelClassId.asSingleFqName())
} else {
findFakePackageToShorten(mostTopLevelTypeElement)
return findClassifierElementsToShorten(
positionScopes,
allClassIds,
allTypeElements,
::ShortenType,
this::findFakePackageToShorten
) {
it.qualifier != null
}
}
@@ -292,60 +372,79 @@ private class ElementsToShortenCollector(
private fun findTypeQualifierToShorten(
wholeClassQualifier: ClassId,
wholeQualifierElement: KtDotQualifiedExpression
): ShortenQualifier? {
val positionScopes =
): ElementToShorten? {
val positionScopes: List<FirScope> =
shorteningContext.findScopesAtPosition(wholeQualifierElement, namesToImport, towerContextProvider) ?: return null
val allClassIds: Sequence<ClassId> = wholeClassQualifier.outerClassesWithSelf
val allQualifiers: Sequence<KtDotQualifiedExpression> = wholeQualifierElement.qualifiersWithSelf
return findClassifierElementsToShorten(
positionScopes,
allClassIds,
allQualifiers,
::ShortenQualifier,
this::findFakePackageToShorten
)
}
val allClassIds = wholeClassQualifier.outerClassesWithSelf
val allQualifiers = wholeQualifierElement.qualifiersWithSelf
private inline fun <E> findClassifierElementsToShorten(
positionScopes: List<FirScope>,
allClassIds: Sequence<ClassId>,
allElements: Sequence<E>,
createElementToShorten: (E, nameToImport: FqName?, importAllInParent: Boolean) -> ElementToShorten,
findFakePackageToShortenFn: (E) -> ElementToShorten?,
elementFilter: (E) -> Boolean = { true },
): ElementToShorten? {
for ((classId, qualifier) in allClassIds.zip(allQualifiers)) {
val firstFoundClass = shorteningContext.findFirstClassifierInScopesByName(positionScopes, classId.shortClassName)?.classId
for ((classId, element) in allClassIds.zip(allElements)) {
if (!elementFilter(element)) return null
val option = classShortenOption(shorteningContext.toClassSymbol(classId) ?: return null)
if (option == ShortenOption.DO_NOT_SHORTEN) continue
if (firstFoundClass == classId) {
return ShortenQualifier(qualifier)
// Find class with the same name that's already available in this file.
val availableClassifier = shorteningContext.findFirstClassifierInScopesByName(positionScopes, classId.shortClassName)
when {
// No class with name `classId.shortClassName` is present in the scope. Hence, we can safely import the name and shorten
// the reference.
availableClassifier == null -> {
// Caller indicates don't shorten if doing that needs importing more names. Hence, we just skip.
if (option == ShortenOption.SHORTEN_IF_ALREADY_IMPORTED) continue
return createElementToShorten(
element,
classId.asSingleFqName(),
option == ShortenOption.SHORTEN_AND_STAR_IMPORT
)
}
// The class with name `classId.shortClassName` happens to be the same class referenced by this qualified access.
availableClassifier.symbol == classId -> {
// Respect caller's request to use star import, if it's not already star-imported.
return when {
availableClassifier.importKind == ImportKind.EXPLICIT && option == ShortenOption.SHORTEN_AND_STAR_IMPORT -> {
createElementToShorten(element, classId.asSingleFqName(), true)
}
// Otherwise, just shorten it and don't alter import statements
else -> createElementToShorten(element, null, false)
}
}
// Allow using star import to overwrite members implicitly imported by default.
availableClassifier.importKind == ImportKind.DEFAULT_STAR && option == ShortenOption.SHORTEN_AND_STAR_IMPORT -> {
return createElementToShorten(element, classId.asSingleFqName(), true)
}
// Allow using explicit import to overwrite members star-imported or in package
availableClassifier.importKind.canBeOverwrittenByExplicitImport && option == ShortenOption.SHORTEN_AND_IMPORT -> {
return createElementToShorten(element, classId.asSingleFqName(), false)
}
}
}
val (mostTopLevelClassId, mostTopLevelQualifier) = allClassIds.zip(allQualifiers).last()
val availableClassifier = shorteningContext.findFirstClassifierInScopesByName(positionScopes, mostTopLevelClassId.shortClassName)
check(availableClassifier?.classId != mostTopLevelClassId) {
"If this condition were true, we would have exited from the loop on the last iteration. ClassId = $mostTopLevelClassId"
}
return if (availableClassifier == null || availableClassifier.isFromStarOrPackageImport) {
ShortenQualifier(mostTopLevelQualifier, mostTopLevelClassId.asSingleFqName())
} else {
findFakePackageToShorten(mostTopLevelQualifier)
}
return findFakePackageToShortenFn(allElements.last())
}
private fun processPropertyReference(resolvedNamedReference: FirResolvedNamedReference) {
val referenceExpression = resolvedNamedReference.psi as? KtNameReferenceExpression
val qualifiedProperty = referenceExpression?.getDotQualifiedExpressionForSelector() ?: return
val firCallableSymbol = resolvedNamedReference.resolvedSymbol as? FirCallableSymbol<*> ?: return
val propertyId = firCallableSymbol.callableId
val scopes = shorteningContext.findScopesAtPosition(qualifiedProperty, namesToImport, towerContextProvider) ?: return
val singleAvailableProperty = shorteningContext.findPropertiesInScopes(scopes, propertyId.callableName)
val propertyToShorten = when {
singleAvailableProperty.isEmpty() -> ShortenQualifier(qualifiedProperty, propertyId.asImportableFqName())
singleAvailableProperty.all { it.callableId == propertyId } -> ShortenQualifier(qualifiedProperty)
else -> findFakePackageToShorten(qualifiedProperty)
}
propertyToShorten?.withQualifierForCallablesToShorten(firCallableSymbol)?.let(::addElementToShorten)
}
private fun ShortenQualifier.withQualifierForCallablesToShorten(callableSymbol: FirCallableSymbol<*>): ShortenQualifier? {
if (callableSymbol.fir is FirEnumEntry) {
val newQualifier = element.receiverExpression as? KtDotQualifiedExpression ?: return null
return withElement(newQualifier)
}
return this
val callableSymbol = resolvedNamedReference.resolvedSymbol as? FirCallableSymbol<*> ?: return
processCallableQualifiedAccess(callableSymbol, qualifiedProperty, qualifiedProperty, shorteningContext::findPropertiesInScopes)
}
private fun processFunctionCall(functionCall: FirFunctionCall) {
@@ -355,17 +454,49 @@ private class ElementsToShortenCollector(
val callExpression = qualifiedCallExpression.selectorExpression as? KtCallExpression ?: return
val calleeReference = functionCall.calleeReference
val callableId = findUnambiguousReferencedCallableId(calleeReference) ?: return
val calledSymbol = findUnambiguousReferencedCallableId(calleeReference) ?: return
processCallableQualifiedAccess(calledSymbol, qualifiedCallExpression, callExpression, shorteningContext::findFunctionsInScopes)
}
val scopes = shorteningContext.findScopesAtPosition(callExpression, namesToImport, towerContextProvider) ?: return
val availableCallables = shorteningContext.findFunctionsInScopes(scopes, callableId.callableName)
private fun processCallableQualifiedAccess(
calledSymbol: FirCallableSymbol<*>,
qualifiedCallExpression: KtDotQualifiedExpression,
expressionToGetScope: KtExpression,
findCallableInScopes: (List<FirScope>, Name) -> List<AvailableSymbol<FirCallableSymbol<*>>>,
) {
val option = callableShortenOption(calledSymbol)
if (option == ShortenOption.DO_NOT_SHORTEN) return
val callableId = calledSymbol.callableId
val scopes = shorteningContext.findScopesAtPosition(expressionToGetScope, namesToImport, towerContextProvider) ?: return
val availableCallables = findCallableInScopes(scopes, callableId.callableName)
val nameToImport = if (calledSymbol is FirConstructorSymbol) {
// A constructor is imported by the class name.
calledSymbol.containingClass()?.classId?.asSingleFqName()
} else {
shorteningContext.convertToImportableName(callableId)
}
val (matchedCallables, otherCallables) = availableCallables.partition { it.symbol.callableId == callableId }
val callToShorten = when {
availableCallables.isEmpty() -> {
val additionalImport = callableId.asImportableFqName()
additionalImport?.let { ShortenQualifier(qualifiedCallExpression, it) }
// TODO: instead of allowing import only if the other callables are all with kind `DEFAULT_STAR`, we should allow import if
// the requested import kind has higher priority than the available symbols.
otherCallables.all { it.importKind == ImportKind.DEFAULT_STAR } -> {
when {
matchedCallables.isEmpty() -> {
if (nameToImport == null || option == ShortenOption.SHORTEN_IF_ALREADY_IMPORTED) return
ShortenQualifier(
qualifiedCallExpression,
nameToImport,
importAllInParent = option == ShortenOption.SHORTEN_AND_STAR_IMPORT
)
}
// Respect caller's request to star import this symbol.
matchedCallables.any { it.importKind == ImportKind.EXPLICIT } && option == ShortenOption.SHORTEN_AND_STAR_IMPORT ->
ShortenQualifier(qualifiedCallExpression, nameToImport, importAllInParent = true)
else -> ShortenQualifier(qualifiedCallExpression)
}
}
availableCallables.all { it.callableId == callableId } -> ShortenQualifier(qualifiedCallExpression)
else -> findFakePackageToShorten(qualifiedCallExpression)
}
@@ -383,7 +514,7 @@ private class ElementsToShortenCollector(
return receiverType.classKind != ClassKind.OBJECT
}
private fun findUnambiguousReferencedCallableId(namedReference: FirNamedReference): CallableId? {
private fun findUnambiguousReferencedCallableId(namedReference: FirNamedReference): FirCallableSymbol<*>? {
val unambiguousSymbol = when (namedReference) {
is FirResolvedNamedReference -> namedReference.resolvedSymbol
is FirErrorNamedReference -> {
@@ -397,7 +528,7 @@ private class ElementsToShortenCollector(
else -> null
}
return (unambiguousSymbol as? FirCallableSymbol<*>)?.callableId
return (unambiguousSymbol as? FirCallableSymbol<*>)
}
/**
@@ -421,13 +552,16 @@ private class ElementsToShortenCollector(
}
private fun addElementToShorten(element: ElementToShorten) {
if (element.importAllInParent && element.nameToImport?.parentOrNull()?.isRoot == false) {
namesToImportWithStar.addIfNotNull(element.nameToImport?.parent())
} else {
namesToImport.addIfNotNull(element.nameToImport)
}
when (element) {
is ShortenType -> {
namesToImport.addIfNotNull(element.nameToImport)
typesToShorten.add(element.element)
}
is ShortenQualifier -> {
namesToImport.addIfNotNull(element.nameToImport)
qualifiersToShorten.add(element.element)
}
}
@@ -446,6 +580,7 @@ private class ElementsToShortenCollector(
private class ShortenCommandImpl(
val targetFile: KtFile,
val importsToAdd: List<FqName>,
val starImportsToAdd: List<FqName>,
val typesToShorten: List<SmartPsiElementPointer<KtUserType>>,
val qualifiersToShorten: List<SmartPsiElementPointer<KtDotQualifiedExpression>>,
) : ShortenCommand {
@@ -456,19 +591,26 @@ private class ShortenCommandImpl(
for (nameToImport in importsToAdd) {
addImportToFile(targetFile.project, targetFile, nameToImport)
}
for (nameToImport in starImportsToAdd) {
addImportToFile(targetFile.project, targetFile, nameToImport, allUnder = true)
}
//todo
// PostprocessReformattingAspect.getInstance(targetFile.project).disablePostprocessFormattingInside {
for (typePointer in typesToShorten) {
val type = typePointer.element ?: continue
type.deleteQualifier()
}
for (typePointer in typesToShorten) {
val type = typePointer.element ?: continue
type.deleteQualifier()
}
for (callPointer in qualifiersToShorten) {
val call = callPointer.element ?: continue
call.deleteQualifier()
}
for (callPointer in qualifiersToShorten) {
val call = callPointer.element ?: continue
call.deleteQualifier()
}
// }
}
override val isEmpty: Boolean get() = typesToShorten.isEmpty() && qualifiersToShorten.isEmpty()
}
private fun KtUserType.hasFakeRootPrefix(): Boolean =
@@ -477,8 +619,6 @@ private fun KtUserType.hasFakeRootPrefix(): Boolean =
private fun KtDotQualifiedExpression.hasFakeRootPrefix(): Boolean =
(receiverExpression as? KtNameReferenceExpression)?.getReferencedName() == ROOT_PREFIX_FOR_IDE_RESOLUTION_MODE
private fun CallableId.asImportableFqName(): FqName? = if (classId == null) packageName.child(callableName) else null
private fun KtElement.getDotQualifiedExpressionForSelector(): KtDotQualifiedExpression? =
getQualifiedExpressionForSelector() as? KtDotQualifiedExpression
@@ -11,7 +11,6 @@ import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtCodeFragment
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtImportDirective
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.resolve.ImportPath
@@ -40,6 +39,8 @@ fun addImportToFile(
alias: Name? = null
) {
val importPath = ImportPath(fqName, allUnder, alias)
// Already imported.
if (file.importDirectives.any { it.importPath == importPath && it.alias?.name == alias?.identifierOrNullIfSpecial }) return
val psiFactory = KtPsiFactory(project)
if (file is KtCodeFragment) {
@@ -47,6 +48,10 @@ fun addImportToFile(
file.addImportsFromString(newDirective.text)
}
if (allUnder) {
file.importDirectives.filter { it.alias == null && it.importPath?.fqName?.parent() == fqName }.forEach { it.delete() }
}
val importList = file.importList
?: error("Trying to insert import $fqName into a file ${file.name} of type ${file::class.java} with no import list.")
@@ -1,3 +1,4 @@
// FIR_IDENTICAL
open class Base<A, B, C>() {
open val method : (A?) -> A = { it!! }
open fun foo(value : B) : B = value
@@ -1,3 +1,4 @@
// FIR_IDENTICAL
open class Base<A, B, C>() {
open val method : (A?) -> A = { it!! }
open fun foo(value : B) : B = value
@@ -1,30 +0,0 @@
open class Base<A, B, C>() {
open val method : (A?) -> A = { it!! }
open fun foo(value : B) : B = value
open fun bar(value : () -> C) : (String) -> C = { value() }
}
class C : Base<String, C, Unit>() {
override fun bar(value: () -> Unit): (String) -> Unit {
<selection><caret>return super.bar(value)</selection>
}
override fun equals(other: Any?): Boolean {
return super.equals(other)
}
override fun foo(value: C): C {
return super.foo(value)
}
override fun hashCode(): Int {
return super.hashCode()
}
override val method: (String?) -> String
get() = method
override fun toString(): String {
return super.toString()
}
}
@@ -7,7 +7,7 @@ class C : A() {
val constant = 42
// Some comment
override val bar: Int
get() = <selection><caret>bar</selection>
get() = <selection><caret>super.bar</selection>
override fun equals(other: Any?): Boolean {
return super.equals(other)