Shorten references to use analyze() for multiple elements

This commit is contained in:
Valentin Kipyatkov
2016-09-07 13:02:51 +03:00
parent 6efb3ac009
commit fc27212456
@@ -32,7 +32,6 @@ import org.jetbrains.kotlin.idea.util.ImportInsertHelper
import org.jetbrains.kotlin.idea.util.getResolutionScope import org.jetbrains.kotlin.idea.util.getResolutionScope
import org.jetbrains.kotlin.incremental.components.NoLookupLocation import org.jetbrains.kotlin.incremental.components.NoLookupLocation
import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext
import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.parents
import org.jetbrains.kotlin.renderer.DescriptorRenderer import org.jetbrains.kotlin.renderer.DescriptorRenderer
import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.BindingContext
@@ -45,6 +44,7 @@ import org.jetbrains.kotlin.resolve.scopes.utils.findClassifier
import org.jetbrains.kotlin.resolve.scopes.utils.findPackage import org.jetbrains.kotlin.resolve.scopes.utils.findPackage
import org.jetbrains.kotlin.resolve.source.getPsi import org.jetbrains.kotlin.resolve.source.getPsi
import org.jetbrains.kotlin.utils.singletonOrEmptyList import org.jetbrains.kotlin.utils.singletonOrEmptyList
import java.lang.IllegalStateException
import java.util.* import java.util.*
class ShortenReferences(val options: (KtElement) -> Options = { Options.DEFAULT }) { class ShortenReferences(val options: (KtElement) -> Options = { Options.DEFAULT }) {
@@ -130,7 +130,7 @@ class ShortenReferences(val options: (KtElement) -> Options = { Options.DEFAULT
} }
@JvmOverloads fun process(elements: Iterable<KtElement>, elementFilter: (PsiElement) -> FilterResult = { FilterResult.PROCESS }): Collection<KtElement> { @JvmOverloads fun process(elements: Iterable<KtElement>, elementFilter: (PsiElement) -> FilterResult = { FilterResult.PROCESS }): Collection<KtElement> {
return elements.groupBy { element -> element.getContainingKtFile() } return elements.groupBy(KtElement::getContainingKtFile)
.flatMap { shortenReferencesInFile(it.key, it.value, elementFilter) } .flatMap { shortenReferencesInFile(it.key, it.value, elementFilter) }
} }
@@ -155,9 +155,25 @@ class ShortenReferences(val options: (KtElement) -> Options = { Options.DEFAULT
ShortenQualifiedExpressionsVisitor(file, elementFilter, failedToImportDescriptors), ShortenQualifiedExpressionsVisitor(file, elementFilter, failedToImportDescriptors),
RemoveExplicitCompanionObjectReferenceVisitor(file, elementFilter, failedToImportDescriptors) RemoveExplicitCompanionObjectReferenceVisitor(file, elementFilter, failedToImportDescriptors)
) )
val descriptorsToImport = visitors.flatMap { analyzeReferences(elementsToUse, it) }.toSet()
// step 1: collect qualified elements to analyze (no resolve at this step)
for (visitor in visitors) {
for (element in elementsToUse) {
visitor.options = options(element)
element.accept(visitor)
}
}
val elementsToAnalyze = visitors.flatMap { it.getElementsToAnalyze() }
// step 2: analyze collected elements with resolve and decide which can be shortened now and which need descriptors to be imported before shortening
val bindingContext = file.getResolutionFacade().analyze(elementsToAnalyze, BodyResolveMode.PARTIAL)
visitors.forEach { it.analyzeCollectedElements(bindingContext) }
// step 3: shorten elements that can be shortened right now
visitors.forEach { it.shortenElements(elementsToUse) } visitors.forEach { it.shortenElements(elementsToUse) }
// step 4: try to import descriptors needed to shorten other elements
val descriptorsToImport = visitors.flatMap { it.getDescriptorsToImport() }.toSet()
var anyChange = false var anyChange = false
for (descriptor in descriptorsToImport) { for (descriptor in descriptorsToImport) {
assert(descriptor !in failedToImportDescriptors) assert(descriptor !in failedToImportDescriptors)
@@ -178,57 +194,103 @@ class ShortenReferences(val options: (KtElement) -> Options = { Options.DEFAULT
private fun dropNestedElements(elements: List<KtElement>): LinkedHashSet<KtElement> { private fun dropNestedElements(elements: List<KtElement>): LinkedHashSet<KtElement> {
val elementSet = elements.toSet() val elementSet = elements.toSet()
val newElements = LinkedHashSet<KtElement>(elementSet.size) return elementSet.filterTo(LinkedHashSet<KtElement>(elementSet.size)) { element ->
for (element in elementSet) { element.parents.none { it in elementSet }
if (!element.parents.any { it in elementSet }) {
newElements.add(element)
}
} }
return newElements
} }
private fun analyzeReferences(elements: Iterable<KtElement>, visitor: ShorteningVisitor<*>): Set<DeclarationDescriptor> { private abstract class ShorteningVisitor<TElement : KtElement>(
for (element in elements) {
visitor.options = options(element)
element.accept(visitor)
}
return visitor.getDescriptorsToImport()
}
private abstract class ShorteningVisitor<in T : KtElement>(
protected val file: KtFile, protected val file: KtFile,
protected val elementFilter: (PsiElement) -> FilterResult, protected val elementFilter: (PsiElement) -> FilterResult,
protected val failedToImportDescriptors: Set<DeclarationDescriptor> protected val failedToImportDescriptors: Set<DeclarationDescriptor>
) : KtVisitorVoid() { ) : KtVisitorVoid() {
var options: Options = Options.DEFAULT var options: Options = Options.DEFAULT
private val elementsToShorten = ArrayList<T>() private data class ElementToAnalyze<TElement>(val element: TElement, val level: Int)
private val elementsToAnalyze = ArrayList<ElementToAnalyze<TElement>>()
private val elementsToShorten = ArrayList<TElement>()
private val descriptorsToImport = LinkedHashSet<DeclarationDescriptor>() private val descriptorsToImport = LinkedHashSet<DeclarationDescriptor>()
protected val resolutionFacade = file.getResolutionFacade() protected val resolutionFacade = file.getResolutionFacade()
protected fun analyze(element: KtElement) private var level = 0
= resolutionFacade.analyze(element, BodyResolveMode.PARTIAL)
protected fun processQualifiedElement(element: T, targets: Collection<DeclarationDescriptor>, canShortenNow: Boolean) { protected fun nextLevel() {
if (canShortenNow) { level++
addElementToShorten(element) }
}
else if (targets.isNotEmpty() && targets.none { it in failedToImportDescriptors } && targets.all { mayImport(it, file) }) { protected fun prevLevel() {
descriptorsToImport.addAll(targets) level--
} assert(level >= 0)
else { }
qualifier(element).accept(this)
/**
* Should be invoked by implementors when visiting the PSI tree for those elements that can potentially be shortened
*/
protected fun addQualifiedElementToAnalyze(element: TElement) {
elementsToAnalyze.add(ElementToAnalyze(element, level))
}
fun analyzeCollectedElements(bindingContext: BindingContext) {
var index = 0
while (index < elementsToAnalyze.size) {
val (element, level) = elementsToAnalyze[index++]
val result = analyzeQualifiedElement(element, bindingContext)
val toBeShortened: Boolean
when (result) {
is ShortenNow -> {
elementsToShorten.add(element)
toBeShortened = true
}
is ImportDescriptors -> {
val tryImport = result.descriptors.isNotEmpty()
&& result.descriptors.none { it in failedToImportDescriptors }
&& result.descriptors.all { mayImport(it, file) }
if (tryImport) {
descriptorsToImport.addAll(result.descriptors)
toBeShortened = true
}
else {
toBeShortened = false
}
}
is Skip -> {
toBeShortened = false
}
}
if (toBeShortened) {
// we are going to shorten qualified element - we must skip all elements inside its qualifier
while (index < elementsToAnalyze.size && elementsToAnalyze[index].level > level) {
index++
}
}
} }
} }
protected fun addElementToShorten(element: T) { /**
elementsToShorten.add(element) * This method is invoked for all qualified elements added by [addQualifiedElementToAnalyze]
*/
protected abstract fun analyzeQualifiedElement(element: TElement, bindingContext: BindingContext): AnalyzeQualifiedElementResult
protected sealed class AnalyzeQualifiedElementResult {
object Skip : AnalyzeQualifiedElementResult()
object ShortenNow : AnalyzeQualifiedElementResult()
class ImportDescriptors(val descriptors: Collection<DeclarationDescriptor>) : AnalyzeQualifiedElementResult()
} }
protected abstract fun qualifier(element: T): KtElement typealias Skip = AnalyzeQualifiedElementResult.Skip
typealias ShortenNow = AnalyzeQualifiedElementResult.ShortenNow
typealias ImportDescriptors = AnalyzeQualifiedElementResult.ImportDescriptors
protected abstract fun shortenElement(element: T): KtElement protected abstract fun shortenElement(element: TElement): KtElement
override fun visitElement(element: PsiElement) { override fun visitElement(element: PsiElement) {
if (elementFilter(element) != FilterResult.SKIP) { if (elementFilter(element) != FilterResult.SKIP) {
@@ -253,6 +315,7 @@ class ShortenReferences(val options: (KtElement) -> Options = { Options.DEFAULT
} }
} }
fun getElementsToAnalyze(): Collection<TElement> = elementsToAnalyze.map { it.element }
fun getDescriptorsToImport(): Set<DeclarationDescriptor> = descriptorsToImport fun getDescriptorsToImport(): Set<DeclarationDescriptor> = descriptorsToImport
} }
@@ -268,33 +331,32 @@ class ShortenReferences(val options: (KtElement) -> Options = { Options.DEFAULT
userType.typeArgumentList?.accept(this) userType.typeArgumentList?.accept(this)
if (filterResult == FilterResult.PROCESS) { if (filterResult == FilterResult.PROCESS) {
processType(userType) addQualifiedElementToAnalyze(userType)
}
else {
userType.qualifier?.accept(this)
} }
// elements in qualifier must be under
nextLevel()
userType.qualifier?.accept(this)
prevLevel()
} }
private fun processType(type: KtUserType) { override fun analyzeQualifiedElement(element: KtUserType, bindingContext: BindingContext): AnalyzeQualifiedElementResult {
if (type.qualifier == null) return if (element.qualifier == null) return Skip
val referenceExpression = type.referenceExpression ?: return val referenceExpression = element.referenceExpression ?: return Skip
val bindingContext = analyze(referenceExpression) val target = referenceExpression.targets(bindingContext).singleOrNull() ?: return Skip
val target = referenceExpression.targets(bindingContext).singleOrNull() ?: return
val scope = type.getResolutionScope(bindingContext, resolutionFacade) val scope = element.getResolutionScope(bindingContext, resolutionFacade)
val name = target.name val name = target.name
val targetByName = if (target is ClassifierDescriptor) val targetByName = if (target is ClassifierDescriptor)
scope.findClassifier(name, NoLookupLocation.FROM_IDE) scope.findClassifier(name, NoLookupLocation.FROM_IDE)
else else
scope.findPackage(name) scope.findPackage(name)
val canShortenNow = targetByName?.asString() == target.asString() val canShortenNow = targetByName?.asString() == target.asString()
return if (canShortenNow) ShortenNow else ImportDescriptors(target.singletonOrEmptyList())
processQualifiedElement(type, target.singletonOrEmptyList(), canShortenNow)
} }
override fun qualifier(element: KtUserType) = element.qualifier!!
override fun shortenElement(element: KtUserType): KtElement { override fun shortenElement(element: KtUserType): KtElement {
element.deleteQualifier() element.deleteQualifier()
return element return element
@@ -306,6 +368,7 @@ class ShortenReferences(val options: (KtElement) -> Options = { Options.DEFAULT
elementFilter: (PsiElement) -> FilterResult, elementFilter: (PsiElement) -> FilterResult,
failedToImportDescriptors: Set<DeclarationDescriptor> failedToImportDescriptors: Set<DeclarationDescriptor>
) : ShorteningVisitor<KtDotQualifiedExpression>(file, elementFilter, failedToImportDescriptors) { ) : ShorteningVisitor<KtDotQualifiedExpression>(file, elementFilter, failedToImportDescriptors) {
override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) { override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) {
val filterResult = elementFilter(expression) val filterResult = elementFilter(expression)
if (filterResult == FilterResult.SKIP) return if (filterResult == FilterResult.SKIP) return
@@ -313,15 +376,14 @@ class ShortenReferences(val options: (KtElement) -> Options = { Options.DEFAULT
expression.selectorExpression?.acceptChildren(this) expression.selectorExpression?.acceptChildren(this)
if (filterResult == FilterResult.PROCESS) { if (filterResult == FilterResult.PROCESS) {
if (process(expression)) return addQualifiedElementToAnalyze(expression)
} }
// elements in receiver must be under
nextLevel()
expression.receiverExpression.accept(this) expression.receiverExpression.accept(this)
prevLevel()
} }
abstract fun process(qualifiedExpression: KtDotQualifiedExpression): Boolean
override fun qualifier(element: KtDotQualifiedExpression) = element.receiverExpression
} }
private class ShortenQualifiedExpressionsVisitor( private class ShortenQualifiedExpressionsVisitor(
@@ -330,56 +392,55 @@ class ShortenReferences(val options: (KtElement) -> Options = { Options.DEFAULT
failedToImportDescriptors: Set<DeclarationDescriptor> failedToImportDescriptors: Set<DeclarationDescriptor>
) : QualifiedExpressionShorteningVisitor(file, elementFilter, failedToImportDescriptors) { ) : QualifiedExpressionShorteningVisitor(file, elementFilter, failedToImportDescriptors) {
override fun process(qualifiedExpression: KtDotQualifiedExpression): Boolean { override fun analyzeQualifiedElement(element: KtDotQualifiedExpression, bindingContext: BindingContext): AnalyzeQualifiedElementResult {
val bindingContext = analyze(qualifiedExpression) val receiver = element.receiverExpression
val receiver = qualifiedExpression.receiverExpression
when (receiver) { when (receiver) {
is KtThisExpression -> { is KtThisExpression -> {
if (!options.removeThis) return false if (!options.removeThis) return Skip
} }
else -> { else -> {
if (bindingContext[BindingContext.QUALIFIER, receiver] == null) return false if (bindingContext[BindingContext.QUALIFIER, receiver] == null) return Skip
} }
} }
if (PsiTreeUtil.getParentOfType( if (PsiTreeUtil.getParentOfType(
qualifiedExpression, element,
KtImportDirective::class.java, KtPackageDirective::class.java) != null) return true KtImportDirective::class.java, KtPackageDirective::class.java) != null) return Skip
val selector = qualifiedExpression.selectorExpression ?: return false val selector = element.selectorExpression ?: return Skip
val callee = selector.getCalleeExpressionIfAny() as? KtReferenceExpression ?: return false val callee = selector.getCalleeExpressionIfAny() as? KtReferenceExpression ?: return Skip
val targets = callee.targets(bindingContext) val targets = callee.targets(bindingContext)
if (targets.isEmpty()) return false if (targets.isEmpty()) return Skip
val scope = qualifiedExpression.getResolutionScope(bindingContext, resolutionFacade) val scope = element.getResolutionScope(bindingContext, resolutionFacade)
val selectorCopy = selector.copy() as KtReferenceExpression val selectorCopy = selector.copy() as KtReferenceExpression
val newContext = selectorCopy.analyzeInContext(scope, selector) val newContext = selectorCopy.analyzeInContext(scope, selector)
val targetsWhenShort = (selectorCopy.getCalleeExpressionIfAny() as KtReferenceExpression).targets(newContext) val targetsWhenShort = (selectorCopy.getCalleeExpressionIfAny() as KtReferenceExpression).targets(newContext)
val targetsMatch = targetsMatch(targets, targetsWhenShort) val targetsMatch = targetsMatch(targets, targetsWhenShort)
if (receiver is KtThisExpression) { if (receiver is KtThisExpression) {
if (!targetsMatch) return false if (!targetsMatch) return Skip
val originalCall = selector.getResolvedCall(bindingContext) ?: return false val originalCall = selector.getResolvedCall(bindingContext) ?: return Skip
val newCall = selectorCopy.getResolvedCall(newContext) ?: return false val newCall = selectorCopy.getResolvedCall(newContext) ?: return Skip
val receiverKind = originalCall.explicitReceiverKind val receiverKind = originalCall.explicitReceiverKind
val newReceiver = when (receiverKind) { val newReceiver = when (receiverKind) {
ExplicitReceiverKind.BOTH_RECEIVERS, ExplicitReceiverKind.EXTENSION_RECEIVER -> newCall.extensionReceiver ExplicitReceiverKind.BOTH_RECEIVERS, ExplicitReceiverKind.EXTENSION_RECEIVER -> newCall.extensionReceiver
ExplicitReceiverKind.DISPATCH_RECEIVER -> newCall.dispatchReceiver ExplicitReceiverKind.DISPATCH_RECEIVER -> newCall.dispatchReceiver
else -> return false else -> return Skip
} as? ImplicitReceiver ?: return false } as? ImplicitReceiver ?: return Skip
val thisTarget = receiver.instanceReference.targets(bindingContext).singleOrNull() val thisTarget = receiver.instanceReference.targets(bindingContext).singleOrNull()
if (newReceiver.declarationDescriptor.asString() != thisTarget?.asString()) return false if (newReceiver.declarationDescriptor.asString() != thisTarget?.asString()) return Skip
} }
if (!targetsMatch && targetsWhenShort.any { it !is ClassDescriptor && it !is PackageViewDescriptor }) { return when {
targetsMatch -> ShortenNow
// it makes no sense to insert import when there is a conflict with function, property etc // it makes no sense to insert import when there is a conflict with function, property etc
return false targetsWhenShort.any { it !is ClassDescriptor && it !is PackageViewDescriptor } -> Skip
}
processQualifiedElement(qualifiedExpression, targets, targetsMatch) else -> ImportDescriptors(targets)
return true }
} }
private fun targetsMatch(targets1: Collection<DeclarationDescriptor>, targets2: Collection<DeclarationDescriptor>): Boolean { private fun targetsMatch(targets1: Collection<DeclarationDescriptor>, targets2: Collection<DeclarationDescriptor>): Boolean {
@@ -404,29 +465,22 @@ class ShortenReferences(val options: (KtElement) -> Options = { Options.DEFAULT
) : ShorteningVisitor<KtThisExpression>(file, elementFilter, failedToImportDescriptors) { ) : ShorteningVisitor<KtThisExpression>(file, elementFilter, failedToImportDescriptors) {
private val simpleThis = KtPsiFactory(file).createExpression("this") as KtThisExpression private val simpleThis = KtPsiFactory(file).createExpression("this") as KtThisExpression
private fun process(thisExpression: KtThisExpression) { override fun analyzeQualifiedElement(element: KtThisExpression, bindingContext: BindingContext): AnalyzeQualifiedElementResult {
if (!options.removeThisLabels || thisExpression.getTargetLabel() == null) return if (!options.removeThisLabels || element.getTargetLabel() == null) return Skip
val bindingContext = analyze(thisExpression) val targetBefore = element.instanceReference.targets(bindingContext).singleOrNull() ?: return Skip
val scope = element.getResolutionScope(bindingContext, resolutionFacade)
val targetBefore = thisExpression.instanceReference.targets(bindingContext).singleOrNull() ?: return val newContext = simpleThis.analyzeInContext(scope, element)
val scope = thisExpression.getResolutionScope(bindingContext, resolutionFacade)
val newContext = simpleThis.analyzeInContext(scope, thisExpression)
val targetAfter = simpleThis.instanceReference.targets(newContext).singleOrNull() val targetAfter = simpleThis.instanceReference.targets(newContext).singleOrNull()
if (targetBefore == targetAfter) { return if (targetBefore == targetAfter) ShortenNow else Skip
addElementToShorten(thisExpression)
}
} }
override fun visitThisExpression(expression: KtThisExpression) { override fun visitThisExpression(expression: KtThisExpression) {
if (elementFilter(expression) == FilterResult.PROCESS) { if (elementFilter(expression) == FilterResult.PROCESS) {
process(expression) addQualifiedElementToAnalyze(expression)
} }
} }
override fun qualifier(element: KtThisExpression): KtElement =
throw AssertionError("Qualifier requested: ${element.getElementTextWithContext()}")
override fun shortenElement(element: KtThisExpression): KtElement { override fun shortenElement(element: KtThisExpression): KtElement {
return element.replace(simpleThis) as KtElement return element.replace(simpleThis) as KtElement
} }
@@ -442,39 +496,33 @@ class ShortenReferences(val options: (KtElement) -> Options = { Options.DEFAULT
return (getCalleeExpressionIfAny() as? KtReferenceExpression)?.targets(context)?.singleOrNull() return (getCalleeExpressionIfAny() as? KtReferenceExpression)?.targets(context)?.singleOrNull()
} }
override fun process(qualifiedExpression: KtDotQualifiedExpression): Boolean { override fun analyzeQualifiedElement(element: KtDotQualifiedExpression, bindingContext: BindingContext): AnalyzeQualifiedElementResult {
val bindingContext = analyze(qualifiedExpression) val receiver = element.receiverExpression
val receiver = qualifiedExpression.receiverExpression
if (PsiTreeUtil.getParentOfType( if (PsiTreeUtil.getParentOfType(
qualifiedExpression, element,
KtImportDirective::class.java, KtPackageDirective::class.java) != null) return false KtImportDirective::class.java, KtPackageDirective::class.java) != null) return Skip
val receiverTarget = receiver.singleTarget(bindingContext) ?: return false val receiverTarget = receiver.singleTarget(bindingContext) ?: return Skip
if (receiverTarget !is ClassDescriptor) return false if (receiverTarget !is ClassDescriptor) return Skip
val selectorExpression = qualifiedExpression.selectorExpression ?: return false val selectorExpression = element.selectorExpression ?: return Skip
val selectorTarget = selectorExpression.singleTarget(bindingContext) ?: return false val selectorTarget = selectorExpression.singleTarget(bindingContext) ?: return Skip
if (receiverTarget.companionObjectDescriptor != selectorTarget) return false if (receiverTarget.companionObjectDescriptor != selectorTarget) return Skip
val selectorsSelector = (qualifiedExpression.parent as? KtDotQualifiedExpression)?.selectorExpression val selectorsSelector = (element.parent as? KtDotQualifiedExpression)?.selectorExpression
if (selectorsSelector == null) { ?: return ShortenNow
addElementToShorten(qualifiedExpression)
return true
}
val selectorsSelectorTarget = selectorsSelector.singleTarget(bindingContext) ?: return false val selectorsSelectorTarget = selectorsSelector.singleTarget(bindingContext) ?: return Skip
if (selectorsSelectorTarget is ClassDescriptor) return false if (selectorsSelectorTarget is ClassDescriptor) return Skip
// TODO: More generic solution may be possible // TODO: More generic solution may be possible
if (selectorsSelectorTarget is PropertyDescriptor) { if (selectorsSelectorTarget is PropertyDescriptor) {
val source = selectorsSelectorTarget.source.getPsi() as? KtProperty val source = selectorsSelectorTarget.source.getPsi() as? KtProperty
if (source != null && isEnumCompanionPropertyWithEntryConflict(source, source.name ?: "")) return false if (source != null && isEnumCompanionPropertyWithEntryConflict(source, source.name ?: "")) return Skip
} }
addElementToShorten(qualifiedExpression) return ShortenNow
return true
} }
override fun shortenElement(element: KtDotQualifiedExpression): KtElement { override fun shortenElement(element: KtDotQualifiedExpression): KtElement {