Refactoring

This commit is contained in:
Valentin Kipyatkov
2016-10-19 22:06:07 +03:00
parent f2b32f6d90
commit 7324ae145e
7 changed files with 156 additions and 141 deletions
@@ -1,5 +1,5 @@
/* /*
* Copyright 2010-2015 JetBrains s.r.o. * Copyright 2010-2016 JetBrains s.r.o.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -14,11 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
package org.jetbrains.kotlin.idea.quickfix.replaceWith package org.jetbrains.kotlin.idea.replacement
import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.intentions.OperatorToFunctionIntention import org.jetbrains.kotlin.idea.intentions.OperatorToFunctionIntention
import org.jetbrains.kotlin.idea.replacement.*
import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
@@ -1,5 +1,5 @@
/* /*
* Copyright 2010-2015 JetBrains s.r.o. * Copyright 2010-2016 JetBrains s.r.o.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -14,12 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package org.jetbrains.kotlin.idea.quickfix.replaceWith package org.jetbrains.kotlin.idea.replacement
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.idea.core.ShortenReferences import org.jetbrains.kotlin.idea.core.ShortenReferences
import org.jetbrains.kotlin.idea.core.replaced import org.jetbrains.kotlin.idea.core.replaced
import org.jetbrains.kotlin.idea.replacement.ReplacementExpression
import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getQualifiedExpressionForSelectorOrThis import org.jetbrains.kotlin.psi.psiUtil.getQualifiedExpressionForSelectorOrThis
import org.jetbrains.kotlin.utils.addToStdlib.check import org.jetbrains.kotlin.utils.addToStdlib.check
@@ -0,0 +1,97 @@
/*
* 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.replacement
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.util.ui.UIUtil
import org.jetbrains.kotlin.idea.core.targetDescriptors
import org.jetbrains.kotlin.idea.references.KtSimpleNameReference
import org.jetbrains.kotlin.idea.stubindex.KotlinSourceFilterScope
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtImportDirective
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
interface UsageReplacementStrategy {
fun createReplacer(usage: KtSimpleNameExpression): (() -> KtElement)?
}
private val LOG = Logger.getInstance(UsageReplacementStrategy::class.java)
fun UsageReplacementStrategy.replaceUsagesInWholeProject(
targetPsiElement: PsiElement,
progressTitle: String,
commandName: String
) {
val project = targetPsiElement.project
ProgressManager.getInstance().run(
object : Task.Modal(project, progressTitle, true) {
override fun run(indicator: ProgressIndicator) {
val usages = runReadAction {
val searchScope = KotlinSourceFilterScope.projectSources(GlobalSearchScope.projectScope(project), project)
ReferencesSearch.search(targetPsiElement, searchScope)
.filterIsInstance<KtSimpleNameReference>()
.map { ref -> ref.expression }
}
this@replaceUsagesInWholeProject.replaceUsages(usages, project, commandName)
}
})
}
private fun UsageReplacementStrategy.replaceUsages(
usages: Collection<KtSimpleNameExpression>,
project: Project,
commandName: String
) {
UIUtil.invokeLaterIfNeeded {
project.executeWriteCommand(commandName) {
// we should delete imports later to not affect other usages
val importsToDelete = arrayListOf<KtImportDirective>()
for (usage in usages) {
try {
if (!usage.isValid) continue // TODO: nested calls
//TODO: keep the import if we don't know how to replace some of the usages
val importDirective = usage.getStrictParentOfType<KtImportDirective>()
if (importDirective != null) {
if (!importDirective.isAllUnder && importDirective.targetDescriptors().size == 1) {
importsToDelete.add(importDirective)
}
continue
}
createReplacer(usage)?.invoke()
}
catch (e: Throwable) {
LOG.error(e)
}
}
importsToDelete.forEach { it.delete() }
}
}
}
@@ -25,6 +25,7 @@ import org.jetbrains.kotlin.idea.core.targetDescriptors
import org.jetbrains.kotlin.idea.quickfix.CleanupFix import org.jetbrains.kotlin.idea.quickfix.CleanupFix
import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory
import org.jetbrains.kotlin.idea.core.moveCaret import org.jetbrains.kotlin.idea.core.moveCaret
import org.jetbrains.kotlin.idea.replacement.UsageReplacementStrategy
import org.jetbrains.kotlin.psi.KtImportDirective import org.jetbrains.kotlin.psi.KtImportDirective
import org.jetbrains.kotlin.psi.KtSimpleNameExpression import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.resolve.calls.callUtil.getCalleeExpressionIfAny import org.jetbrains.kotlin.resolve.calls.callUtil.getCalleeExpressionIfAny
@@ -20,21 +20,31 @@ import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile import com.intellij.psi.PsiFile
import org.jetbrains.kotlin.descriptors.CallableDescriptor import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ConstructorDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor
import org.jetbrains.kotlin.diagnostics.Diagnostic import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory import org.jetbrains.kotlin.diagnostics.DiagnosticFactory
import org.jetbrains.kotlin.diagnostics.Errors import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.core.OptionalParametersHelper import org.jetbrains.kotlin.idea.core.OptionalParametersHelper
import org.jetbrains.kotlin.idea.quickfix.KotlinQuickFixAction import org.jetbrains.kotlin.idea.quickfix.KotlinQuickFixAction
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.idea.replacement.CallableUsageReplacementStrategy
import org.jetbrains.kotlin.idea.replacement.ClassUsageReplacementStrategy
import org.jetbrains.kotlin.idea.replacement.UsageReplacementStrategy
import org.jetbrains.kotlin.psi.KtConstructorCalleeExpression import org.jetbrains.kotlin.psi.KtConstructorCalleeExpression
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtSimpleNameExpression import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.resolve.DescriptorUtils import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.annotations.argumentValue import org.jetbrains.kotlin.resolve.annotations.argumentValue
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.model.isReallySuccess
import org.jetbrains.kotlin.resolve.constants.StringValue import org.jetbrains.kotlin.resolve.constants.StringValue
import org.jetbrains.kotlin.resolve.descriptorUtil.builtIns import org.jetbrains.kotlin.resolve.descriptorUtil.builtIns
import org.jetbrains.kotlin.resolve.descriptorUtil.hasDefaultValue import org.jetbrains.kotlin.resolve.descriptorUtil.hasDefaultValue
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
//TODO: different replacements for property accessors //TODO: different replacements for property accessors
@@ -46,13 +56,13 @@ abstract class DeprecatedSymbolUsageFixBase(
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile): Boolean { override fun isAvailable(project: Project, editor: Editor?, file: PsiFile): Boolean {
val element = element ?: return false val element = element ?: return false
if (!super.isAvailable(project, editor, file)) return false if (!super.isAvailable(project, editor, file)) return false
val strategy = UsageReplacementStrategy.build(element, replaceWith, recheckAnnotation = true) val strategy = buildUsageReplacementStrategy(element, replaceWith, recheckAnnotation = true)
return strategy != null && strategy.createReplacer(element) != null return strategy != null && strategy.createReplacer(element) != null
} }
final override fun invoke(project: Project, editor: Editor?, file: KtFile) { final override fun invoke(project: Project, editor: Editor?, file: KtFile) {
val element = element ?: return val element = element ?: return
val strategy = UsageReplacementStrategy.build(element, replaceWith, recheckAnnotation = false)!! val strategy = buildUsageReplacementStrategy(element, replaceWith, recheckAnnotation = false)!!
invoke(strategy, project, editor) invoke(strategy, project, editor)
} }
@@ -106,5 +116,43 @@ abstract class DeprecatedSymbolUsageFixBase(
val replacement = DeprecatedSymbolUsageFixBase.fetchReplaceWithPattern(descriptor, nameExpression.project) ?: return null val replacement = DeprecatedSymbolUsageFixBase.fetchReplaceWithPattern(descriptor, nameExpression.project) ?: return null
return Data(nameExpression, replacement, descriptor) return Data(nameExpression, replacement, descriptor)
} }
private fun buildUsageReplacementStrategy(element: KtSimpleNameExpression, replaceWith: ReplaceWith, recheckAnnotation: Boolean): UsageReplacementStrategy? {
val resolutionFacade = element.getResolutionFacade()
val bindingContext = resolutionFacade.analyze(element, BodyResolveMode.PARTIAL)
var target = element.mainReference.resolveToDescriptors(bindingContext).singleOrNull() ?: return null
var replacePatternFromSymbol = DeprecatedSymbolUsageFixBase.fetchReplaceWithPattern(target, resolutionFacade.project)
if (replacePatternFromSymbol == null && target is ConstructorDescriptor) {
target = target.containingDeclaration
replacePatternFromSymbol = DeprecatedSymbolUsageFixBase.fetchReplaceWithPattern(target, resolutionFacade.project)
}
// check that ReplaceWith hasn't changed
if (recheckAnnotation && replacePatternFromSymbol != replaceWith) return null
when (target) {
is CallableDescriptor -> {
val resolvedCall = element.getResolvedCall(bindingContext) ?: return null
if (!resolvedCall.isReallySuccess()) return null
val replacement = ReplaceWithAnnotationAnalyzer.analyzeCallableReplacement(replaceWith, target, resolutionFacade) ?: return null
return CallableUsageReplacementStrategy(replacement)
}
is ClassDescriptor -> {
val replacementType = ReplaceWithAnnotationAnalyzer.analyzeClassReplacement(replaceWith, target, resolutionFacade)
if (replacementType != null) { //TODO: check that it's really resolved and is not an object otherwise it can be expression as well
return ClassUsageReplacementStrategy(replacementType, null, element.project)
}
else {
val constructor = target.unsubstitutedPrimaryConstructor ?: return null
val replacementExpression = ReplaceWithAnnotationAnalyzer.analyzeCallableReplacement(replaceWith, constructor, resolutionFacade) ?: return null
return ClassUsageReplacementStrategy(null, replacementExpression, element.project)
}
}
else -> return null
}
}
} }
} }
@@ -17,28 +17,17 @@
package org.jetbrains.kotlin.idea.quickfix.replaceWith package org.jetbrains.kotlin.idea.quickfix.replaceWith
import com.intellij.codeInsight.intention.IntentionAction import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile import com.intellij.psi.PsiFile
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.util.ui.UIUtil
import org.jetbrains.kotlin.diagnostics.Diagnostic import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.idea.core.targetDescriptors
import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory
import org.jetbrains.kotlin.idea.references.KtSimpleNameReference
import org.jetbrains.kotlin.idea.references.mainReference import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.idea.stubindex.KotlinSourceFilterScope import org.jetbrains.kotlin.idea.replacement.UsageReplacementStrategy
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand import org.jetbrains.kotlin.idea.replacement.replaceUsagesInWholeProject
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.renderer.DescriptorRenderer
import org.jetbrains.kotlin.renderer.ClassifierNamePolicy import org.jetbrains.kotlin.renderer.ClassifierNamePolicy
import org.jetbrains.kotlin.renderer.DescriptorRenderer
import org.jetbrains.kotlin.renderer.ParameterNameRenderingPolicy import org.jetbrains.kotlin.renderer.ParameterNameRenderingPolicy
class DeprecatedSymbolUsageInWholeProjectFix( class DeprecatedSymbolUsageInWholeProjectFix(
@@ -47,8 +36,6 @@ class DeprecatedSymbolUsageInWholeProjectFix(
private val text: String private val text: String
) : DeprecatedSymbolUsageFixBase(element, replaceWith) { ) : DeprecatedSymbolUsageFixBase(element, replaceWith) {
private val LOG = Logger.getInstance(DeprecatedSymbolUsageInWholeProjectFix::class.java)
override fun getFamilyName() = "Replace deprecated symbol usage in whole project" override fun getFamilyName() = "Replace deprecated symbol usage in whole project"
override fun getText() = text override fun getText() = text
@@ -62,19 +49,7 @@ class DeprecatedSymbolUsageInWholeProjectFix(
override fun invoke(replacementStrategy: UsageReplacementStrategy, project: Project, editor: Editor?) { override fun invoke(replacementStrategy: UsageReplacementStrategy, project: Project, editor: Editor?) {
val psiElement = targetPsiElement()!! val psiElement = targetPsiElement()!!
replacementStrategy.replaceUsagesInWholeProject(psiElement, progressTitle = "Applying '$text'", commandName = text)
ProgressManager.getInstance().run(
object : Task.Modal(project, "Applying '$text'", true) {
override fun run(indicator: ProgressIndicator) {
val usages = runReadAction {
val searchScope = KotlinSourceFilterScope.projectSources(GlobalSearchScope.projectScope(project), project)
ReferencesSearch.search(psiElement, searchScope)
.filterIsInstance<KtSimpleNameReference>()
.map { ref -> ref.expression }
}
replaceUsages(project, usages, replacementStrategy)
}
})
} }
private fun targetPsiElement(): KtDeclaration? { private fun targetPsiElement(): KtDeclaration? {
@@ -87,37 +62,6 @@ class DeprecatedSymbolUsageInWholeProjectFix(
} }
} }
private fun replaceUsages(project: Project, usages: Collection<KtSimpleNameExpression>, replacementStrategy: UsageReplacementStrategy) {
UIUtil.invokeLaterIfNeeded {
project.executeWriteCommand(text) {
// we should delete imports later to not affect other usages
val importsToDelete = arrayListOf<KtImportDirective>()
for (usage in usages) {
try {
if (!usage.isValid) continue // TODO: nested calls
//TODO: keep the import if we don't know how to replace some of the usages
val importDirective = usage.getStrictParentOfType<KtImportDirective>()
if (importDirective != null) {
if (!importDirective.isAllUnder && importDirective.targetDescriptors().size == 1) {
importsToDelete.add(importDirective)
}
continue
}
replacementStrategy.createReplacer(usage)?.invoke()
}
catch (e: Throwable) {
LOG.error(e)
}
}
importsToDelete.forEach { it.delete() }
}
}
}
companion object : KotlinSingleIntentionActionFactory() { companion object : KotlinSingleIntentionActionFactory() {
//TODO: better rendering needed //TODO: better rendering needed
private val RENDERER = DescriptorRenderer.withOptions { private val RENDERER = DescriptorRenderer.withOptions {
@@ -136,6 +80,5 @@ class DeprecatedSymbolUsageInWholeProjectFix(
val descriptorName = RENDERER.render(descriptor) val descriptorName = RENDERER.render(descriptor)
return DeprecatedSymbolUsageInWholeProjectFix(nameExpression, replacement, "Replace usages of '$descriptorName' in whole project") return DeprecatedSymbolUsageInWholeProjectFix(nameExpression, replacement, "Replace usages of '$descriptorName' in whole project")
} }
} }
} }
@@ -1,72 +0,0 @@
/*
* Copyright 2010-2015 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.quickfix.replaceWith
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ConstructorDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.model.isReallySuccess
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
interface UsageReplacementStrategy {
fun createReplacer(usage: KtSimpleNameExpression): (() -> KtElement)?
companion object {
fun build(element: KtSimpleNameExpression, replaceWith: ReplaceWith, recheckAnnotation: Boolean): UsageReplacementStrategy? {
val resolutionFacade = element.getResolutionFacade()
val bindingContext = resolutionFacade.analyze(element, BodyResolveMode.PARTIAL)
var target = element.mainReference.resolveToDescriptors(bindingContext).singleOrNull() ?: return null
var replacePatternFromSymbol = DeprecatedSymbolUsageFixBase.fetchReplaceWithPattern(target, resolutionFacade.project)
if (replacePatternFromSymbol == null && target is ConstructorDescriptor) {
target = target.containingDeclaration
replacePatternFromSymbol = DeprecatedSymbolUsageFixBase.fetchReplaceWithPattern(target, resolutionFacade.project)
}
// check that ReplaceWith hasn't changed
if (recheckAnnotation && replacePatternFromSymbol != replaceWith) return null
when (target) {
is CallableDescriptor -> {
val resolvedCall = element.getResolvedCall(bindingContext) ?: return null
if (!resolvedCall.isReallySuccess()) return null
val replacement = ReplaceWithAnnotationAnalyzer.analyzeCallableReplacement(replaceWith, target, resolutionFacade) ?: return null
return CallableUsageReplacementStrategy(replacement)
}
is ClassDescriptor -> {
val replacementType = ReplaceWithAnnotationAnalyzer.analyzeClassReplacement(replaceWith, target, resolutionFacade)
if (replacementType != null) { //TODO: check that it's really resolved and is not an object otherwise it can be expression as well
return ClassUsageReplacementStrategy(replacementType, null, element.project)
}
else {
val constructor = target.unsubstitutedPrimaryConstructor ?: return null
val replacementExpression = ReplaceWithAnnotationAnalyzer.analyzeCallableReplacement(replaceWith, constructor, resolutionFacade) ?: return null
return ClassUsageReplacementStrategy(null, replacementExpression, element.project)
}
}
else -> return null
}
}
}
}