On-the-fly import optimization
This commit is contained in:
@@ -26,9 +26,6 @@ import kotlin.platform.platformStatic
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
public abstract class ImportInsertHelper {
|
public abstract class ImportInsertHelper {
|
||||||
|
|
||||||
public abstract fun optimizeImportsOnTheFly(file: JetFile): Boolean
|
|
||||||
|
|
||||||
/*TODO: implementation is not quite correct*/
|
/*TODO: implementation is not quite correct*/
|
||||||
public abstract fun isImportedWithDefault(importPath: ImportPath, contextFile: JetFile): Boolean
|
public abstract fun isImportedWithDefault(importPath: ImportPath, contextFile: JetFile): Boolean
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ public class ShortenReferences(val options: (JetElement) -> Options = { Options.
|
|||||||
//TODO: that's not correct since we have options!
|
//TODO: that's not correct since we have options!
|
||||||
val elementsToUse = dropNestedElements(elements)
|
val elementsToUse = dropNestedElements(elements)
|
||||||
|
|
||||||
val importInserter = ImportInserter(file)
|
val helper = ImportInsertHelper.getInstance(file.getProject())
|
||||||
|
|
||||||
val failedToImportDescriptors = LinkedHashSet<DeclarationDescriptor>()
|
val failedToImportDescriptors = LinkedHashSet<DeclarationDescriptor>()
|
||||||
|
|
||||||
@@ -161,11 +161,7 @@ public class ShortenReferences(val options: (JetElement) -> Options = { Options.
|
|||||||
for (descriptor in descriptorsToImport) {
|
for (descriptor in descriptorsToImport) {
|
||||||
assert(descriptor !in failedToImportDescriptors)
|
assert(descriptor !in failedToImportDescriptors)
|
||||||
|
|
||||||
if (importInserter.optimizeImports()) {
|
val result = helper.importDescriptor(file, descriptor)
|
||||||
anyChange = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = importInserter.addImport(descriptor)
|
|
||||||
if (result != ImportInsertHelper.ImportDescriptorResult.ALREADY_IMPORTED) {
|
if (result != ImportInsertHelper.ImportDescriptorResult.ALREADY_IMPORTED) {
|
||||||
anyChange = true
|
anyChange = true
|
||||||
}
|
}
|
||||||
@@ -457,20 +453,4 @@ public class ShortenReferences(val options: (JetElement) -> Options = { Options.
|
|||||||
return element.replace(element.getReceiverExpression()) as JetElement
|
return element.replace(element.getReceiverExpression()) as JetElement
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// this class is needed to optimize imports only when we actually insert any import (optimization)
|
|
||||||
private class ImportInserter(val file: JetFile) {
|
|
||||||
private var optimizeImports = true
|
|
||||||
private val helper = ImportInsertHelper.getInstance(file.getProject())
|
|
||||||
|
|
||||||
fun addImport(target: DeclarationDescriptor): ImportInsertHelper.ImportDescriptorResult {
|
|
||||||
return helper.importDescriptor(file, target)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun optimizeImports(): Boolean {
|
|
||||||
if (!optimizeImports) return false
|
|
||||||
optimizeImports = false
|
|
||||||
return helper.optimizeImportsOnTheFly(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ package org.jetbrains.kotlin.idea.imports
|
|||||||
|
|
||||||
import com.google.common.collect.HashMultimap
|
import com.google.common.collect.HashMultimap
|
||||||
import com.intellij.lang.ImportOptimizer
|
import com.intellij.lang.ImportOptimizer
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
|
||||||
import com.intellij.openapi.progress.ProgressIndicatorProvider
|
import com.intellij.openapi.progress.ProgressIndicatorProvider
|
||||||
import com.intellij.psi.PsiElement
|
import com.intellij.psi.PsiElement
|
||||||
import com.intellij.psi.PsiFile
|
import com.intellij.psi.PsiFile
|
||||||
@@ -29,11 +28,11 @@ import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
|||||||
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
|
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
|
||||||
import org.jetbrains.kotlin.idea.core.formatter.JetCodeStyleSettings
|
import org.jetbrains.kotlin.idea.core.formatter.JetCodeStyleSettings
|
||||||
import org.jetbrains.kotlin.idea.references.JetReference
|
import org.jetbrains.kotlin.idea.references.JetReference
|
||||||
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
|
|
||||||
import org.jetbrains.kotlin.idea.util.ImportInsertHelper
|
import org.jetbrains.kotlin.idea.util.ImportInsertHelper
|
||||||
import org.jetbrains.kotlin.idea.util.application.runWriteAction
|
import org.jetbrains.kotlin.idea.util.application.runWriteAction
|
||||||
import org.jetbrains.kotlin.name.FqName
|
import org.jetbrains.kotlin.name.FqName
|
||||||
import org.jetbrains.kotlin.name.Name
|
import org.jetbrains.kotlin.name.Name
|
||||||
|
import org.jetbrains.kotlin.psi
|
||||||
import org.jetbrains.kotlin.psi.*
|
import org.jetbrains.kotlin.psi.*
|
||||||
import org.jetbrains.kotlin.psi.psiUtil.getReceiverExpression
|
import org.jetbrains.kotlin.psi.psiUtil.getReceiverExpression
|
||||||
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
|
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
|
||||||
@@ -56,32 +55,99 @@ public class KotlinImportOptimizer() : ImportOptimizer {
|
|||||||
OptimizeProcess(file as JetFile).execute()
|
OptimizeProcess(file as JetFile).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
private class OptimizeProcess(val file: JetFile) {
|
private class OptimizeProcess(private val file: JetFile) {
|
||||||
private val codeStyleSettings = JetCodeStyleSettings.getInstance(file.getProject())
|
|
||||||
private val aliasImports: Map<Name, FqName>
|
|
||||||
|
|
||||||
init {
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.aliasImports = aliasImports
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun execute() {
|
public fun execute() {
|
||||||
val oldImports = file.getImportDirectives()
|
val oldImports = file.importDirectives
|
||||||
if (oldImports.isEmpty()) return
|
if (oldImports.isEmpty()) return
|
||||||
|
|
||||||
//TODO: keep existing imports? at least aliases (comments)
|
//TODO: keep existing imports? at least aliases (comments)
|
||||||
|
|
||||||
val importInsertHelper = ImportInsertHelper.getInstance(file.getProject())
|
val visitor = CollectUsedDescriptorsVisitor(file, recursive = true)
|
||||||
|
file.accept(visitor)
|
||||||
|
val descriptorsToImport = visitor.descriptors
|
||||||
|
|
||||||
val descriptorsToImport = detectDescriptorsToImport()
|
val imports = prepareOptimizedImports(file, descriptorsToImport) ?: return
|
||||||
|
|
||||||
|
runWriteAction { replaceImports(file, imports) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public open class CollectUsedDescriptorsVisitor(val file: JetFile, val recursive: Boolean) : JetVisitorVoid() {
|
||||||
|
private val _descriptors = HashSet<DeclarationDescriptor>()
|
||||||
|
private val currentPackageName = file.packageFqName
|
||||||
|
|
||||||
|
public val descriptors: Set<DeclarationDescriptor>
|
||||||
|
get() = _descriptors
|
||||||
|
|
||||||
|
override fun visitElement(element: PsiElement) {
|
||||||
|
if (recursive) {
|
||||||
|
ProgressIndicatorProvider.checkCanceled()
|
||||||
|
element.acceptChildren(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitImportList(importList: JetImportList) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitPackageDirective(directive: JetPackageDirective) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitJetElement(element: JetElement) {
|
||||||
|
if (!recursive && element.parents.any { it is JetImportDirective || it is JetPackageDirective }) return
|
||||||
|
|
||||||
|
for (reference in element.references) {
|
||||||
|
if (reference !is JetReference) continue
|
||||||
|
|
||||||
|
val referencedName = (element as? JetNameReferenceExpression)?.getReferencedNameAsName() //TODO: other types of references
|
||||||
|
|
||||||
|
val bindingContext = element.analyze()
|
||||||
|
//class qualifiers that refer to companion objects should be considered (containing) class references
|
||||||
|
val targets = bindingContext[BindingContext.SHORT_REFERENCE_TO_COMPANION_OBJECT, element as? JetReferenceExpression]?.let { listOf(it) }
|
||||||
|
?: reference.resolveToDescriptors(bindingContext)
|
||||||
|
for (target in targets) {
|
||||||
|
if (!target.canBeReferencedViaImport()) continue
|
||||||
|
val importableDescriptor = target.getImportableDescriptor()
|
||||||
|
val parentFqName = DescriptorUtils.getFqNameSafe(importableDescriptor).parent()
|
||||||
|
if (target is PackageViewDescriptor && parentFqName == FqName.ROOT) continue // no need to import top-level packages
|
||||||
|
if (target !is PackageViewDescriptor && parentFqName == currentPackageName) continue
|
||||||
|
|
||||||
|
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 (referencedName != null && importableDescriptor.name != referencedName) continue // resolved via alias
|
||||||
|
|
||||||
|
if (isAccessibleAsMember(importableDescriptor, element)) continue
|
||||||
|
|
||||||
|
_descriptors.add(importableDescriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.visitJetElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAccessibleAsMember(target: DeclarationDescriptor, place: JetElement): Boolean {
|
||||||
|
val container = target.containingDeclaration
|
||||||
|
if (container !is ClassDescriptor) return false
|
||||||
|
val scope = if (DescriptorUtils.isCompanionObject(container))
|
||||||
|
container.getContainingDeclaration() as? ClassDescriptor ?: return false
|
||||||
|
else
|
||||||
|
container
|
||||||
|
val classBody = (DescriptorToSourceUtils.getSourceFromDescriptor(scope) as? JetClassOrObject)?.getBody()
|
||||||
|
return classBody != null && classBody.containingFile == file && classBody.isAncestor(place)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
public fun prepareOptimizedImports(
|
||||||
|
file: JetFile,
|
||||||
|
descriptorsToImport: Collection<DeclarationDescriptor>
|
||||||
|
): List<ImportPath>? {
|
||||||
|
val importInsertHelper = ImportInsertHelper.getInstance(file.project)
|
||||||
|
val codeStyleSettings = JetCodeStyleSettings.getInstance(file.project)
|
||||||
|
val aliasImports = buildAliasImportMap(file)
|
||||||
|
|
||||||
val importsToGenerate = HashSet<ImportPath>()
|
val importsToGenerate = HashSet<ImportPath>()
|
||||||
|
|
||||||
@@ -127,8 +193,8 @@ public class KotlinImportOptimizer() : ImportOptimizer {
|
|||||||
|
|
||||||
// now check that there are no conflicts and all classes are really imported
|
// now check that there are no conflicts and all classes are really imported
|
||||||
val fileWithImportsText = StringBuilder {
|
val fileWithImportsText = StringBuilder {
|
||||||
append("package ").append(file.getPackageFqName().render()).append("\n")
|
append("package ").append(file.packageFqName.render()).append("\n")
|
||||||
importsToGenerate.filter { it.isAllUnder() }.map { "import " + it.getPathStr() }.joinTo(this, "\n")
|
importsToGenerate.filter { it.isAllUnder() }.map { "import " + it.pathStr }.joinTo(this, "\n")
|
||||||
}.toString()
|
}.toString()
|
||||||
val fileWithImports = JetPsiFactory(file).createAnalyzableFile("Dummy.kt", fileWithImportsText, file)
|
val fileWithImports = JetPsiFactory(file).createAnalyzableFile("Dummy.kt", fileWithImportsText, file)
|
||||||
val scope = fileWithImports.getResolutionFacade().getFileTopLevelScope(fileWithImports)
|
val scope = fileWithImports.getResolutionFacade().getFileTopLevelScope(fileWithImports)
|
||||||
@@ -156,94 +222,37 @@ public class KotlinImportOptimizer() : ImportOptimizer {
|
|||||||
val sortedImportsToGenerate = importsToGenerate.sortBy(importInsertHelper.importSortComparator)
|
val sortedImportsToGenerate = importsToGenerate.sortBy(importInsertHelper.importSortComparator)
|
||||||
|
|
||||||
// check if no changes to imports required
|
// check if no changes to imports required
|
||||||
if (oldImports.size() == sortedImportsToGenerate.size() && oldImports.map { it.getImportPath() } == sortedImportsToGenerate) return
|
val oldImports = file.importDirectives
|
||||||
|
if (oldImports.size() == sortedImportsToGenerate.size() && oldImports.map { it.importPath } == sortedImportsToGenerate) return null
|
||||||
|
|
||||||
runWriteAction {
|
return sortedImportsToGenerate
|
||||||
val importList = file.getImportList()!!
|
}
|
||||||
val psiFactory = JetPsiFactory(file.getProject())
|
|
||||||
for (importPath in sortedImportsToGenerate) {
|
|
||||||
importList.addBefore(psiFactory.createImportDirective(importPath), oldImports.lastOrNull()) // insert into the middle to keep collapsed state
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove old imports after adding new ones to keep imports folding state
|
private fun buildAliasImportMap(file: JetFile): Map<Name, FqName> {
|
||||||
for (import in oldImports) {
|
val imports = file.importDirectives
|
||||||
import.delete()
|
val aliasImports = HashMap<Name, FqName>()
|
||||||
|
for (import in imports) {
|
||||||
|
val path = import.importPath ?: continue
|
||||||
|
val aliasName = path.alias
|
||||||
|
if (aliasName != null) {
|
||||||
|
aliasImports.put(aliasName, path.fqnPart())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return aliasImports
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun detectDescriptorsToImport(): Set<DeclarationDescriptor> {
|
public fun replaceImports(file: JetFile, imports: List<ImportPath>) {
|
||||||
val visitor = CollectUsedDescriptorsVisitor(file, recursive = true)
|
val importList = file.importList!!
|
||||||
file.accept(visitor)
|
val oldImports = importList.imports
|
||||||
return visitor.descriptors
|
val psiFactory = JetPsiFactory(file.project)
|
||||||
}
|
for (importPath in imports) {
|
||||||
}
|
importList.addBefore(psiFactory.createImportDirective(importPath), oldImports.lastOrNull()) // insert into the middle to keep collapsed state
|
||||||
|
|
||||||
public class CollectUsedDescriptorsVisitor(val file: JetFile, val recursive: Boolean) : JetVisitorVoid() {
|
|
||||||
private val _descriptors = HashSet<DeclarationDescriptor>()
|
|
||||||
private val currentPackageName = file.getPackageFqName()
|
|
||||||
|
|
||||||
public val descriptors: Set<DeclarationDescriptor>
|
|
||||||
get() = _descriptors
|
|
||||||
|
|
||||||
override fun visitElement(element: PsiElement) {
|
|
||||||
if (recursive) {
|
|
||||||
ProgressIndicatorProvider.checkCanceled()
|
|
||||||
element.acceptChildren(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun visitImportList(importList: JetImportList) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun visitPackageDirective(directive: JetPackageDirective) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun visitJetElement(element: JetElement) {
|
|
||||||
if (!recursive && element.parents.any { it is JetImportDirective || it is JetPackageDirective }) return
|
|
||||||
|
|
||||||
for (reference in element.getReferences()) {
|
|
||||||
if (reference !is JetReference) continue
|
|
||||||
|
|
||||||
val referencedName = (element as? JetNameReferenceExpression)?.getReferencedNameAsName() //TODO: other types of references
|
|
||||||
|
|
||||||
val bindingContext = element.analyze()
|
|
||||||
//class qualifiers that refer to companion objects should be considered (containing) class references
|
|
||||||
val targets = bindingContext[BindingContext.SHORT_REFERENCE_TO_COMPANION_OBJECT, element as? JetReferenceExpression]?.let { listOf(it) }
|
|
||||||
?: reference.resolveToDescriptors(bindingContext)
|
|
||||||
for (target in targets) {
|
|
||||||
if (!target.canBeReferencedViaImport()) continue
|
|
||||||
val importableDescriptor = target.getImportableDescriptor()
|
|
||||||
val parentFqName = DescriptorUtils.getFqNameSafe(importableDescriptor).parent()
|
|
||||||
if (target is PackageViewDescriptor && parentFqName == FqName.ROOT) continue // no need to import top-level packages
|
|
||||||
if (target !is PackageViewDescriptor && parentFqName == currentPackageName) continue
|
|
||||||
|
|
||||||
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 (referencedName != null && importableDescriptor.getName() != referencedName) continue // resolved via alias
|
|
||||||
|
|
||||||
if (isAccessibleAsMember(importableDescriptor, element)) continue
|
|
||||||
|
|
||||||
_descriptors.add(importableDescriptor)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
super.visitJetElement(element)
|
// remove old imports after adding new ones to keep imports folding state
|
||||||
}
|
for (import in oldImports) {
|
||||||
|
import.delete()
|
||||||
private fun isAccessibleAsMember(target: DeclarationDescriptor, place: JetElement): Boolean {
|
}
|
||||||
val container = target.getContainingDeclaration()
|
|
||||||
if (container !is ClassDescriptor) return false
|
|
||||||
val scope = if (DescriptorUtils.isCompanionObject(container))
|
|
||||||
container.getContainingDeclaration() as? ClassDescriptor ?: return false
|
|
||||||
else
|
|
||||||
container
|
|
||||||
val classBody = (DescriptorToSourceUtils.getSourceFromDescriptor(scope) as? JetClassOrObject)?.getBody()
|
|
||||||
return classBody != null && classBody.getContainingFile() == file && classBody.isAncestor(place)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,26 @@
|
|||||||
|
|
||||||
package org.jetbrains.kotlin.idea.inspections
|
package org.jetbrains.kotlin.idea.inspections
|
||||||
|
|
||||||
|
import com.intellij.codeInsight.CodeInsightSettings
|
||||||
import com.intellij.codeInsight.actions.OptimizeImportsProcessor
|
import com.intellij.codeInsight.actions.OptimizeImportsProcessor
|
||||||
|
import com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerEx
|
||||||
|
import com.intellij.codeInsight.daemon.impl.DaemonListeners
|
||||||
|
import com.intellij.codeInsight.daemon.impl.HighlightingSessionImpl
|
||||||
import com.intellij.codeInspection.*
|
import com.intellij.codeInspection.*
|
||||||
|
import com.intellij.lang.annotation.HighlightSeverity
|
||||||
|
import com.intellij.openapi.Disposable
|
||||||
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
|
import com.intellij.openapi.command.undo.UndoManager
|
||||||
|
import com.intellij.openapi.editor.Editor
|
||||||
|
import com.intellij.openapi.progress.ProgressManager
|
||||||
|
import com.intellij.openapi.progress.util.ProgressWrapper
|
||||||
import com.intellij.openapi.project.Project
|
import com.intellij.openapi.project.Project
|
||||||
|
import com.intellij.openapi.util.Disposer
|
||||||
import com.intellij.openapi.util.Key
|
import com.intellij.openapi.util.Key
|
||||||
|
import com.intellij.psi.PsiDocumentManager
|
||||||
import com.intellij.psi.PsiElementVisitor
|
import com.intellij.psi.PsiElementVisitor
|
||||||
|
import com.intellij.util.DocumentUtil
|
||||||
|
import com.intellij.util.Processor
|
||||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
||||||
import org.jetbrains.kotlin.idea.imports.KotlinImportOptimizer
|
import org.jetbrains.kotlin.idea.imports.KotlinImportOptimizer
|
||||||
import org.jetbrains.kotlin.idea.imports.importableFqNameSafe
|
import org.jetbrains.kotlin.idea.imports.importableFqNameSafe
|
||||||
@@ -29,26 +44,37 @@ import org.jetbrains.kotlin.psi.JetFile
|
|||||||
import org.jetbrains.kotlin.psi.JetImportDirective
|
import org.jetbrains.kotlin.psi.JetImportDirective
|
||||||
import org.jetbrains.kotlin.psi.JetSimpleNameExpression
|
import org.jetbrains.kotlin.psi.JetSimpleNameExpression
|
||||||
import org.jetbrains.kotlin.psi.psiUtil.getQualifiedElementSelector
|
import org.jetbrains.kotlin.psi.psiUtil.getQualifiedElementSelector
|
||||||
|
import org.jetbrains.kotlin.resolve.ImportPath
|
||||||
import org.jetbrains.kotlin.resolve.bindingContextUtil.getReferenceTargets
|
import org.jetbrains.kotlin.resolve.bindingContextUtil.getReferenceTargets
|
||||||
import java.util.HashSet
|
import java.util.HashSet
|
||||||
|
|
||||||
class UnusedImportInspection : AbstractKotlinInspection() {
|
class UnusedImportInspection : AbstractKotlinInspection() {
|
||||||
private val visitorKey = Key<KotlinImportOptimizer.CollectUsedDescriptorsVisitor>("UnusedImportInspection.visitorKey")
|
private val visitorKey = Key<MyVisitor>("UnusedImportInspection.visitorKey")
|
||||||
|
|
||||||
override fun runForWholeFile() = true
|
override fun runForWholeFile() = true
|
||||||
|
|
||||||
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
|
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
|
||||||
val visitor = KotlinImportOptimizer.CollectUsedDescriptorsVisitor(session.getFile() as JetFile, recursive = false)
|
val visitor = MyVisitor(session.file as JetFile)
|
||||||
session.putUserData(visitorKey, visitor)
|
session.putUserData(visitorKey, visitor)
|
||||||
return visitor
|
return visitor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class MyVisitor(file: JetFile) : KotlinImportOptimizer.CollectUsedDescriptorsVisitor(file, recursive = false) {
|
||||||
|
var anyImportVisited = false
|
||||||
|
|
||||||
|
override fun visitImportDirective(importDirective: JetImportDirective) {
|
||||||
|
anyImportVisited = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun inspectionFinished(session: LocalInspectionToolSession, problemsHolder: ProblemsHolder) {
|
override fun inspectionFinished(session: LocalInspectionToolSession, problemsHolder: ProblemsHolder) {
|
||||||
val usedDescriptors = session.getUserData(visitorKey)!!.descriptors
|
val visitor = session.getUserData(visitorKey)!!
|
||||||
|
if (!visitor.anyImportVisited) return // workaround for the problem (bug?) that IDEA does not call us for all elements when we press Enter
|
||||||
|
val descriptorsToImport = visitor.descriptors
|
||||||
|
|
||||||
val fqNames = HashSet<FqName>()
|
val fqNames = HashSet<FqName>()
|
||||||
val parentFqNames = HashSet<FqName>()
|
val parentFqNames = HashSet<FqName>()
|
||||||
for (descriptor in usedDescriptors) {
|
for (descriptor in descriptorsToImport) {
|
||||||
val fqName = descriptor.importableFqNameSafe
|
val fqName = descriptor.importableFqNameSafe
|
||||||
fqNames.add(fqName)
|
fqNames.add(fqName)
|
||||||
val parentFqName = fqName.parent()
|
val parentFqName = fqName.parent()
|
||||||
@@ -57,11 +83,11 @@ class UnusedImportInspection : AbstractKotlinInspection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val file = session.getFile() as JetFile
|
val file = session.file as JetFile
|
||||||
val directives = file.getImportDirectives()
|
val directives = file.importDirectives
|
||||||
for (directive in directives) {
|
for (directive in directives) {
|
||||||
val importPath = directive.getImportPath() ?: continue
|
val importPath = directive.importPath ?: continue
|
||||||
if (importPath.getAlias() != null) continue // highlighting of unused alias imports not supported yet
|
if (importPath.alias != null) continue // highlighting of unused alias imports not supported yet
|
||||||
val isUsed = if (importPath.isAllUnder()) {
|
val isUsed = if (importPath.isAllUnder()) {
|
||||||
importPath.fqnPart() in parentFqNames
|
importPath.fqnPart() in parentFqNames
|
||||||
}
|
}
|
||||||
@@ -70,7 +96,7 @@ class UnusedImportInspection : AbstractKotlinInspection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isUsed) {
|
if (!isUsed) {
|
||||||
val nameExpression = directive.getImportedReference()?.getQualifiedElementSelector() as? JetSimpleNameExpression
|
val nameExpression = directive.importedReference?.getQualifiedElementSelector() as? JetSimpleNameExpression
|
||||||
if (nameExpression == null || nameExpression.getReferenceTargets(nameExpression.analyze()).isEmpty()) continue // do not highlight unresolved imports as unused
|
if (nameExpression == null || nameExpression.getReferenceTargets(nameExpression.analyze()).isEmpty()) continue // do not highlight unresolved imports as unused
|
||||||
|
|
||||||
problemsHolder.registerProblem(
|
problemsHolder.registerProblem(
|
||||||
@@ -80,12 +106,70 @@ class UnusedImportInspection : AbstractKotlinInspection() {
|
|||||||
OptimizeImportsQuickFix(file))
|
OptimizeImportsQuickFix(file))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (CodeInsightSettings.getInstance().OPTIMIZE_IMPORTS_ON_THE_FLY) {
|
||||||
|
val optimizedImports = KotlinImportOptimizer.prepareOptimizedImports(file, descriptorsToImport) ?: return // return if already optimized
|
||||||
|
|
||||||
|
// unwrap progress indicator
|
||||||
|
val progress = sequence(ProgressManager.getInstance().progressIndicator) {
|
||||||
|
(it as? ProgressWrapper)?.originalProgressIndicator
|
||||||
|
}.last()
|
||||||
|
val highlightingSession = HighlightingSessionImpl.getHighlightingSession(file, progress)
|
||||||
|
|
||||||
|
val project = highlightingSession.project
|
||||||
|
val editor = highlightingSession.editor
|
||||||
|
if (editor != null) {
|
||||||
|
Disposer.register(highlightingSession, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
// later because should invoke when highlighting is finished
|
||||||
|
ApplicationManager.getApplication().invokeLater {
|
||||||
|
if (timeToOptimizeImportsOnTheFly(file, editor, project)) {
|
||||||
|
optimizeImportsOnTheFly(file, optimizedImports, editor, project)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun timeToOptimizeImportsOnTheFly(file: JetFile, editor: Editor, project: Project): Boolean {
|
||||||
|
if (project.isDisposed() || !file.isValid() || editor.isDisposed()) return false
|
||||||
|
|
||||||
|
if (!file.isWritable()) return false
|
||||||
|
|
||||||
|
// do not optimize imports on the fly during undo/redo
|
||||||
|
val undoManager = UndoManager.getInstance(editor.project)
|
||||||
|
if (undoManager.isUndoInProgress() || undoManager.isRedoInProgress()) return false
|
||||||
|
|
||||||
|
// if we stand inside import statements, do not optimize
|
||||||
|
val importsRange = file.importList?.textRange ?: return false
|
||||||
|
if (importsRange.containsOffset(editor.caretModel.offset)) return false
|
||||||
|
|
||||||
|
val codeAnalyzer = DaemonCodeAnalyzerEx.getInstanceEx(project)
|
||||||
|
if (!codeAnalyzer.isHighlightingAvailable(file)) return false
|
||||||
|
if (!codeAnalyzer.isErrorAnalyzingFinished(file)) return false
|
||||||
|
|
||||||
|
val document = editor.document
|
||||||
|
var hasErrors = false
|
||||||
|
DaemonCodeAnalyzerEx.processHighlights(document, project, HighlightSeverity.ERROR, 0, document.textLength, Processor { hasErrors = true; false })
|
||||||
|
if (hasErrors) return false
|
||||||
|
|
||||||
|
return DaemonListeners.canChangeFileSilently(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun optimizeImportsOnTheFly(file: JetFile, optimizedImports: List<ImportPath>, editor: Editor, project: Project) {
|
||||||
|
PsiDocumentManager.getInstance(file.project).commitAllDocuments()
|
||||||
|
DocumentUtil.writeInRunUndoTransparentAction {
|
||||||
|
KotlinImportOptimizer.replaceImports(file, optimizedImports)
|
||||||
|
PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class OptimizeImportsQuickFix(private val file: JetFile): LocalQuickFix {
|
private class OptimizeImportsQuickFix(private val file: JetFile): LocalQuickFix {
|
||||||
override fun getName() = "Optimize imports"
|
override fun getName() = "Optimize imports"
|
||||||
|
|
||||||
override fun getFamilyName() = getName()
|
override fun getFamilyName() = name
|
||||||
|
|
||||||
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
|
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
|
||||||
OptimizeImportsProcessor(project, file).run()
|
OptimizeImportsProcessor(project, file).run()
|
||||||
|
|||||||
@@ -55,16 +55,6 @@ public class ImportInsertHelperImpl(private val project: Project) : ImportInsert
|
|||||||
private val codeStyleSettings: JetCodeStyleSettings
|
private val codeStyleSettings: JetCodeStyleSettings
|
||||||
get() = JetCodeStyleSettings.getInstance(project)
|
get() = JetCodeStyleSettings.getInstance(project)
|
||||||
|
|
||||||
override fun optimizeImportsOnTheFly(file: JetFile): Boolean {
|
|
||||||
if (CodeInsightSettings.getInstance().OPTIMIZE_IMPORTS_ON_THE_FLY) {
|
|
||||||
OptimizeImportsProcessor(project, file).runWithoutProgress()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val importSortComparator: Comparator<ImportPath>
|
override val importSortComparator: Comparator<ImportPath>
|
||||||
get() = ImportPathComparator
|
get() = ImportPathComparator
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user