diff --git a/idea/idea-fir/tests/org/jetbrains/kotlin/shortenRefs/FirShortenRefsTestGenerated.java b/idea/idea-fir/tests/org/jetbrains/kotlin/shortenRefs/FirShortenRefsTestGenerated.java index 9d091b804e9..8c886c9a6bd 100644 --- a/idea/idea-fir/tests/org/jetbrains/kotlin/shortenRefs/FirShortenRefsTestGenerated.java +++ b/idea/idea-fir/tests/org/jetbrains/kotlin/shortenRefs/FirShortenRefsTestGenerated.java @@ -46,6 +46,16 @@ public class FirShortenRefsTestGenerated extends AbstractFirShortenRefsTest { runTest("idea/testData/shortenRefsFir/types/ParameterType.kt"); } + @TestMetadata("ParameterTypeConflictingTopLevelClassNotUsed.kt") + public void testParameterTypeConflictingTopLevelClassNotUsed() throws Exception { + runTest("idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassNotUsed.kt"); + } + + @TestMetadata("ParameterTypeConflictingTopLevelClassUsed.kt") + public void testParameterTypeConflictingTopLevelClassUsed() throws Exception { + runTest("idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassUsed.kt"); + } + @TestMetadata("ParameterTypeFunctionalType.kt") public void testParameterTypeFunctionalType() throws Exception { runTest("idea/testData/shortenRefsFir/types/ParameterTypeFunctionalType.kt"); @@ -66,6 +76,11 @@ public class FirShortenRefsTestGenerated extends AbstractFirShortenRefsTest { runTest("idea/testData/shortenRefsFir/types/ParameterTypeNestedType.kt"); } + @TestMetadata("ParameterTypeNonImportedClass.kt") + public void testParameterTypeNonImportedClass() throws Exception { + runTest("idea/testData/shortenRefsFir/types/ParameterTypeNonImportedClass.kt"); + } + @TestMetadata("ParameterTypeStarImportedTypeLoses.kt") public void testParameterTypeStarImportedTypeLoses() throws Exception { runTest("idea/testData/shortenRefsFir/types/ParameterTypeStarImportedTypeLoses.kt"); @@ -81,6 +96,11 @@ public class FirShortenRefsTestGenerated extends AbstractFirShortenRefsTest { runTest("idea/testData/shortenRefsFir/types/ParameterTypeTopLevelTypeWins.kt"); } + @TestMetadata("ParameterTypeTwoNonImportedClassesConflict.kt") + public void testParameterTypeTwoNonImportedClassesConflict() throws Exception { + runTest("idea/testData/shortenRefsFir/types/ParameterTypeTwoNonImportedClassesConflict.kt"); + } + @TestMetadata("VariableType.kt") public void testVariableType() throws Exception { runTest("idea/testData/shortenRefsFir/types/VariableType.kt"); diff --git a/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/components/KtReferenceShortener.kt b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/components/KtReferenceShortener.kt index 2d6bea635e3..7e5ac143ef2 100644 --- a/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/components/KtReferenceShortener.kt +++ b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/components/KtReferenceShortener.kt @@ -5,27 +5,12 @@ package org.jetbrains.kotlin.idea.frontend.api.components -import com.intellij.openapi.application.ApplicationManager -import com.intellij.psi.SmartPsiElementPointer -import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.psi.KtUserType abstract class KtReferenceShortener : KtAnalysisSessionComponent() { abstract fun collectShortenings(file: KtFile, from: Int, to: Int): ShortenCommand } -class ShortenCommand( - val targetFile: KtFile, - val importsToAdd: List, - val typesToShorten: List> -) { - fun invokeShortening() { - ApplicationManager.getApplication().assertWriteAccessAllowed() - - for (typePointer in typesToShorten) { - val type = typePointer.element ?: continue - type.deleteQualifier() - } - } +interface ShortenCommand { + fun invokeShortening() } \ No newline at end of file diff --git a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/components/KtFirReferenceShortener.kt b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/components/KtFirReferenceShortener.kt index 6fa60c8d2a1..4c7b0fd8c55 100644 --- a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/components/KtFirReferenceShortener.kt +++ b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/components/KtFirReferenceShortener.kt @@ -5,6 +5,9 @@ package org.jetbrains.kotlin.idea.frontend.api.fir.components +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.psi.SmartPsiElementPointer import org.jetbrains.kotlin.fir.FirElement import org.jetbrains.kotlin.fir.declarations.FirFile import org.jetbrains.kotlin.fir.psi @@ -19,15 +22,14 @@ import org.jetbrains.kotlin.idea.fir.low.level.api.api.FirModuleResolveState import org.jetbrains.kotlin.idea.fir.low.level.api.api.getOrBuildFir import org.jetbrains.kotlin.idea.fir.low.level.api.api.getOrBuildFirOfType import org.jetbrains.kotlin.idea.frontend.api.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.KtReferenceShortener 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.psi.KtElement -import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.psi.KtTypeReference -import org.jetbrains.kotlin.psi.KtUserType +import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer internal class KtFirReferenceShortener( @@ -39,10 +41,12 @@ internal class KtFirReferenceShortener( resolveFileToBodyResolve(file) val firFile = file.getOrBuildFirOfType(firResolveState) + val typesToImport = mutableListOf() val typesToShorten = mutableListOf() - firFile.acceptChildren(TypesCollectingVisitor(typesToShorten)) - return ShortenCommand(file, emptyList(), typesToShorten.map { it.createSmartPointer() }) + firFile.acceptChildren(TypesCollectingVisitor(typesToImport, typesToShorten)) + + return ShortenCommandImpl(file, typesToImport, typesToShorten.map { it.createSmartPointer() }) } private fun findFirstClassifierInScopesByName(positionScopes: List, targetClassName: Name): ClassId? { @@ -56,14 +60,12 @@ internal class KtFirReferenceShortener( return null } - private fun resolveFileToBodyResolve(file: KtFile) { for (declaration in file.declarations) { declaration.getOrBuildFir(firResolveState) // temporary hack, resolves declaration to BODY_RESOLVE stage } } - @OptIn(ExperimentalStdlibApi::class) private fun FirScope.findFirstClassifierByName(name: Name): FirClassifierSymbol<*>? { var element: FirClassifierSymbol<*>? = null @@ -83,7 +85,10 @@ internal class KtFirReferenceShortener( return availableScopes.asReversed() } - private inner class TypesCollectingVisitor(private val collectedTypes: MutableList) : FirVisitorVoid() { + private inner class TypesCollectingVisitor( + private val typesToImport: MutableList, + private val typesToShorten: MutableList, + ) : FirVisitorVoid() { override fun visitElement(element: FirElement) { element.acceptChildren(this) } @@ -103,23 +108,62 @@ internal class KtFirReferenceShortener( if (wholeTypeElement.qualifier == null) return - val typeToShorten = findBiggestClassifierToShorten(wholeClassifierId, wholeTypeElement) ?: return - collectedTypes.add(typeToShorten) + collectTypeIfNeedsToBeShortened(wholeClassifierId, wholeTypeElement) } - private fun findBiggestClassifierToShorten(wholeClassifierId: ClassId, wholeTypeElement: KtUserType): KtUserType? { + private fun collectTypeIfNeedsToBeShortened(wholeClassifierId: ClassId, wholeTypeElement: KtUserType) { val allClassIds = generateSequence(wholeClassifierId) { it.outerClassId } val allTypeElements = generateSequence(wholeTypeElement) { it.qualifier } - val positionScopes = findScopesAtPosition(wholeTypeElement) ?: return null + val positionScopes = findScopesAtPosition(wholeTypeElement) ?: return for ((classId, typeElement) in allClassIds.zip(allTypeElements)) { val firstFoundClass = findFirstClassifierInScopesByName(positionScopes, classId.shortClassName) - if (firstFoundClass == classId) return typeElement + if (firstFoundClass == classId) { + addTypeToShorten(typeElement) + return + } } - return null + // none class matched + val (mostTopLevelClassId, mostTopLevelTypeElement) = allClassIds.zip(allTypeElements).last() + val firstFoundClass = findFirstClassifierInScopesByName(positionScopes, mostTopLevelClassId.shortClassName) + + check(firstFoundClass != mostTopLevelClassId) { "This should not be true" } + + if (firstFoundClass == null) { + addTypeToImportAndShorten(mostTopLevelClassId.asSingleFqName(), mostTopLevelTypeElement) + } + } + + private fun addTypeToShorten(typeElement: KtUserType) { + typesToShorten.add(typeElement) + } + + private fun addTypeToImportAndShorten(classFqName: FqName, mostTopLevelTypeElement: KtUserType) { + typesToImport.add(classFqName) + typesToShorten.add(mostTopLevelTypeElement) } } -} \ No newline at end of file +} + +private class ShortenCommandImpl( + val targetFile: KtFile, + val importsToAdd: List, + val typesToShorten: List> +) : ShortenCommand { + + override fun invokeShortening() { + ApplicationManager.getApplication().assertWriteAccessAllowed() + + for (nameToImport in importsToAdd) { + addImportToFile(targetFile.project, targetFile, nameToImport) + } + + for (typePointer in typesToShorten) { + val type = typePointer.element ?: continue + type.deleteQualifier() + } + } +} diff --git a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/utils/shortenReferencesUtils.kt b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/utils/shortenReferencesUtils.kt new file mode 100644 index 00000000000..00d6a8af987 --- /dev/null +++ b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/utils/shortenReferencesUtils.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2010-2021 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.idea.frontend.api.fir.utils + +import com.intellij.openapi.project.Project +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 + +private val SimpleImportPathComparator: Comparator = compareBy(ImportPath::toString) + +/** + * This is a partial copy from `org.jetbrains.kotlin.idea.util.ImportInsertHelperImpl.Companion.addImport`. + * + * We want it as a copy because we do not yet care about imports ordering, so we do not need a fancy comparator. + */ +internal fun addImportToFile( + project: Project, + file: KtFile, + fqName: FqName, + allUnder: Boolean = false, + alias: Name? = null +) { + val importPath = ImportPath(fqName, allUnder, alias) + + val psiFactory = KtPsiFactory(project) + if (file is KtCodeFragment) { + val newDirective = psiFactory.createImportDirective(importPath) + file.addImportsFromString(newDirective.text) + } + + val importList = file.importList + ?: error("Trying to insert import $fqName into a file ${file.name} of type ${file::class.java} with no import list.") + + val newDirective = psiFactory.createImportDirective(importPath) + val imports = importList.imports + if (imports.isEmpty()) { //TODO: strange hack + importList.add(psiFactory.createNewLine()) + importList.add(newDirective) + } else { + val insertAfter = imports + .lastOrNull { + val directivePath = it.importPath + + directivePath != null && SimpleImportPathComparator.compare(directivePath, importPath) <= 0 + } + + importList.addAfter(newDirective, insertAfter) + } +} diff --git a/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassNotUsed.dependency.kt b/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassNotUsed.dependency.kt new file mode 100644 index 00000000000..9bf4a365e82 --- /dev/null +++ b/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassNotUsed.dependency.kt @@ -0,0 +1,3 @@ +package dependency + +class Foo \ No newline at end of file diff --git a/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassNotUsed.kt b/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassNotUsed.kt new file mode 100644 index 00000000000..58e135b0680 --- /dev/null +++ b/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassNotUsed.kt @@ -0,0 +1,6 @@ +// FIR_COMPARISON +package test + +class Foo + +fun foo(p: dependency.Foo) {} diff --git a/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassNotUsed.kt.after b/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassNotUsed.kt.after new file mode 100644 index 00000000000..5a4271c299e --- /dev/null +++ b/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassNotUsed.kt.after @@ -0,0 +1,8 @@ +// FIR_COMPARISON +package test + +import dependency.Foo + +class Foo + +fun foo(p: Foo) {} diff --git a/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassUsed.dependency.kt b/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassUsed.dependency.kt new file mode 100644 index 00000000000..9bf4a365e82 --- /dev/null +++ b/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassUsed.dependency.kt @@ -0,0 +1,3 @@ +package dependency + +class Foo \ No newline at end of file diff --git a/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassUsed.kt b/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassUsed.kt new file mode 100644 index 00000000000..db9b1eae096 --- /dev/null +++ b/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassUsed.kt @@ -0,0 +1,8 @@ +// FIR_COMPARISON +package test + +class Foo + +fun foo(p: dependency.Foo) {} + +fun bar(): Foo {} \ No newline at end of file diff --git a/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassUsed.kt.after b/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassUsed.kt.after new file mode 100644 index 00000000000..f6a6fd9a23f --- /dev/null +++ b/idea/testData/shortenRefsFir/types/ParameterTypeConflictingTopLevelClassUsed.kt.after @@ -0,0 +1,8 @@ +// FIR_COMPARISON +package test + +class Foo + +fun foo(p: dependency.Foo) {} + +fun bar(): Foo {} \ No newline at end of file diff --git a/idea/testData/shortenRefsFir/types/ParameterTypeNonImportedClass.dependency.kt b/idea/testData/shortenRefsFir/types/ParameterTypeNonImportedClass.dependency.kt new file mode 100644 index 00000000000..fc6fb4fa1c8 --- /dev/null +++ b/idea/testData/shortenRefsFir/types/ParameterTypeNonImportedClass.dependency.kt @@ -0,0 +1,3 @@ +package dependency + +class T \ No newline at end of file diff --git a/idea/testData/shortenRefsFir/types/ParameterTypeNonImportedClass.kt b/idea/testData/shortenRefsFir/types/ParameterTypeNonImportedClass.kt new file mode 100644 index 00000000000..0eab57c0d30 --- /dev/null +++ b/idea/testData/shortenRefsFir/types/ParameterTypeNonImportedClass.kt @@ -0,0 +1,4 @@ +// FIR_COMPARISON +package test + +fun foo(p: dependency.T) {} \ No newline at end of file diff --git a/idea/testData/shortenRefsFir/types/ParameterTypeNonImportedClass.kt.after b/idea/testData/shortenRefsFir/types/ParameterTypeNonImportedClass.kt.after new file mode 100644 index 00000000000..524ec1c07a6 --- /dev/null +++ b/idea/testData/shortenRefsFir/types/ParameterTypeNonImportedClass.kt.after @@ -0,0 +1,6 @@ +// FIR_COMPARISON +package test + +import dependency.T + +fun foo(p: T) {} \ No newline at end of file diff --git a/idea/testData/shortenRefsFir/types/ParameterTypeTwoNonImportedClassesConflict.dependency1.kt b/idea/testData/shortenRefsFir/types/ParameterTypeTwoNonImportedClassesConflict.dependency1.kt new file mode 100644 index 00000000000..e868126b897 --- /dev/null +++ b/idea/testData/shortenRefsFir/types/ParameterTypeTwoNonImportedClassesConflict.dependency1.kt @@ -0,0 +1,3 @@ +package dependency1 + +class T \ No newline at end of file diff --git a/idea/testData/shortenRefsFir/types/ParameterTypeTwoNonImportedClassesConflict.dependency2.kt b/idea/testData/shortenRefsFir/types/ParameterTypeTwoNonImportedClassesConflict.dependency2.kt new file mode 100644 index 00000000000..83d8674a6c4 --- /dev/null +++ b/idea/testData/shortenRefsFir/types/ParameterTypeTwoNonImportedClassesConflict.dependency2.kt @@ -0,0 +1,3 @@ +package dependency2 + +class T \ No newline at end of file diff --git a/idea/testData/shortenRefsFir/types/ParameterTypeTwoNonImportedClassesConflict.kt b/idea/testData/shortenRefsFir/types/ParameterTypeTwoNonImportedClassesConflict.kt new file mode 100644 index 00000000000..74c6e672a01 --- /dev/null +++ b/idea/testData/shortenRefsFir/types/ParameterTypeTwoNonImportedClassesConflict.kt @@ -0,0 +1,4 @@ +// FIR_COMPARISON +package test + +fun foo(p1: dependency1.T, p2: dependency2.T) {} \ No newline at end of file diff --git a/idea/testData/shortenRefsFir/types/ParameterTypeTwoNonImportedClassesConflict.kt.after b/idea/testData/shortenRefsFir/types/ParameterTypeTwoNonImportedClassesConflict.kt.after new file mode 100644 index 00000000000..2a37419d864 --- /dev/null +++ b/idea/testData/shortenRefsFir/types/ParameterTypeTwoNonImportedClassesConflict.kt.after @@ -0,0 +1,6 @@ +// FIR_COMPARISON +package test + +import dependency1.T + +fun foo(p1: T, p2: dependency2.T) {} \ No newline at end of file