Move: Support method moving

This commit is contained in:
aleksandrina-streltsova
2020-06-03 11:17:57 +03:00
committed by Igor Yakovlev
parent a5368e443c
commit 5515465760
34 changed files with 1050 additions and 0 deletions
@@ -829,6 +829,8 @@ label.text.receiver.type=Receiver &type:\u0020
label.text.target.file.name=Target file name:
label.text.to.file=To &file:
label.text.to.package=To p&ackage:
label.text.to.class=To class:
label.text.to.object=To object:
class.name.prompt=Class &name:
search.for.text.occurrences=Search for &text occurrences
search.in.comments.and.strings=Search in &comments and strings
@@ -952,6 +954,13 @@ text.move.declaration.supports.only.top.levels.and.nested.classes=Move declarati
text.move.declaration.proceed.move.without.mpp.counterparts.text=This refactoring will move selected declaration without it's expect/actual counterparts that may lead to compilation errors.\nDo you wish to proceed?
text.move.declaration.proceed.move.without.mpp.counterparts.title=MPP declarations does not supported by this refactoring.
text.move.declarations=Move declarations
text.no.destination.object.specified=No destination object specified
text.select.a.name.for.this.parameter=Select a name for ''this@{0}'' parameter
text.move.method.is.not.supported.for.non.project.methods=Move method is not supported for non-project methods
text.move.method.is.not.supported.for.generic.classes=Move method is not supported for generic classes
text.move.method.is.not.supported.when.method.is.a.part.of.inheritance.hierarchy=Move method is not supported when method is a part of inheritance hierarchy
text.references.to.outer.classes.have.to.be.added.manually=References to outer classes will have to be added manually after move
text.move.method=Move method
text.move.file.0=Move {0}
text.move.refactoring.not.available.during.indexing=Move refactoring is not available while indexing is in progress
text.moving.multiple.nested.classes.to.top.level.not.supported=Moving multiple nested classes to top-level is not supported
@@ -1020,6 +1029,8 @@ title.change.signature=Change Signature
title.inline.function=Inline Function
title.introduce.parameter.to.declaration=Introduce parameter to declaration
title.move.nested.classes.to.upper.level=Move Nested Classes to Upper Level
title.move.method=Move Method
title.choose.destination.object=Choose Destination Object
title.select.target.code.block=Select target code block
unsupported.usage.0=Unsupported usage: {0}
@@ -1925,6 +1936,7 @@ title.parameters=Parameters
signature.preview=Signature Preview
move.members.from=Move members from:
open.moved.members.in.editor=Open moved members in editor
open.moved.method.in.editor=Open moved method in editor
to.fully.qualified.name=To (fully qualified name):
toggle.library.to.source.dependency.support=Toggle library to source dependency support
enable.components.for.library.to.source.analysis.in.kotlin=Enable components for analyzing libraries depending on project source files in Kotlin
@@ -324,6 +324,10 @@
id="kotlinAwareJavaMoveClassesOrPackagesHandler"
implementation="org.jetbrains.kotlin.idea.refactoring.move.moveClassesOrPackages.KotlinAwareJavaMoveClassesOrPackagesHandler"
order="first" />
<refactoring.moveHandler
id="kotlin.moveMethod"
implementation="org.jetbrains.kotlin.idea.refactoring.move.moveMethod.MoveKotlinMethodHandler"
order="first,before kotlin.moveTopLevelDeclarations" />
<moveFileHandler implementation="org.jetbrains.kotlin.idea.refactoring.move.moveFilesOrDirectories.MoveKotlinFileHandler" />
<moveFileHandler
implementation="org.jetbrains.kotlin.idea.refactoring.move.moveFilesOrDirectories.FqNameFixingMoveJavaFileHandler"
@@ -0,0 +1,290 @@
package org.jetbrains.kotlin.idea.refactoring.move.moveMethod
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.ide.util.TreeClassChooser
import com.intellij.ide.util.TreeJavaClassChooserDialog
import com.intellij.openapi.ui.VerticalFlowLayout
import com.intellij.psi.PsiClass
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.refactoring.ui.RefactoringDialog
import com.intellij.ui.EditorTextField
import com.intellij.ui.ScrollPaneFactory
import com.intellij.ui.TitledSeparator
import com.intellij.ui.components.JBList
import com.intellij.util.ui.JBUI
import com.jetbrains.rd.util.firstOrNull
import org.jetbrains.kotlin.asJava.classes.KtLightClassForSourceDeclaration
import org.jetbrains.kotlin.asJava.toLightClass
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.KotlinIconProvider.Companion.getBaseIcon
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.completion.extraCompletionFilter
import org.jetbrains.kotlin.idea.core.KotlinNameSuggester.suggestNamesByType
import org.jetbrains.kotlin.idea.core.NewDeclarationNameValidator
import org.jetbrains.kotlin.idea.core.completion.DeclarationLookupObject
import org.jetbrains.kotlin.idea.core.completion.PackageLookupObject
import org.jetbrains.kotlin.idea.projectView.KtClassOrObjectTreeNode
import org.jetbrains.kotlin.idea.refactoring.canRefactor
import org.jetbrains.kotlin.idea.refactoring.ui.KotlinTypeReferenceEditorComboWithBrowseButton
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
import org.jetbrains.kotlin.name.Name.isValidIdentifier
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.renderer.ClassifierNamePolicy
import java.awt.Component
import java.awt.Dimension
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import java.awt.event.ActionListener
import javax.swing.*
import javax.swing.tree.DefaultMutableTreeNode
class MoveKotlinMethodDialog(
private val method: KtNamedFunction,
private val variableToClassMap: Map<KtNamedDeclaration, KtClass>,
private val targetContainer: KtClassOrObject?
) : RefactoringDialog(method.project, true) {
private val variables = variableToClassMap.keys.toList()
private val targetVariableList = createTargetVariableChooser()
private val thisClassesToMembers = getThisClassesToMembers(method)
private val oldClassParameterNameFields = LinkedHashMap<KtClass, EditorTextField>()
private val targetObjectChooser = createTargetObjectChooser()
private val toClassRadioButton = JRadioButton(KotlinBundle.message("label.text.to.class"))
private val toObjectRadioButton = JRadioButton(KotlinBundle.message("label.text.to.object"))
private var selectedTarget: KtNamedDeclaration? = null
private val openInEditorCheckBox = JCheckBox(KotlinBundle.message("open.moved.method.in.editor"), true)
init {
super.init()
title = KotlinBundle.message("title.move.method")
}
override fun doAction() {
if (toClassRadioButton.isSelected) selectedTarget = targetVariableList.selectedValue
if (toObjectRadioButton.isSelected && selectedTarget == null) {
setErrorText(KotlinBundle.message("text.no.destination.object.specified"))
return
}
val oldClassParameterNames = LinkedHashMap<KtClass, String>()
for ((ktClass, field) in oldClassParameterNameFields) {
if (!isValidIdentifier(field.text)) {
setErrorText(KotlinBundle.message("parameter.name.is.invalid", field.text))
return
}
if (field.isEnabled) {
oldClassParameterNames[ktClass] = field.text
}
}
val processor = MoveKotlinMethodProcessor(method, selectedTarget!!, oldClassParameterNames, openInEditorCheckBox.isSelected)
invokeRefactoring(processor)
}
override fun createCenterPanel(): JComponent? {
val mainPanel = JPanel(GridBagLayout())
val buttonGroup = ButtonGroup()
toClassRadioButton.addActionListener { enableTargetChooser() }
toObjectRadioButton.addActionListener { enableTargetChooser() }
buttonGroup.add(toClassRadioButton)
buttonGroup.add(toObjectRadioButton)
targetVariableList.addListSelectionListener { enableTextFields() }
val scrollPane = ScrollPaneFactory.createScrollPane(targetVariableList)
mainPanel.add(
scrollPane, GridBagConstraints(
1, 0, 1, 1, 1.0, 1.0,
GridBagConstraints.WEST, GridBagConstraints.BOTH, JBUI.emptyInsets(), 0, 0
)
)
mainPanel.add(
targetObjectChooser, GridBagConstraints(
1, GridBagConstraints.RELATIVE, 1, 1, 1.0, 1.0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, JBUI.emptyInsets(), 0, 0
)
)
mainPanel.add(
toClassRadioButton, GridBagConstraints(
0, GridBagConstraints.RELATIVE, 1, 1, 1.0, 1.0,
GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, JBUI.emptyInsets(), 0, 0
)
)
mainPanel.add(
toObjectRadioButton, GridBagConstraints(
0, GridBagConstraints.RELATIVE, 1, 1, 1.0, 1.0,
GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, JBUI.emptyInsets(), 0, 0
)
)
val parametersPanel: JPanel? = createParametersPanel()
if (parametersPanel != null) {
mainPanel.add(
parametersPanel, GridBagConstraints(
0, GridBagConstraints.RELATIVE, 2, 1, 1.0, 0.0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, JBUI.emptyInsets(), 0, 0
)
)
}
mainPanel.add(
openInEditorCheckBox, GridBagConstraints(
0, GridBagConstraints.RELATIVE, 2, 1, 0.0, 0.0,
GridBagConstraints.WEST, GridBagConstraints.NONE, JBUI.emptyInsets(), 0, 0
)
)
if (variables.isNotEmpty() && targetContainer !is KtObjectDeclaration) {
toClassRadioButton.isSelected = true
} else {
toObjectRadioButton.isSelected = true
if (variables.isEmpty()) {
toClassRadioButton.isEnabled = false
}
}
enableTextFields()
enableTargetChooser()
updateOnChanged(targetVariableList)
return mainPanel
}
private fun createTargetVariableChooser(): JList<KtNamedDeclaration> {
val listModel = object : AbstractListModel<KtNamedDeclaration>() {
override fun getElementAt(index: Int): KtNamedDeclaration = variables[index]
override fun getSize(): Int = variables.size
}
val list = JBList(listModel)
val listCellRenderer = object : DefaultListCellRenderer() {
private val renderer = IdeDescriptorRenderers.SOURCE_CODE_TYPES.withOptions {
classifierNamePolicy = ClassifierNamePolicy.SHORT
}
override fun getListCellRendererComponent(
list: JList<*>?, value: Any?, index: Int, isSelected: Boolean, cellHasFocus: Boolean
): Component {
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus)
if (value is KtNamedDeclaration) {
icon = value.getBaseIcon()
text = value.nameAsSafeName.identifier
(value.resolveToDescriptorIfAny() as? CallableDescriptor)?.returnType?.let { type ->
text = "$text: ${renderer.renderType(type)}"
}
}
return this
}
}
list.cellRenderer = listCellRenderer
list.selectionMode = ListSelectionModel.SINGLE_SELECTION
val defaultVariableIndex = variables.indexOf(variableToClassMap.filter { it.value == targetContainer }.firstOrNull()?.key)
list.selectedIndex = if (defaultVariableIndex != -1) defaultVariableIndex else 0
list.selectionModel.addListSelectionListener { updateOnChanged(list) }
return list
}
private fun createTargetObjectChooser(): KotlinTypeReferenceEditorComboWithBrowseButton {
val targetObjectChooser = KotlinTypeReferenceEditorComboWithBrowseButton(
ActionListener {
val chooser: TreeClassChooser = object : TreeJavaClassChooserDialog(
KotlinBundle.message("title.choose.destination.object"),
project,
GlobalSearchScope.projectScope(project),
{ psiClass ->
if (psiClass !is KtLightClassForSourceDeclaration) {
false
} else {
val ktClassOrObject = psiClass.kotlinOrigin
ktClassOrObject is KtObjectDeclaration && !ktClassOrObject.isObjectLiteral()
}
},
null,
null,
true
) {
override fun getSelectedFromTreeUserObject(node: DefaultMutableTreeNode): PsiClass? {
val psiClass = super.getSelectedFromTreeUserObject(node)
if (psiClass != null) return psiClass
val userObject = node.userObject
return if (userObject !is KtClassOrObjectTreeNode) null else userObject.value.toLightClass()
}
}
chooser.selectDirectory(method.containingFile.containingDirectory)
chooser.showDialog()
val psiClass = chooser.selected
if (psiClass is KtLightClassForSourceDeclaration) {
selectedTarget = psiClass.kotlinOrigin
targetObjectChooser.text = psiClass.kotlinOrigin.fqName.toString()
}
},
(targetContainer ?: method.containingClassOrObject as? KtObjectDeclaration)?.fqName?.asString(),
targetContainer ?: method.containingClassOrObject!!,
RECENTS_KEY
)
targetObjectChooser.codeFragment?.let { codeFragment ->
codeFragment.extraCompletionFilter = { lookupElement: LookupElement ->
val lookupObject = lookupElement.getObject() as? DeclarationLookupObject
val psiElement = lookupObject?.psiElement
lookupObject is PackageLookupObject || psiElement is KtObjectDeclaration && psiElement.canRefactor()
}
}
return targetObjectChooser
}
private fun updateOnChanged(list: JList<*>) {
okAction.isEnabled = !list.selectionModel.isSelectionEmpty
}
private fun enableTextFields() {
for (textField in oldClassParameterNameFields.values) {
textField.isEnabled = true
}
if (toClassRadioButton.isSelected) {
val variable = variables[targetVariableList.selectedIndex]
val containingClass = variable.containingClassOrObject as? KtClass ?: return
if (variable !is KtParameter || variable.hasValOrVar()) {
if (thisClassesToMembers[containingClass]?.size == 1
&& thisClassesToMembers[containingClass]?.contains(variable) == true
) {
oldClassParameterNameFields[containingClass]?.isEnabled = false
}
}
}
}
private fun enableTargetChooser() {
if (toClassRadioButton.isSelected) {
targetVariableList.isEnabled = true
targetObjectChooser.isEnabled = false
} else {
targetVariableList.isEnabled = false
targetObjectChooser.isEnabled = true
}
enableTextFields()
}
private fun createParametersPanel(): JPanel? {
if (thisClassesToMembers.isEmpty()) return null
val panel = JPanel(VerticalFlowLayout(VerticalFlowLayout.TOP, 0, 0, true, true))
val validator = NewDeclarationNameValidator(method, null, NewDeclarationNameValidator.Target.VARIABLES)
for (ktClass in thisClassesToMembers.keys) {
val text = KotlinBundle.message("text.select.a.name.for.this.parameter", ktClass.nameAsSafeName.identifier)
panel.add(TitledSeparator(text, null))
val suggestedName = suggestNamesByType(ktClass.resolveToDescriptorIfAny()!!.defaultType, validator).firstOrNull() ?: "parameter"
val field = EditorTextField(suggestedName, project, KotlinFileType.INSTANCE)
field.minimumSize = Dimension(field.preferredSize)
oldClassParameterNameFields[ktClass] = field
panel.add(field)
}
panel.border = BorderFactory.createEmptyBorder(5, 0, 5, 0)
return panel
}
companion object {
val RECENTS_KEY = "${MoveKotlinMethodDialog::class.java.name}.RECENTS_KEY"
}
}
@@ -0,0 +1,118 @@
package org.jetbrains.kotlin.idea.refactoring.move.moveMethod
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiReference
import com.intellij.refactoring.move.MoveCallback
import com.intellij.refactoring.move.MoveHandlerDelegate
import com.intellij.refactoring.util.CommonRefactoringUtil
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.isObjectLiteral
class MoveKotlinMethodHandler : MoveHandlerDelegate() {
private fun showErrorHint(project: Project, dataContext: DataContext?, message: String) {
val editor = if (dataContext == null) null else CommonDataKeys.EDITOR.getData(dataContext)
CommonRefactoringUtil.showErrorHint(project, editor, message, KotlinBundle.message("text.move.method"), null)
}
private fun invokeMoveMethodRefactoring(
project: Project, method: KtNamedFunction, targetContainer: KtClassOrObject?, dataContext: DataContext?
) {
var message: String? = null
if (method.containingClassOrObject == null) return
if (!method.manager.isInProject(method)) {
message = KotlinBundle.message("text.move.method.is.not.supported.for.non.project.methods")
} else if (method.mentionsTypeParameters()) {
message = KotlinBundle.message("text.move.method.is.not.supported.for.generic.classes")
} else if (method.hasModifier(KtTokens.OVERRIDE_KEYWORD) || method.hasModifier(KtTokens.OPEN_KEYWORD)) {
message = KotlinBundle.message("text.move.method.is.not.supported.when.method.is.a.part.of.inheritance.hierarchy")
}
message?.let {
showErrorHint(project, dataContext, message)
return
}
MoveKotlinMethodDialog(method, collectSuitableVariables(method), targetContainer).show()
}
private fun collectSuitableVariables(method: KtNamedFunction): Map<KtNamedDeclaration, KtClass> {
val sourceClassOrObject = method.containingClassOrObject ?: return emptyMap()
val allVariables = mutableListOf<KtNamedDeclaration>()
allVariables.addAll(method.valueParameters)
allVariables.addAll(sourceClassOrObject.declarations.filterIsInstance<KtProperty>())
allVariables.addAll(sourceClassOrObject.primaryConstructorParameters.filter { parameter -> parameter.hasValOrVar() })
val variableToClassMap = LinkedHashMap<KtNamedDeclaration, KtClass>()
for (variable in allVariables) {
(variable.resolveToDescriptorIfAny() as? CallableDescriptor)?.returnType?.let { type ->
if (type.arguments.isEmpty()) {
val ktClass = type.constructor.declarationDescriptor?.findPsi() as? KtClass
if (ktClass != null && method.manager.isInProject(ktClass)) {
variableToClassMap[variable] = ktClass
}
}
}
}
return variableToClassMap
}
override fun canMove(elements: Array<PsiElement?>, targetContainer: PsiElement?, reference: PsiReference?): Boolean {
if (elements.size != 1) return false
val method = elements[0] as? KtNamedFunction ?: return false
val sourceContainer = method.containingClassOrObject
return (targetContainer == null || super.canMove(elements, targetContainer, reference))
&& sourceContainer != null && !sourceContainer.isObjectLiteral()
}
override fun isValidTarget(psiElement: PsiElement?, sources: Array<out PsiElement>): Boolean {
return psiElement is KtClassOrObject && !psiElement.hasModifier(KtTokens.ANNOTATION_KEYWORD)
}
override fun doMove(project: Project, elements: Array<out PsiElement>, targetContainer: PsiElement?, callback: MoveCallback?) {
if (elements.size != 1) return
val method = elements[0] as? KtNamedFunction ?: return
val sourceContainer = method.containingClassOrObject
if (sourceContainer == null || sourceContainer.isObjectLiteral()) return
invokeMoveMethodRefactoring(project, elements[0] as KtNamedFunction, targetContainer as? KtClassOrObject, null)
}
override fun tryToMove(
element: PsiElement, project: Project, dataContext: DataContext?, reference: PsiReference?, editor: Editor?
): Boolean {
if (element is KtNamedFunction) {
element.containingClassOrObject?.let { sourceContainer ->
if (!sourceContainer.isObjectLiteral()) {
invokeMoveMethodRefactoring(project, element, null, dataContext)
return true
}
}
}
return false
}
private fun KtNamedFunction.mentionsTypeParameters(): Boolean {
var ktClassOrObject = containingClassOrObject
val typeParameters = mutableListOf<KtTypeParameter>()
while (ktClassOrObject != null) {
typeParameters.addAll(ktClassOrObject.typeParameters)
ktClassOrObject = if (ktClassOrObject.hasModifier(KtTokens.INNER_KEYWORD)) ktClassOrObject.containingClassOrObject else null
}
collectDescendantsOfType<KtUserType>().forEach { userType ->
if (userType.referenceExpression?.mainReference?.resolve() in typeParameters) return true
}
return false
}
override fun getActionName(elements: Array<out PsiElement>): String = "${KotlinBundle.message("text.move.method")}.."
}
@@ -0,0 +1,334 @@
package org.jetbrains.kotlin.idea.refactoring.move.moveMethod
import com.intellij.ide.util.EditorHelper
import com.intellij.openapi.util.Ref
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiMethodCallExpression
import com.intellij.psi.PsiReferenceExpression
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.refactoring.BaseRefactoringProcessor
import com.intellij.refactoring.move.MoveMultipleElementsViewDescriptor
import com.intellij.refactoring.util.MoveRenameUsageInfo
import com.intellij.usageView.UsageInfo
import com.intellij.usageView.UsageViewBundle
import com.intellij.usageView.UsageViewDescriptor
import com.intellij.util.IncorrectOperationException
import com.intellij.util.containers.MultiMap
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithVisibility
import org.jetbrains.kotlin.descriptors.Visibilities
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.caches.resolve.*
import org.jetbrains.kotlin.idea.core.setVisibility
import org.jetbrains.kotlin.idea.refactoring.move.*
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.KotlinMoveTargetForExistingElement
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.MoveConflictChecker
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.Mover
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.idea.search.projectScope
import org.jetbrains.kotlin.idea.util.getFactoryForImplicitReceiverWithSubtypeOf
import org.jetbrains.kotlin.idea.util.getResolutionScope
import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.name.tail
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.descriptorUtil.isAncestorOf
import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitReceiver
import org.jetbrains.kotlin.resolve.scopes.utils.getImplicitReceiversHierarchy
import org.jetbrains.kotlin.util.containingNonLocalDeclaration
class MoveKotlinMethodProcessor(
private val method: KtNamedFunction,
private val targetVariable: KtNamedDeclaration,
private val oldClassParameterNames: Map<KtClass, String>,
private val openInEditor: Boolean = false
) : BaseRefactoringProcessor(method.project) {
private val targetClassOrObject: KtClassOrObject = if (targetVariable is KtObjectDeclaration) targetVariable else
(targetVariable.resolveToDescriptorIfAny() as? CallableDescriptor)?.returnType?.constructor?.declarationDescriptor?.findPsi() as KtClass
private val factory = KtPsiFactory(myProject)
private val conflicts = MultiMap<PsiElement, String>()
override fun createUsageViewDescriptor(usages: Array<out UsageInfo>): UsageViewDescriptor {
return MoveMultipleElementsViewDescriptor(
arrayOf(method), (targetClassOrObject.fqName ?: UsageViewBundle.message("default.package.presentable.name")).toString()
)
}
override fun preprocessUsages(refUsages: Ref<Array<UsageInfo>>): Boolean {
return showConflicts(conflicts, refUsages.get())
}
override fun findUsages(): Array<UsageInfo> {
val changeInfo = ContainerChangeInfo(
ContainerInfo.Class(method.containingClassOrObject!!.fqName!!),
ContainerInfo.Class(targetClassOrObject.fqName!!)
)
val conflictChecker =
MoveConflictChecker(myProject, listOf(method), KotlinMoveTargetForExistingElement(targetClassOrObject), method)
val searchScope = myProject.projectScope()
val internalUsages = mutableSetOf<UsageInfo>()
val methodCallUsages = mutableSetOf<UsageInfo>()
methodCallUsages += ReferencesSearch.search(method, searchScope).mapNotNull { ref ->
createMoveUsageInfoIfPossible(ref, method, addImportToOriginalFile = true, isInternal = method.isAncestor(ref.element))
}
if (targetVariableIsMethodParameter()) {
internalUsages += ReferencesSearch.search(targetVariable, searchScope).mapNotNull { ref ->
createMoveUsageInfoIfPossible(ref, targetVariable, addImportToOriginalFile = false, isInternal = true)
}
}
internalUsages += method.getInternalReferencesToUpdateOnPackageNameChange(changeInfo)
traverseOuterInstanceReferences(method) { internalUsages += it }
conflictChecker.checkAllConflicts(
methodCallUsages.filter { it is KotlinMoveUsage && !it.isInternal }.toMutableSet(), internalUsages, conflicts
)
if (oldClassParameterNames.size > 1) {
for (usage in methodCallUsages.filter { it.element is KtNameReferenceExpression || it.element is PsiReferenceExpression }) {
conflicts.putValue(usage.element, KotlinBundle.message("text.references.to.outer.classes.have.to.be.added.manually"))
}
}
return (internalUsages + methodCallUsages).toTypedArray()
}
override fun performRefactoring(usages: Array<out UsageInfo>) {
val usagesToProcess = mutableListOf<UsageInfo>()
fun changeMethodSignature() {
if (targetVariableIsMethodParameter()) {
val parameterIndex = method.valueParameters.indexOf(targetVariable as KtParameter)
method.valueParameterList?.removeParameter(parameterIndex)
}
for ((ktClass, parameterName) in oldClassParameterNames) {
method.valueParameterList?.addParameterBefore(
factory.createParameter("$parameterName: ${ktClass.nameAsSafeName.identifier}"),
method.valueParameters.firstOrNull()
)
}
}
fun KtNameReferenceExpression.getImplicitReceiver(): KtExpression? {
val scope = getResolutionScope(this.analyze()) ?: return null
val descriptor = this.resolveToCall()?.resultingDescriptor ?: return null
val receiverDescriptor = descriptor.extensionReceiverParameter
?: descriptor.dispatchReceiverParameter
?: return null
val expressionFactory = scope.getFactoryForImplicitReceiverWithSubtypeOf(receiverDescriptor.type)
?: return null
val receiverText = if (expressionFactory.isImmediate) "this" else expressionFactory.expressionText
return factory.createExpression(receiverText)
}
fun escalateTargetVariableVisibilityIfNeeded(where: DeclarationDescriptor?) {
if (where == null || targetVariableIsMethodParameter()) return
val targetDescriptor = targetVariable.resolveToDescriptorIfAny() as? DeclarationDescriptorWithVisibility
?: return
if (!Visibilities.isVisibleIgnoringReceiver(targetDescriptor, where) && method.manager.isInProject(targetVariable)) {
targetVariable.setVisibility(KtTokens.PUBLIC_KEYWORD)
}
}
fun correctMethodCall(expression: PsiElement) {
when (expression) {
is KtNameReferenceExpression -> {
val callExpression = expression.parent as? KtCallExpression ?: return
escalateTargetVariableVisibilityIfNeeded(callExpression.containingNonLocalDeclaration()?.resolveToDescriptorIfAny())
val oldReceiver = callExpression.getQualifiedExpressionForSelector()?.receiverExpression
?: expression.getImplicitReceiver()
?: return
val newReceiver = if (targetVariable is KtObjectDeclaration) {
factory.createExpression(targetVariable.nameAsSafeName.identifier)
} else if (targetVariableIsMethodParameter()) {
val parameterIndex = method.valueParameters.indexOf(targetVariable as KtParameter)
if (parameterIndex in callExpression.valueArguments.indices) {
val argumentExpression = callExpression.valueArguments[parameterIndex].getArgumentExpression()
?: return
callExpression.valueArgumentList?.removeArgument(parameterIndex)
argumentExpression
} else targetVariable.defaultValue
} else {
factory.createExpression("${oldReceiver.text}.${targetVariable.nameAsSafeName.identifier}")
} ?: return
if (method.containingClassOrObject in oldClassParameterNames) {
callExpression.valueArgumentList?.addArgumentBefore(
factory.createArgument(oldReceiver),
callExpression.valueArguments.firstOrNull()
)
}
val resultingExpression = callExpression.getQualifiedExpressionForSelectorOrThis()
.replace(factory.createExpressionByPattern("$0.$1", newReceiver.text, callExpression))
if (targetVariable is KtObjectDeclaration) {
val ref = (resultingExpression as? KtQualifiedExpression)?.receiverExpression?.mainReference ?: return
createMoveUsageInfoIfPossible(
ref, targetClassOrObject, addImportToOriginalFile = true,
isInternal = targetClassOrObject.isAncestor(ref.element)
)?.let { usagesToProcess += it }
}
}
is PsiReferenceExpression -> {
val callExpression = expression.parent as? PsiMethodCallExpression ?: return
val oldReceiver = callExpression.methodExpression.qualifierExpression ?: return
val newReceiver = if (targetVariable is KtObjectDeclaration) {
// todo: fix usage of target object (import might be needed)
val targetObjectName = targetVariable.fqName?.tail(targetVariable.containingKtFile.packageFqName)?.toString()
?: targetVariable.nameAsSafeName.identifier
JavaPsiFacade.getElementFactory(myProject).createExpressionFromText("$targetObjectName.INSTANCE", null)
} else if (targetVariableIsMethodParameter()) {
val parameterIndex = method.valueParameters.indexOf(targetVariable as KtParameter)
val arguments = callExpression.argumentList.expressions
if (parameterIndex in arguments.indices) {
val argumentExpression = arguments[parameterIndex].copy() ?: return
arguments[parameterIndex].delete()
argumentExpression
} else return
} else {
val getterName = "get${targetVariable.nameAsSafeName.identifier.capitalize()}"
JavaPsiFacade.getElementFactory(myProject).createExpressionFromText("${oldReceiver.text}.$getterName()", null)
}
if (method.containingClassOrObject in oldClassParameterNames) {
callExpression.argumentList.addBefore(oldReceiver, callExpression.argumentList.expressions.firstOrNull())
}
oldReceiver.replace(newReceiver)
}
}
}
fun replaceReferenceToTargetWithThis(element: KtExpression) {
val scope = element.getResolutionScope(element.analyze()) ?: return
val receivers = scope.getImplicitReceiversHierarchy()
val receiverText =
if (receivers.isEmpty() || receivers[0].containingDeclaration == method.containingClassOrObject?.resolveToDescriptorIfAny())
"this" else "this@${targetClassOrObject.nameAsSafeName.identifier}"
element.replace(factory.createExpression(receiverText))
}
val (methodCallUsages, internalUsages) = usages.partition { it is MoveRenameUsageInfo && it.referencedElement == method }
val newInternalUsages = mutableListOf<UsageInfo>()
val oldInternalUsages = mutableListOf<UsageInfo>()
try {
for (usage in methodCallUsages) {
usage.element?.let { element ->
correctMethodCall(element)
}
}
for (usage in internalUsages) {
val element = usage.element ?: continue
if (usage is MoveRenameUsageInfo) {
if (usage.referencedElement == targetVariable && element is KtNameReferenceExpression) {
replaceReferenceToTargetWithThis(element)
} else {
oldInternalUsages += usage
}
}
if (usage is SourceInstanceReferenceUsageInfo) {
if (usage.member == targetVariable && element is KtNameReferenceExpression) {
replaceReferenceToTargetWithThis(
(element as? KtThisExpression)?.getQualifiedExpressionForReceiver() ?: element
)
} else {
val receiverText = oldClassParameterNames[usage.sourceOrOuter] ?: continue
when (element) {
is KtThisExpression -> element.replace(factory.createExpression(receiverText))
is KtNameReferenceExpression -> {
val elementToReplace = (element.parent as? KtCallExpression) ?: element
elementToReplace.replace(factory.createExpressionByPattern("$0.$1", receiverText, elementToReplace))
}
}
}
}
}
changeMethodSignature()
markInternalUsages(oldInternalUsages)
val movedMethod = Mover.Default(method, targetClassOrObject)
val oldToNewMethodMap = mapOf<PsiElement, PsiElement>(method to movedMethod)
newInternalUsages += restoreInternalUsages(movedMethod, oldToNewMethodMap)
usagesToProcess += newInternalUsages
postProcessMoveUsages(usagesToProcess, oldToNewMethodMap)
if (openInEditor) EditorHelper.openInEditor(movedMethod)
} catch (e: IncorrectOperationException) {
} finally {
cleanUpInternalUsages(oldInternalUsages + newInternalUsages)
}
}
private fun targetVariableIsMethodParameter(): Boolean = targetVariable is KtParameter && !targetVariable.hasValOrVar()
override fun getCommandName(): String = KotlinBundle.message("text.move.method")
}
internal fun getThisClassesToMembers(method: KtNamedFunction) = traverseOuterInstanceReferences(method)
private fun traverseOuterInstanceReferences(
method: KtNamedFunction,
body: (SourceInstanceReferenceUsageInfo) -> Unit = {}
): LinkedHashMap<KtClass, MutableSet<KtNamedDeclaration>> {
val context = method.analyzeWithContent()
val containingClassOrObject = method.containingClassOrObject ?: return LinkedHashMap()
val descriptor = containingClassOrObject.unsafeResolveToDescriptor()
fun getClassOrObjectAndMemberReferencedBy(reference: KtExpression): Pair<DeclarationDescriptor?, CallableDescriptor?> {
var classOrObjectDescriptor: DeclarationDescriptor? = null
var memberDescriptor: CallableDescriptor? = null
if (reference is KtThisExpression) {
classOrObjectDescriptor = context[BindingContext.REFERENCE_TARGET, reference.instanceReference]
if (classOrObjectDescriptor?.isAncestorOf(descriptor, false) == true) {
memberDescriptor =
reference.getQualifiedExpressionForReceiver()?.selectorExpression?.getResolvedCall(context)?.resultingDescriptor
}
}
if (reference is KtNameReferenceExpression) {
val dispatchReceiver = reference.getResolvedCall(context)?.dispatchReceiver as? ImplicitReceiver
classOrObjectDescriptor = dispatchReceiver?.declarationDescriptor
if (classOrObjectDescriptor?.isAncestorOf(descriptor, false) == true) {
memberDescriptor = reference.getResolvedCall(context)?.resultingDescriptor
}
}
return classOrObjectDescriptor to memberDescriptor
}
val thisClassesToMembers = LinkedHashMap<KtClass, MutableSet<KtNamedDeclaration>>()
method.bodyExpression?.forEachDescendantOfType<KtExpression> { reference ->
val (classOrObjectDescriptor, memberDescriptor) = getClassOrObjectAndMemberReferencedBy(reference)
(classOrObjectDescriptor?.findPsi() as? KtClassOrObject)?.let { resolvedClassOrObject ->
val resolvedMember = memberDescriptor?.findPsi() as? KtNamedDeclaration
if (resolvedClassOrObject is KtClass) {
if (resolvedClassOrObject in thisClassesToMembers) thisClassesToMembers[resolvedClassOrObject]?.add(
resolvedMember
?: resolvedClassOrObject
)
else thisClassesToMembers[resolvedClassOrObject] = mutableSetOf(resolvedMember ?: resolvedClassOrObject)
}
body(SourceInstanceReferenceUsageInfo(reference, resolvedClassOrObject, resolvedMember))
}
}
return thisClassesToMembers
}
internal class SourceInstanceReferenceUsageInfo(
reference: KtExpression, val sourceOrOuter: KtClassOrObject, val member: KtNamedDeclaration?
) : UsageInfo(reference)
@@ -0,0 +1,14 @@
package a
import b.B
class A {
fun bar(b: B) {
b.foo()
val a = A().apply {
b.foo()
b.foo()
}
}
}
@@ -0,0 +1,13 @@
package b
import a.A
class B (val a: A) {
fun baz(b: B) {
b.foo()
}
fun foo() {
val a1 = this.a
}
}
@@ -0,0 +1,10 @@
package usages;
import a.A;
import b.B;
public class JavaUsage {
void javaUsage(A a, B b) {
b.foo();
}
}
@@ -0,0 +1,6 @@
import a.A
import b.B
fun kotlinUsage(a: A, b: B) {
b.foo()
}
@@ -0,0 +1,17 @@
package a
import b.B
class A {
fun foo(b: B) {
val a1 = b.a
}
fun bar(b: B) {
foo(b)
val a = A().apply {
foo(b)
this@A.foo(b)
}
}
}
@@ -0,0 +1,9 @@
package b
import a.A
class B (val a: A) {
fun baz(b: B) {
a.foo(b)
}
}
@@ -0,0 +1,10 @@
package usages;
import a.A;
import b.B;
public class JavaUsage {
void javaUsage(A a, B b) {
a.foo(b);
}
}
@@ -0,0 +1,6 @@
import a.A
import b.B
fun kotlinUsage(a: A, b: B) {
a.foo(b)
}
@@ -0,0 +1,6 @@
{
"mainFile": "a/A.kt",
"type": "MOVE_KOTLIN_METHOD",
"methodToMove": "foo",
"methodParameter": "b"
}
@@ -0,0 +1,8 @@
package a
import b.B
class A(val b: B) {
fun bar() {}
}
@@ -0,0 +1,14 @@
package b
import a.A
class B (val a: A) {
fun baz() {
a.b.foo(a)
}
fun foo(a: A) {
val a1 = this.a
a.bar()
}
}
@@ -0,0 +1,9 @@
package usages;
import a.A;
public class JavaUsage {
void javaUsage(A a) {
a.getB().foo(a);
}
}
@@ -0,0 +1,5 @@
import a.A
fun kotlinUsage(a: A) {
a.b.foo(a)
}
@@ -0,0 +1,12 @@
package a
import b.B
class A(val b: B) {
fun foo() {
val a1 = b.a
bar()
}
fun bar() {}
}
@@ -0,0 +1,9 @@
package b
import a.A
class B (val a: A) {
fun baz() {
a.foo()
}
}
@@ -0,0 +1,9 @@
package usages;
import a.A;
public class JavaUsage {
void javaUsage(A a) {
a.foo();
}
}
@@ -0,0 +1,5 @@
import a.A
fun kotlinUsage(a: A) {
a.foo()
}
@@ -0,0 +1,7 @@
{
"mainFile": "a/A.kt",
"type": "MOVE_KOTLIN_METHOD",
"methodToMove": "foo",
"sourceProperty": "b",
"outerInstanceParameter": "a"
}
@@ -0,0 +1,8 @@
package a
import b.B
class A(val b: B) {
fun bar() {}
}
@@ -0,0 +1,13 @@
package b
import a.A
object B {
fun baz(a: A) {
B.foo(a)
}
fun foo(a: A) {
a.bar()
}
}
@@ -0,0 +1,10 @@
package usages;
import a.A;
import b.B;
public class JavaUsage {
void javaUsage(A a) {
B.INSTANCE.foo(a);
}
}
@@ -0,0 +1,6 @@
import a.A
import b.B
fun kotlinUsage(a: A) {
B.foo(a)
}
@@ -0,0 +1,11 @@
package a
import b.B
class A(val b: B) {
fun foo() {
bar()
}
fun bar() {}
}
@@ -0,0 +1,9 @@
package b
import a.A
object B {
fun baz(a: A) {
a.foo()
}
}
@@ -0,0 +1,10 @@
package usages;
import a.A;
import b.B;
public class JavaUsage {
void javaUsage(A a) {
a.foo();
}
}
@@ -0,0 +1,5 @@
import a.A
fun kotlinUsage(a: A) {
a.foo()
}
@@ -0,0 +1,7 @@
{
"mainFile": "a/A.kt",
"type": "MOVE_KOTLIN_METHOD",
"methodToMove": "foo",
"targetObject": "b.B",
"outerInstanceParameter": "a"
}
@@ -29,14 +29,19 @@ import org.jetbrains.kotlin.idea.refactoring.createKotlinFile
import org.jetbrains.kotlin.idea.refactoring.move.changePackage.KotlinChangePackageRefactoring
import org.jetbrains.kotlin.idea.refactoring.move.moveClassesOrPackages.KotlinAwareDelegatingMoveDestination
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.*
import org.jetbrains.kotlin.idea.refactoring.move.moveMethod.MoveKotlinMethodProcessor
import org.jetbrains.kotlin.idea.refactoring.runRefactoringTest
import org.jetbrains.kotlin.idea.search.allScope
import org.jetbrains.kotlin.idea.search.projectScope
import org.jetbrains.kotlin.idea.stubindex.KotlinFullClassNameIndex
import org.jetbrains.kotlin.idea.stubindex.KotlinFunctionShortNameIndex
import org.jetbrains.kotlin.idea.stubindex.KotlinPropertyShortNameIndex
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
abstract class AbstractMoveTest : AbstractMultifileRefactoringTest() {
@@ -283,5 +288,29 @@ enum class MoveAction : AbstractMultifileRefactoringTest.RefactoringAction {
val descriptor = MoveDeclarationsDescriptor(project, MoveSource(elementToMove), moveTarget, delegate)
MoveKotlinDeclarationsProcessor(descriptor).run()
}
},
MOVE_KOTLIN_METHOD {
override fun runRefactoring(rootDir: VirtualFile, mainFile: PsiFile, elementsAtCaret: List<PsiElement>, config: JsonObject) {
val project = mainFile.project
val method =
KotlinFunctionShortNameIndex.getInstance().get(config.getString("methodToMove"), project, project.projectScope()).first()
val methodParameterName = config.getNullableString("methodParameter")
val sourcePropertyName = config.getNullableString("sourceProperty")
val targetObjectName = config.getNullableString("targetObject")
val targetVariable = when {
methodParameterName != null -> method.valueParameters.find { it.name == methodParameterName }!!
sourcePropertyName != null -> KotlinPropertyShortNameIndex.getInstance()
.get(sourcePropertyName, project, project.projectScope()).first()
else -> KotlinFullClassNameIndex.getInstance().get(targetObjectName!!, project, project.projectScope()).first()
}
val oldClassParameterNames = mutableMapOf<KtClass, String>()
val outerInstanceParameter = config.getNullableString("outerInstanceParameter")
if (outerInstanceParameter != null) {
oldClassParameterNames[method.containingClassOrObject as KtClass] = outerInstanceParameter
}
MoveKotlinMethodProcessor(method, targetVariable, oldClassParameterNames).run()
}
};
}
@@ -323,6 +323,21 @@ public class MoveTestGenerated extends AbstractMoveTest {
runTest("idea/testData/refactoring/move/kotlin/moveFile/typeRefWithArguments/typeRefWithArguments.test");
}
@TestMetadata("kotlin/moveMethod/moveToClass/parameterAsReference/parameterAsReference.test")
public void testKotlin_moveMethod_moveToClass_parameterAsReference_ParameterAsReference() throws Exception {
runTest("idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/parameterAsReference.test");
}
@TestMetadata("kotlin/moveMethod/moveToClass/propertyAsReference/propertyAsReference.test")
public void testKotlin_moveMethod_moveToClass_propertyAsReference_PropertyAsReference() throws Exception {
runTest("idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/propertyAsReference.test");
}
@TestMetadata("kotlin/moveMethod/moveToObject/moveToObject.test")
public void testKotlin_moveMethod_moveToObject_MoveToObject() throws Exception {
runTest("idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/moveToObject.test");
}
@TestMetadata("kotlin/moveNestedClass/callableReferences/nestedToAnotherClass/nestedToAnotherClass.test")
public void testKotlin_moveNestedClass_callableReferences_nestedToAnotherClass_NestedToAnotherClass() throws Exception {
runTest("idea/testData/refactoring/move/kotlin/moveNestedClass/callableReferences/nestedToAnotherClass/nestedToAnotherClass.test");