Initial implementation of new optimize imports algorithm

This commit is contained in:
Valentin Kipyatkov
2015-02-13 20:33:30 +03:00
parent 63182a53c6
commit 03f7c265d7
16 changed files with 196 additions and 111 deletions
@@ -16,7 +16,6 @@
package org.jetbrains.kotlin.idea.util
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.JetFile
import org.jetbrains.kotlin.psi.JetImportDirective
import org.jetbrains.kotlin.resolve.ImportPath
@@ -24,6 +23,7 @@ import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import com.intellij.openapi.project.Project
import com.intellij.openapi.components.ServiceManager
import kotlin.platform.platformStatic
import java.util.*
public abstract class ImportInsertHelper {
@@ -31,10 +31,10 @@ public abstract class ImportInsertHelper {
public abstract fun isImportedWithDefault(importPath: ImportPath, contextFile: JetFile): Boolean
public abstract fun needImport(importPath: ImportPath, file: JetFile, importDirectives: List<JetImportDirective> = file.getImportDirectives()): Boolean
public abstract fun mayImportByCodeStyle(descriptor: DeclarationDescriptor): Boolean
public abstract val importSortComparator: Comparator<ImportPath>
public enum class ImportDescriptorResult {
FAIL
IMPORT_ADDED
@@ -60,8 +60,3 @@ public fun JetType.canBeReferencedViaImport(): Boolean {
val descriptor = getConstructor().getDeclarationDescriptor()
return descriptor != null && descriptor.canBeReferencedViaImport()
}
public fun isInReceiverScope(referenceElement: PsiElement, referencedDescriptor: DeclarationDescriptor): Boolean {
val isExpressionWithReceiver = referenceElement is JetSimpleNameExpression && referenceElement.getReceiverExpression() != null
return isExpressionWithReceiver && !referencedDescriptor.isExtension
}
@@ -22,82 +22,137 @@ import com.intellij.openapi.progress.ProgressIndicatorProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.ImportPath
import org.jetbrains.kotlin.name.*
import org.jetbrains.kotlin.idea.references.JetReference
import java.util.HashSet
import java.util.ArrayList
import org.jetbrains.kotlin.idea.util.ImportInsertHelper
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.idea.refactoring.fqName.isImported
import java.util.*
import org.jetbrains.kotlin.resolve.descriptorUtil.*
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.idea.formatter.JetCodeStyleSettings
import org.jetbrains.kotlin.resolve.*
import org.jetbrains.kotlin.psi.psiUtil.*
public class KotlinImportOptimizer() : ImportOptimizer {
override fun supports(file: PsiFile?) = file is JetFile
override fun processFile(file: PsiFile?) = Runnable() {
val jetFile = file as JetFile
val usedQualifiedNames = extractUsedQualifiedNames(jetFile)
OptimizeProcess(file as JetFile).execute()
}
val directives = jetFile.getImportDirectives()
private class OptimizeProcess(val file: JetFile) {
private val codeStyleSettings = JetCodeStyleSettings.getInstance(file.getProject())
private val aliasImports: Map<Name, FqName>
val directivesBeforeCurrent = ArrayList<JetImportDirective>()
val directivesAfterCurrent = ArrayList(jetFile.getImportDirectives())
ApplicationManager.getApplication()!!.runWriteAction(Runnable {
val importInsertHelper = ImportInsertHelper.getInstance(file.getProject())
// Remove only unnecessary imports
for (anImport in directives) {
directivesAfterCurrent.remove(anImport)
val importPath = anImport.getImportPath()
if (importPath == null) {
continue
}
if (isUseful(importPath, usedQualifiedNames)
&& importInsertHelper.needImport(importPath, jetFile, directivesBeforeCurrent)
&& importInsertHelper.needImport(importPath, jetFile, directivesAfterCurrent)
) {
directivesBeforeCurrent.add(anImport)
;{
val imports = file.getImportDirectives()
val aliasImports = HashMap<Name, FqName>()
for (import in imports) {
val path = import.getImportPath() ?: continue
val aliasName = path.getAlias()
if (aliasName != null) {
aliasImports.put(aliasName, path.fqnPart())
}
else {
anImport.delete()
//TODO: detect other "special" imports
}
}
})
}
this.aliasImports = aliasImports
}
private fun isUseful(importPath: ImportPath, usedNames: Collection<FqName>): Boolean {
// TODO: Add better analysis for aliases
return importPath.hasAlias() || usedNames.any { it.isImported(importPath) }
}
public fun execute() {
//TODO: keep existing imports? at least aliases (comments)
private fun extractUsedQualifiedNames(jetFile: JetFile): Set<FqName> {
val usedQualifiedNames = HashSet<FqName>()
jetFile.accept(object : JetVisitorVoid() {
override fun visitElement(element: PsiElement) {
ProgressIndicatorProvider.checkCanceled()
element?.acceptChildren(this)
val importInsertHelper = ImportInsertHelper.getInstance(file.getProject())
val currentPackageName = file.getPackageFqName()
val descriptorsToImport = detectDescriptorsToImport()
val importsToGenerate = ArrayList<ImportPath>()
if (codeStyleSettings.PREFER_ALL_UNDER_IMPORTS) {
//TODO
//TODO: don't forget that packages may not be imported by *
}
else {
for (descriptor in descriptorsToImport) {
val fqName = descriptor.importableFqNameSafe
if (descriptor !is PackageViewDescriptor && fqName.parent() == currentPackageName) continue
val importPath = ImportPath(fqName, false)
if (importInsertHelper.isImportedWithDefault(importPath, file)/*TODO: implementation is not quite correct*/) continue
importsToGenerate.add(importPath)
}
}
override fun visitJetElement(element: JetElement) {
if (element.getStrictParentOfType<JetImportDirective>() != null ||
element.getStrictParentOfType<JetPackageDirective>() != null) {
return
//TODO: drop unused aliases
aliasImports.mapTo(importsToGenerate) { ImportPath(it.getValue(), false, it.getKey())}
val sortedImportsToGenerate = importsToGenerate.sortBy(importInsertHelper.importSortComparator)
//TODO: do not touch file if everything is already correct?
ApplicationManager.getApplication()!!.runWriteAction(Runnable {
for (import in file.getImportDirectives()) {
import.delete()
}
val reference = element.getReference()
if (reference is JetReference) {
val referencedDescriptors = reference.resolveToDescriptors()
val importableDescriptors = referencedDescriptors.filter {
it.canBeReferencedViaImport() && !isInReceiverScope(element, it)
val importList = file.getImportList()!!
val psiFactory = JetPsiFactory(file.getProject())
for (importPath in sortedImportsToGenerate) {
importList.add(psiFactory.createImportDirective(importPath))
}
})
}
private fun detectDescriptorsToImport(): Set<DeclarationDescriptor> {
val usedDescriptors = HashSet<DeclarationDescriptor>()
file.accept(object : JetVisitorVoid() {
override fun visitElement(element: PsiElement) {
ProgressIndicatorProvider.checkCanceled()
element.acceptChildren(this)
}
override fun visitImportList(importList: JetImportList) {
}
override fun visitPackageDirective(directive: JetPackageDirective) {
}
override fun visitJetElement(element: JetElement) {
val reference = element.getReference()
if (reference is JetReference) {
val targets = reference.resolveToDescriptors()
//TODO: check if it's resolved through alias
for (target in targets) {
if (!target.canBeReferencedViaImport()) continue
if (target is PackageViewDescriptor && target.getFqName().parent() == FqName.ROOT) continue // no need to import top-level packages (TODO: is that always true?)
if (!target.isExtension) { // for non-extension targets, count only non-qualified simple name usages
if (element !is JetNameReferenceExpression) continue
if (element.getIdentifier() == null) continue // skip 'this' etc
if (element.getReceiverExpression() != null) continue
}
if (isAccessibleAsMember(target, element)) continue
usedDescriptors.add(target.getImportableDescriptor())
}
}
usedQualifiedNames.addAll(importableDescriptors.map { it.importableFqName }.filterNotNull())
}
super.visitJetElement(element)
}
})
return usedQualifiedNames
super.visitJetElement(element)
}
})
return usedDescriptors
}
private fun isAccessibleAsMember(target: DeclarationDescriptor, place: JetElement): Boolean {
val container = target.getContainingDeclaration()
if (container !is ClassDescriptor) return false
val scope = if (container.getKind() == ClassKind.CLASS_OBJECT)
container.getContainingDeclaration() as? ClassDescriptor ?: return false
else
container
val declaration = DescriptorToSourceUtils.classDescriptorToDeclaration(scope)
return declaration != null && declaration.getContainingFile() == file && declaration.isAncestor(place)
}
}
}
@@ -102,6 +102,9 @@ public class ImportInsertHelperImpl(private val project: Project) : ImportInsert
}
}
override val importSortComparator: Comparator<ImportPath>
get() = ImportPathComparator
private object ImportPathComparator : Comparator<ImportPath> {
override fun compare(import1: ImportPath, import2: ImportPath): Int {
// alias imports placed last
@@ -125,29 +128,6 @@ public class ImportInsertHelperImpl(private val project: Project) : ImportInsert
}
}
/**
* Check that import is useless.
*/
private fun isImportedByDefault(importPath: ImportPath, jetFile: JetFile): Boolean {
if (importPath.fqnPart().isRoot()) {
return true
}
if (!importPath.isAllUnder() && !importPath.hasAlias()) {
// Single element import without .* and alias is useless
if (importPath.fqnPart().isOneSegmentFQN()) {
return true
}
// There's no need to import a declaration from the package of current file
if (jetFile.getPackageFqName() == importPath.fqnPart().parent()) {
return true
}
}
return isImportedWithDefault(importPath, jetFile)
}
override fun isImportedWithDefault(importPath: ImportPath, contextFile: JetFile): Boolean {
val defaultImports = if (ProjectStructureUtil.isJsKotlinModule(contextFile))
TopDownAnalyzerFacadeForJS.DEFAULT_IMPORTS
@@ -156,24 +136,6 @@ public class ImportInsertHelperImpl(private val project: Project) : ImportInsert
return importPath.isImported(defaultImports)
}
override fun needImport(importPath: ImportPath, file: JetFile, importDirectives: List<JetImportDirective>): Boolean {
if (isImportedByDefault(importPath, file)) {
return false
}
if (!importDirectives.isEmpty()) {
// Check if import is already present
for (directive in importDirectives) {
val existentImportPath = directive.getImportPath()
if (existentImportPath != null && importPath.isImported(existentImportPath)) {
return false
}
}
}
return true
}
override fun mayImportByCodeStyle(descriptor: DeclarationDescriptor): Boolean {
val importable = descriptor.getImportableDescriptor()
return when (importable) {
@@ -1,6 +1,7 @@
package test
import dependency.*
import dependency.A
import dependency.T
fun f(a: A, t: T) {
a.f()
@@ -0,0 +1,8 @@
package ppp
import ppp.ppp1
open class C
class D : C()
class E : ppp1.C1()
@@ -0,0 +1,3 @@
package ppp.ppp1
class C1
@@ -0,0 +1,9 @@
package ppp
import ppp.*
import ppp.ppp1
open class C
class D : C()
class E : ppp1.C1()
@@ -1,4 +1,4 @@
import MyClass.*
import MyClass.TEST
fun main() {
TEST
@@ -0,0 +1,18 @@
import Outer.C.Inner2
class Outer {
class Nested
class C {
fun foo(p1: Nested, p2: ClassObjectNested, p3: Nested2) { }
inner class Inner
inner class Inner2
}
class object {
class ClassObjectNested
}
fun f(i: Inner2){}
}
@@ -0,0 +1,21 @@
import Outer.Nested
import Outer.ClassObjectNested
import Outer.C.Inner
import Outer.C.Inner2
class Outer {
class Nested
class C {
fun foo(p1: Nested, p2: ClassObjectNested, p3: Nested2) { }
inner class Inner
inner class Inner2
}
class object {
class ClassObjectNested
}
fun f(i: Inner2){}
}
@@ -1,4 +1,5 @@
import java.util.*
import java.util.ArrayList
import java.util.HashSet
class Action {
fun test(hash: HashSet<Int>) {
@@ -1,4 +1,5 @@
import java.util.*
import java.util.ArrayList
import java.util.HashSet
class Action {
fun test(hash: HashSet<Int>) {
@@ -1,3 +1,3 @@
import java.util.*
import java.util.Comparator
val c = Comparator {(x: Int, y: Int) -> 1}
@@ -1,5 +1,5 @@
import java.io as JavaIO
import java.util.ArrayList
import java.io as JavaIO
import java.util.ArrayList as SomeThing
class Action {
@@ -17,7 +17,6 @@
package org.jetbrains.kotlin.idea.imports;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.InnerTestClasses;
import org.jetbrains.kotlin.test.JUnit3RunnerWithInners;
import org.jetbrains.kotlin.test.JetTestUtils;
import org.jetbrains.kotlin.test.TestMetadata;
@@ -60,6 +59,12 @@ public class OptimizeImportsTestGenerated extends AbstractOptimizeImportsTest {
doTest(fileName);
}
@TestMetadata("CurrentPackage")
public void testCurrentPackage() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/editor/optimizeImports/CurrentPackage/");
doTest(fileName);
}
@TestMetadata("DefaultJsImports")
public void testDefaultJsImports() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/editor/optimizeImports/DefaultJsImports/");
@@ -126,6 +131,12 @@ public class OptimizeImportsTestGenerated extends AbstractOptimizeImportsTest {
doTest(fileName);
}
@TestMetadata("MembersInScope")
public void testMembersInScope() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/editor/optimizeImports/MembersInScope/");
doTest(fileName);
}
@TestMetadata("PartiallyQualified")
public void testPartiallyQualified() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/editor/optimizeImports/PartiallyQualified/");