Optimize imports to prevent accedential changes in resolve because of import priorities

#KT-11640 Fixed
This commit is contained in:
Valentin Kipyatkov
2016-09-15 15:30:56 +03:00
parent 7628fa1608
commit 95a3a29382
40 changed files with 651 additions and 191 deletions
@@ -19,23 +19,14 @@
package org.jetbrains.kotlin.idea.imports
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.util.ImportInsertHelper
import org.jetbrains.kotlin.idea.util.getFileResolutionScope
import org.jetbrains.kotlin.incremental.components.NoLookupLocation
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.psi.KtReferenceExpression
import org.jetbrains.kotlin.renderer.render
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.ImportPath
import org.jetbrains.kotlin.resolve.bindingContextUtil.getReferenceTargets
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.resolve.descriptorUtil.getImportableDescriptor
import org.jetbrains.kotlin.resolve.scopes.utils.findClassifier
import org.jetbrains.kotlin.types.KotlinType
import java.util.*
@@ -97,117 +88,3 @@ fun KtReferenceExpression.getImportableTargets(bindingContext: BindingContext):
return targets.map { it.getImportableDescriptor() }.toSet()
}
fun prepareOptimizedImports(
file: KtFile,
descriptorsToImport: Collection<DeclarationDescriptor>,
nameCountToUseStarImport: Int,
nameCountToUseStarImportForMembers: Int,
isInPackagesToUseStarImport: (FqName) -> Boolean
): List<ImportPath>? {
val importInsertHelper = ImportInsertHelper.getInstance(file.project)
val aliasImports = buildAliasImportMap(file)
val importsToGenerate = HashSet<ImportPath>()
val descriptorsByParentFqName = hashMapOf<FqName, MutableSet<DeclarationDescriptor>>()
for (descriptor in descriptorsToImport) {
val fqName = descriptor.importableFqName!!
val container = descriptor.containingDeclaration
val parentFqName = fqName.parent()
val canUseStarImport = when {
parentFqName.isRoot -> false
(container as? ClassDescriptor)?.kind == ClassKind.OBJECT -> false
else -> true
}
if (canUseStarImport) {
val descriptors = descriptorsByParentFqName.getOrPut(parentFqName) { hashSetOf() }
descriptors.add(descriptor)
}
else {
importsToGenerate.add(ImportPath(fqName, false))
}
}
val classNamesToCheck = HashSet<FqName>()
fun isImportedByDefault(fqName: FqName) = importInsertHelper.isImportedWithDefault(ImportPath(fqName, false), file)
for (parentFqName in descriptorsByParentFqName.keys) {
val descriptors = descriptorsByParentFqName[parentFqName]!!
val fqNames = descriptors.map { it.importableFqName!! }.toSet()
val isMember = descriptors.first().containingDeclaration is ClassDescriptor
val nameCountToUseStar = if (isMember)
nameCountToUseStarImportForMembers
else
nameCountToUseStarImport
val explicitImports = fqNames.size < nameCountToUseStar && !isInPackagesToUseStarImport(parentFqName)
if (explicitImports) {
for (fqName in fqNames) {
if (!isImportedByDefault(fqName)) {
importsToGenerate.add(ImportPath(fqName, false))
}
}
}
else {
for (descriptor in descriptors) {
if (descriptor is ClassDescriptor) {
classNamesToCheck.add(descriptor.importableFqName!!)
}
}
if (!fqNames.all(::isImportedByDefault)) {
importsToGenerate.add(ImportPath(parentFqName, true))
}
}
}
// now check that there are no conflicts and all classes are really imported
val fileWithImportsText = buildString {
append("package ").append(file.packageFqName.toUnsafe().render()).append("\n")
importsToGenerate.filter { it.isAllUnder }.map { "import " + it.pathStr }.joinTo(this, "\n")
}
val fileWithImports = KtPsiFactory(file).createAnalyzableFile("Dummy.kt", fileWithImportsText, file)
val scope = fileWithImports.getResolutionFacade().getFileResolutionScope(fileWithImports)
for (fqName in classNamesToCheck) {
if (scope.findClassifier(fqName.shortName(), NoLookupLocation.FROM_IDE)?.importableFqName != fqName) {
// add explicit import if failed to import with * (or from current package)
importsToGenerate.add(ImportPath(fqName, false))
val parentFqName = fqName.parent()
val parentDescriptors = descriptorsByParentFqName[parentFqName]!!
for (descriptor in parentDescriptors.filter { it.importableFqName == fqName }) {
parentDescriptors.remove(descriptor)
}
if (parentDescriptors.isEmpty()) { // star import is not really needed
importsToGenerate.remove(ImportPath(parentFqName, true))
}
}
}
//TODO: drop unused aliases?
aliasImports.mapTo(importsToGenerate) { ImportPath(it.value, false, it.key) }
val sortedImportsToGenerate = importsToGenerate.sortedWith(importInsertHelper.importSortComparator)
// check if no changes to imports required
val oldImports = file.importDirectives
if (oldImports.size == sortedImportsToGenerate.size && oldImports.map { it.importPath } == sortedImportsToGenerate) return null
return sortedImportsToGenerate
}
private fun buildAliasImportMap(file: KtFile): Map<Name, FqName> {
val imports = file.importDirectives
val aliasImports = HashMap<Name, FqName>()
for (import in imports) {
val path = import.importPath ?: continue
val aliasName = path.alias
if (aliasName != null && aliasName != path.fqnPart().shortName() /* we do not keep trivial aliases */) {
aliasImports.put(aliasName, path.fqnPart())
}
}
return aliasImports
}
@@ -0,0 +1,301 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.imports
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.idea.analysis.analyzeInContext
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.resolve.frontendService
import org.jetbrains.kotlin.idea.util.ImportInsertHelper
import org.jetbrains.kotlin.idea.util.getResolutionScope
import org.jetbrains.kotlin.incremental.components.NoLookupLocation
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.renderer.render
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.ImportPath
import org.jetbrains.kotlin.resolve.lazy.FileScopeProvider
import org.jetbrains.kotlin.resolve.scopes.ImportingScope
import org.jetbrains.kotlin.resolve.scopes.utils.findClassifier
import org.jetbrains.kotlin.resolve.scopes.utils.parentsWithSelf
import org.jetbrains.kotlin.resolve.scopes.utils.replaceImportingScopes
import org.jetbrains.kotlin.utils.singletonOrEmptyList
import java.util.*
class OptimizedImportsBuilder(
private val file: KtFile,
private val data: InputData,
private val options: Options
) {
companion object {
@TestOnly
var testLog: StringBuilder? = null
}
interface AbstractReference {
val element: KtElement
val dependsOnNames: Collection<Name>
fun resolve(bindingContext: BindingContext): Collection<DeclarationDescriptor>
}
data class InputData(
val descriptorsToImport: Set<DeclarationDescriptor>,
val references: Collection<AbstractReference>
)
data class Options(
val nameCountToUseStarImport: Int,
val nameCountToUseStarImportForMembers: Int,
val isInPackagesToUseStarImport: (FqName) -> Boolean
)
private val importInsertHelper = ImportInsertHelper.getInstance(file.project)
private val lockedImports = HashSet<ImportPath>()
fun buildOptimizedImports(): List<ImportPath>? {
// TODO: should we drop unused aliases?
// keep all non-trivial aliases
file.importDirectives
.mapNotNull { it.importPath }
.filterTo(lockedImports) {
val aliasName = it.alias
aliasName != null && aliasName != it.fqnPart().shortName()
}
while (true) {
val lockedImportsBefore = lockedImports.size
val result = tryBuildOptimizedImports()
if (lockedImports.size == lockedImportsBefore) return result
testLog?.append("Trying to build import list again with locked imports: ${lockedImports.joinToString { it.pathStr }}\n")
}
}
private fun getExpressionToAnalyze(element: KtElement): KtExpression? {
val parent = element.parent
return when {
parent is KtQualifiedExpression && element == parent.selectorExpression -> parent
parent is KtCallExpression && element == parent.calleeExpression -> getExpressionToAnalyze(parent)
parent is KtUserType -> null //TODO: is it always correct?
else -> element as? KtExpression //TODO: what if not expression? Example: KtPropertyDelegationMethodsReference
}
}
private fun tryBuildOptimizedImports(): List<ImportPath>? {
val importsToGenerate = HashSet<ImportPath>()
importsToGenerate.addAll(lockedImports)
val descriptorsByParentFqName = HashMap<FqName, MutableSet<DeclarationDescriptor>>()
for (descriptor in data.descriptorsToImport) {
val fqName = descriptor.importableFqName!!
val explicitImportPath = ImportPath(fqName, false)
if (explicitImportPath in lockedImports) continue
if (canUseStarImport(descriptor, fqName)) {
descriptorsByParentFqName.getOrPut(fqName.parent()) { HashSet() }.add(descriptor)
}
else {
importsToGenerate.add(explicitImportPath)
}
}
val classNamesToCheck = HashSet<FqName>()
for (parentFqName in descriptorsByParentFqName.keys) {
val starImportPath = ImportPath(parentFqName, true)
if (starImportPath in lockedImports) continue
val descriptors = descriptorsByParentFqName[parentFqName]!!
val fqNames = descriptors.map { it.importableFqName!! }.toSet()
val nameCountToUseStar = descriptors.first().nameCountToUseStar()
val useExplicitImports = fqNames.size < nameCountToUseStar && !options.isInPackagesToUseStarImport(parentFqName)
if (useExplicitImports) {
fqNames
.filter { !isImportedByDefault(it) }
.mapTo(importsToGenerate) { ImportPath(it, false) }
}
else {
descriptors
.filterIsInstance<ClassDescriptor>()
.mapTo(classNamesToCheck) { it.importableFqName!! }
if (!fqNames.all(this::isImportedByDefault)) {
importsToGenerate.add(starImportPath)
}
}
}
// now check that there are no conflicts and all classes are really imported
addExplicitImportsForClassesWhenRequired(classNamesToCheck, descriptorsByParentFqName, importsToGenerate, file)
val sortedImportsToGenerate = importsToGenerate.sortedWith(importInsertHelper.importSortComparator)
// check if no changes to imports required
val oldImports = file.importDirectives
if (oldImports.size == sortedImportsToGenerate.size && oldImports.map { it.importPath } == sortedImportsToGenerate) return null
val originalFileScope = file.getFileResolutionScope()
val newFileScope = buildScopeByImports(file, sortedImportsToGenerate)
var references = data.references
if (testLog != null) {
// to make log the same for all runs
references = references.sortedBy { it.toString() }
}
for ((names, refs) in references.groupBy { it.dependsOnNames }) {
if (!areScopeSlicesEqual(originalFileScope, newFileScope, names)) {
for (ref in refs) {
val element = ref.element
val bindingContext = element.analyze()
val expressionToAnalyze = getExpressionToAnalyze(element) ?: continue
val newScope = element.getResolutionScope(bindingContext, file.getResolutionFacade()).replaceImportingScopes(newFileScope)
val newBindingContext = expressionToAnalyze.analyzeInContext(newScope, expressionToAnalyze)
testLog?.append("Additional checking of reference $ref\n")
val oldTargets = ref.resolve(bindingContext)
val newTargets = ref.resolve(newBindingContext)
if (!areTargetsEqual(oldTargets, newTargets)) {
testLog?.append("Changed resolve of $ref\n")
(oldTargets + newTargets).forEach { lockImportForDescriptor(it) }
}
}
}
}
return sortedImportsToGenerate
}
private fun lockImportForDescriptor(descriptor: DeclarationDescriptor) {
val fqName = descriptor.importableFqName ?: return
val explicitImportPath = ImportPath(fqName, false)
val starImportPath = ImportPath(fqName.parent(), true)
if (file.importDirectives.any { it.importPath == explicitImportPath }) {
lockedImports.add(explicitImportPath)
}
else if (file.importDirectives.any { it.importPath == starImportPath }) {
lockedImports.add(starImportPath)
}
}
private fun addExplicitImportsForClassesWhenRequired(
classNamesToCheck: Collection<FqName>,
descriptorsByParentFqName: Map<FqName, MutableSet<DeclarationDescriptor>>,
importsToGenerate: MutableSet<ImportPath>,
originalFile: KtFile
) {
val scope = buildScopeByImports(originalFile, importsToGenerate.filter { it.isAllUnder })
for (fqName in classNamesToCheck) {
if (scope.findClassifier(fqName.shortName(), NoLookupLocation.FROM_IDE)?.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 = descriptorsByParentFqName[parentFqName]!!
for (descriptor in siblingsToImport.filter { it.importableFqName == fqName }) {
siblingsToImport.remove(descriptor)
}
if (siblingsToImport.isEmpty()) { // star import is not really needed
importsToGenerate.remove(ImportPath(parentFqName, true))
}
}
}
}
private fun buildScopeByImports(originalFile: KtFile, importsToGenerate: Collection<ImportPath>): ImportingScope {
val fileText = buildString {
append("package ")
append(originalFile.packageFqName.toUnsafe().render())
append("\n")
for (importPath in importsToGenerate) {
append("import ")
append(importPath.pathStr)
if (importPath.hasAlias()) {
append("=")
append(importPath.alias!!.render())
}
append("\n")
}
}
val fileWithImports = KtPsiFactory(originalFile).createAnalyzableFile("Dummy.kt", fileText, originalFile)
return fileWithImports.getFileResolutionScope()
}
private fun KtFile.getFileResolutionScope() = getResolutionFacade().frontendService<FileScopeProvider>().getFileScopes(this).importingScope
private fun areScopeSlicesEqual(scope1: ImportingScope, scope2: ImportingScope, names: Collection<Name>): Boolean {
val tower1 = scope1.extractSliceTower(names)
val tower2 = scope2.extractSliceTower(names)
val iterator1 = tower1.iterator()
val iterator2 = tower2.iterator()
while (true) {
if (!iterator1.hasNext()) {
return !iterator2.hasNext()
}
else if (!iterator2.hasNext()) {
return false
}
else {
if (!areTargetsEqual(iterator1.next(), iterator2.next())) return false
}
}
}
private fun ImportingScope.extractSliceTower(names: Collection<Name>): Sequence<Collection<DeclarationDescriptor>> {
return parentsWithSelf
.map { scope ->
names.flatMap { name ->
scope.getContributedFunctions(name, NoLookupLocation.FROM_IDE) +
scope.getContributedVariables(name, NoLookupLocation.FROM_IDE) +
scope.getContributedClassifier(name, NoLookupLocation.FROM_IDE).singletonOrEmptyList()
}
}
.filter { it.isNotEmpty() }
}
private fun canUseStarImport(descriptor: DeclarationDescriptor, fqName: FqName): Boolean {
return when {
fqName.parent().isRoot -> false
(descriptor.containingDeclaration as? ClassDescriptor)?.kind == ClassKind.OBJECT -> false
else -> true
}
}
private fun isImportedByDefault(fqName: FqName) = importInsertHelper.isImportedWithDefault(ImportPath(fqName, false), file)
private fun DeclarationDescriptor.nameCountToUseStar(): Int {
val isMember = containingDeclaration is ClassDescriptor
return if (isMember)
options.nameCountToUseStarImportForMembers
else
options.nameCountToUseStarImport
}
private fun areTargetsEqual(descriptors1: Collection<DeclarationDescriptor>, descriptors2: Collection<DeclarationDescriptor>): Boolean {
return descriptors1.size == descriptors2.size &&
descriptors1.zip(descriptors2).all { it.first.importableFqName == it.second.importableFqName } //TODO: can have different order?
}
}