diff --git a/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/imports/HierarchicalScope.kt b/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/imports/HierarchicalScope.kt new file mode 100644 index 000000000000..9dc5d0c1ecf6 --- /dev/null +++ b/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/imports/HierarchicalScope.kt @@ -0,0 +1,36 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.kotlin.idea.k2.codeinsight.imports + +import org.jetbrains.kotlin.analysis.api.KaSession +import org.jetbrains.kotlin.analysis.api.components.KaScopeContext +import org.jetbrains.kotlin.analysis.api.scopes.KaScope +import org.jetbrains.kotlin.analysis.api.symbols.KaClassifierSymbol +import org.jetbrains.kotlin.name.Name + +internal class HierarchicalScope private constructor(private val scopes: List) { + + /** + * Finds classifier symbols corresponding to the given name across the scope layers. + * + * Symbols from higher priority scope layers are returned first. + * + * Each list in the sequence represents classifiers found in a single scopes layer. + */ + fun findClassifiers(name: Name): Sequence> { + return scopes.asSequence() + .map { it.classifiers(name).toList() } + .filter { it.isNotEmpty() } + } + + companion object { + fun KaSession.createFrom(scopeContext: KaScopeContext): HierarchicalScope { + val scopeGroupsSorted = scopeContext.scopes + .groupBy({ it.kind }, { it.scope }) + .toSortedMap(compareByDescending { it.indexInTower }) + + val scopes = scopeGroupsSorted.values.map { it.asCompositeScope() } + + return HierarchicalScope(scopes) + } + } +} \ No newline at end of file diff --git a/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/imports/OptimizedImportsBuilder.kt b/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/imports/OptimizedImportsBuilder.kt index ad6a3f7de19c..29f423dfd31d 100644 --- a/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/imports/OptimizedImportsBuilder.kt +++ b/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/imports/OptimizedImportsBuilder.kt @@ -15,6 +15,7 @@ import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtImportDirective import org.jetbrains.kotlin.resolve.ImportPath +import kotlin.collections.filter internal fun KaSession.buildOptimizedImports( file: KtFile, @@ -51,8 +52,8 @@ internal class OptimizedImportsBuilder( val parentFqName = resultFqName.parent() if ( - alias == null - && canUseStarImport(importableSymbol, resultFqName) && + alias == null && + canUseStarImport(importableSymbol, resultFqName) && ImportPath(parentFqName, isAllUnder = true).isAllowedByRules() ) { symbolsByParentFqName.getOrPut(parentFqName) { hashSetOf() }.add(importableSymbol) @@ -88,6 +89,30 @@ internal class OptimizedImportsBuilder( } } + val importingScopeContext = buildScopeContextByImports(file, importsToGenerate.filter { it.isAllUnder }) + val importingScopes = HierarchicalScope.run { createFrom(importingScopeContext) } + + for (fqName in classNamesToCheck) { + val foundClassifiers = importingScopes.findClassifiers(fqName.shortName()).firstOrNull() + val singleFoundClassifier = foundClassifiers?.singleOrNull() + + if (singleFoundClassifier?.importableFqName != fqName) { + // add explicit import if failed to import with * (or from current package) + importsToGenerate.add(ImportPath(fqName, false)) + + val parentFqName = fqName.parent() + + val siblingsToImport = symbolsByParentFqName.getValue(parentFqName) + for (descriptor in siblingsToImport.filter { it.run {computeImportableName() } == fqName }) { + siblingsToImport.remove(descriptor) + } + + if (siblingsToImport.isEmpty()) { // star import is not really needed + importsToGenerate.remove(ImportPath(parentFqName, true)) + } + } + } + val sortedImportsToGenerate = importsToGenerate.sortedWith(KotlinImportPathComparator.create(file)) val oldImports = file.importDirectives diff --git a/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/imports/scopeUtils.kt b/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/imports/scopeUtils.kt new file mode 100644 index 000000000000..df046dca2759 --- /dev/null +++ b/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/imports/scopeUtils.kt @@ -0,0 +1,33 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.kotlin.idea.k2.codeinsight.imports + +import org.jetbrains.kotlin.analysis.api.KaSession +import org.jetbrains.kotlin.analysis.api.components.KaScopeContext +import org.jetbrains.kotlin.psi.KtBlockCodeFragment +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.resolve.ImportPath + +internal fun KaSession.buildScopeContextByImports(originalFile: KtFile, importsToGenerate: Collection): KaScopeContext { + val fileWithImports = buildFileWithImports(originalFile, importsToGenerate) + + return fileWithImports.importingScopeContext +} + +private fun buildFileWithImports( + originalFile: KtFile, + importsToGenerate: Collection +): KtFile { + val imports = buildString { + for (importPath in importsToGenerate) { + append("import ") + append(importPath) + append("\n") + } + } + + // TODO this code fragment misses a package declaration + val fileWithImports = KtBlockCodeFragment(originalFile.project, "Dummy_" + originalFile.name, "", imports, originalFile) + + return fileWithImports +} + diff --git a/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/imports/FirJvmOptimizeImportsTestGenerated.java b/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/imports/FirJvmOptimizeImportsTestGenerated.java index 494019963449..122d6b3da544 100644 --- a/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/imports/FirJvmOptimizeImportsTestGenerated.java +++ b/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/imports/FirJvmOptimizeImportsTestGenerated.java @@ -579,6 +579,11 @@ public abstract class FirJvmOptimizeImportsTestGenerated extends AbstractFirJvmO runTest("../../idea/tests/testData/editor/optimizeImports/common/Enums.kt"); } + @TestMetadata("ExplicitImportIsNotRemovedToAvoidConflict.kt") + public void testExplicitImportIsNotRemovedToAvoidConflict() throws Exception { + runTest("../../idea/tests/testData/editor/optimizeImports/common/ExplicitImportIsNotRemovedToAvoidConflict.kt"); + } + @TestMetadata("ExtensionFunWithThisReference.kt") public void testExtensionFunWithThisReference() throws Exception { runTest("../../idea/tests/testData/editor/optimizeImports/common/ExtensionFunWithThisReference.kt"); diff --git a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/imports/JsOptimizeImportsTestGenerated.java b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/imports/JsOptimizeImportsTestGenerated.java index f1e29e332a5c..8bec235d7aff 100644 --- a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/imports/JsOptimizeImportsTestGenerated.java +++ b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/imports/JsOptimizeImportsTestGenerated.java @@ -311,6 +311,11 @@ public abstract class JsOptimizeImportsTestGenerated extends AbstractJsOptimizeI runTest("testData/editor/optimizeImports/common/Enums.kt"); } + @TestMetadata("ExplicitImportIsNotRemovedToAvoidConflict.kt") + public void testExplicitImportIsNotRemovedToAvoidConflict() throws Exception { + runTest("testData/editor/optimizeImports/common/ExplicitImportIsNotRemovedToAvoidConflict.kt"); + } + @TestMetadata("ExtensionFunWithThisReference.kt") public void testExtensionFunWithThisReference() throws Exception { runTest("testData/editor/optimizeImports/common/ExtensionFunWithThisReference.kt"); diff --git a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/imports/JvmOptimizeImportsTestGenerated.java b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/imports/JvmOptimizeImportsTestGenerated.java index 347b2007adbb..00da1bb5110e 100644 --- a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/imports/JvmOptimizeImportsTestGenerated.java +++ b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/imports/JvmOptimizeImportsTestGenerated.java @@ -584,6 +584,11 @@ public abstract class JvmOptimizeImportsTestGenerated extends AbstractJvmOptimiz runTest("testData/editor/optimizeImports/common/Enums.kt"); } + @TestMetadata("ExplicitImportIsNotRemovedToAvoidConflict.kt") + public void testExplicitImportIsNotRemovedToAvoidConflict() throws Exception { + runTest("testData/editor/optimizeImports/common/ExplicitImportIsNotRemovedToAvoidConflict.kt"); + } + @TestMetadata("ExtensionFunWithThisReference.kt") public void testExtensionFunWithThisReference() throws Exception { runTest("testData/editor/optimizeImports/common/ExtensionFunWithThisReference.kt"); diff --git a/plugins/kotlin/idea/tests/testData/editor/optimizeImports/common/ExplicitImportIsNotRemovedToAvoidConflict.dependency1.kt b/plugins/kotlin/idea/tests/testData/editor/optimizeImports/common/ExplicitImportIsNotRemovedToAvoidConflict.dependency1.kt new file mode 100644 index 000000000000..4e3702b2c902 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/editor/optimizeImports/common/ExplicitImportIsNotRemovedToAvoidConflict.dependency1.kt @@ -0,0 +1,7 @@ +package dependencyFoo + +class CommonClass + +fun fooOne() {} +fun fooTwo() {} +fun fooThree() {} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/editor/optimizeImports/common/ExplicitImportIsNotRemovedToAvoidConflict.dependency2.kt b/plugins/kotlin/idea/tests/testData/editor/optimizeImports/common/ExplicitImportIsNotRemovedToAvoidConflict.dependency2.kt new file mode 100644 index 000000000000..ac0c31e21479 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/editor/optimizeImports/common/ExplicitImportIsNotRemovedToAvoidConflict.dependency2.kt @@ -0,0 +1,7 @@ +package dependencyBar + +class CommonClass + +fun barOne() {} +fun barTwo() {} +fun barThree() {} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/editor/optimizeImports/common/ExplicitImportIsNotRemovedToAvoidConflict.kt b/plugins/kotlin/idea/tests/testData/editor/optimizeImports/common/ExplicitImportIsNotRemovedToAvoidConflict.kt new file mode 100644 index 000000000000..89da6eaeffa2 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/editor/optimizeImports/common/ExplicitImportIsNotRemovedToAvoidConflict.kt @@ -0,0 +1,22 @@ +// NAME_COUNT_TO_USE_STAR_IMPORT: 2 +package test + +import dependencyBar.* +import dependencyFoo.* +import dependencyFoo.CommonClass + +fun barUsage() { + barOne() + barTwo() + barThree() +} + +fun fooUsage() { + fooOne() + fooTwo() + fooThree() +} + +fun test() { + CommonClass() +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/editor/optimizeImports/common/ExplicitImportIsNotRemovedToAvoidConflict.kt.after b/plugins/kotlin/idea/tests/testData/editor/optimizeImports/common/ExplicitImportIsNotRemovedToAvoidConflict.kt.after new file mode 100644 index 000000000000..f77410bdbefc --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/editor/optimizeImports/common/ExplicitImportIsNotRemovedToAvoidConflict.kt.after @@ -0,0 +1,22 @@ +// NAME_COUNT_TO_USE_STAR_IMPORT: 2 +package test + +import dependencyBar.* +import dependencyFoo.* +import dependencyFoo.CommonClass + +fun barUsage() { + barOne() + barTwo() + barThree() +} + +fun fooUsage() { + fooOne() + fooTwo() + fooThree() +} + +fun test() { + CommonClass() +}