From 551546576036d6264d7d4b5433a003820b582015 Mon Sep 17 00:00:00 2001 From: aleksandrina-streltsova Date: Wed, 3 Jun 2020 11:17:57 +0300 Subject: [PATCH] Move: Support method moving --- .../messages/KotlinBundle.properties | 12 + idea/resources/META-INF/plugin-common.xml | 4 + .../move/moveMethod/MoveKotlinMethodDialog.kt | 290 +++++++++++++++ .../moveMethod/MoveKotlinMethodHandler.kt | 118 +++++++ .../moveMethod/MoveKotlinMethodProcessor.kt | 334 ++++++++++++++++++ .../parameterAsReference/after/a/A.kt | 14 + .../parameterAsReference/after/b/B.kt | 13 + .../after/usages/JavaUsage.java | 10 + .../after/usages/kotlinUsage.kt | 6 + .../parameterAsReference/before/a/A.kt | 17 + .../parameterAsReference/before/b/B.kt | 9 + .../before/usages/JavaUsage.java | 10 + .../before/usages/kotlinUsage.kt | 6 + .../parameterAsReference.test | 6 + .../propertyAsReference/after/a/A.kt | 8 + .../propertyAsReference/after/b/B.kt | 14 + .../after/usages/JavaUsage.java | 9 + .../after/usages/kotlinUsage.kt | 5 + .../propertyAsReference/before/a/A.kt | 12 + .../propertyAsReference/before/b/B.kt | 9 + .../before/usages/JavaUsage.java | 9 + .../before/usages/kotlinUsage.kt | 5 + .../propertyAsReference.test | 7 + .../moveMethod/moveToObject/after/a/A.kt | 8 + .../moveMethod/moveToObject/after/b/B.kt | 13 + .../moveToObject/after/usages/JavaUsage.java | 10 + .../moveToObject/after/usages/kotlinUsage.kt | 6 + .../moveMethod/moveToObject/before/a/A.kt | 11 + .../moveMethod/moveToObject/before/b/B.kt | 9 + .../moveToObject/before/usages/JavaUsage.java | 10 + .../moveToObject/before/usages/kotlinUsage.kt | 5 + .../moveMethod/moveToObject/moveToObject.test | 7 + .../idea/refactoring/move/AbstractMoveTest.kt | 29 ++ .../refactoring/move/MoveTestGenerated.java | 15 + 34 files changed, 1050 insertions(+) create mode 100644 idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveMethod/MoveKotlinMethodDialog.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveMethod/MoveKotlinMethodHandler.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveMethod/MoveKotlinMethodProcessor.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/after/a/A.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/after/b/B.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/after/usages/JavaUsage.java create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/after/usages/kotlinUsage.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/before/a/A.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/before/b/B.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/before/usages/JavaUsage.java create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/before/usages/kotlinUsage.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/parameterAsReference.test create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/after/a/A.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/after/b/B.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/after/usages/JavaUsage.java create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/after/usages/kotlinUsage.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/before/a/A.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/before/b/B.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/before/usages/JavaUsage.java create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/before/usages/kotlinUsage.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/propertyAsReference.test create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/after/a/A.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/after/b/B.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/after/usages/JavaUsage.java create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/after/usages/kotlinUsage.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/before/a/A.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/before/b/B.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/before/usages/JavaUsage.java create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/before/usages/kotlinUsage.kt create mode 100644 idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/moveToObject.test diff --git a/idea/resources-en/messages/KotlinBundle.properties b/idea/resources-en/messages/KotlinBundle.properties index 566ce14db1b..714caa46638 100644 --- a/idea/resources-en/messages/KotlinBundle.properties +++ b/idea/resources-en/messages/KotlinBundle.properties @@ -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 diff --git a/idea/resources/META-INF/plugin-common.xml b/idea/resources/META-INF/plugin-common.xml index c2d044dffb8..e84f5118026 100644 --- a/idea/resources/META-INF/plugin-common.xml +++ b/idea/resources/META-INF/plugin-common.xml @@ -324,6 +324,10 @@ id="kotlinAwareJavaMoveClassesOrPackagesHandler" implementation="org.jetbrains.kotlin.idea.refactoring.move.moveClassesOrPackages.KotlinAwareJavaMoveClassesOrPackagesHandler" order="first" /> + , + 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() + 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() + 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 { + val listModel = object : AbstractListModel() { + 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" + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveMethod/MoveKotlinMethodHandler.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveMethod/MoveKotlinMethodHandler.kt new file mode 100644 index 00000000000..903e04be004 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveMethod/MoveKotlinMethodHandler.kt @@ -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 { + val sourceClassOrObject = method.containingClassOrObject ?: return emptyMap() + val allVariables = mutableListOf() + + allVariables.addAll(method.valueParameters) + allVariables.addAll(sourceClassOrObject.declarations.filterIsInstance()) + allVariables.addAll(sourceClassOrObject.primaryConstructorParameters.filter { parameter -> parameter.hasValOrVar() }) + + val variableToClassMap = LinkedHashMap() + 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, 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): Boolean { + return psiElement is KtClassOrObject && !psiElement.hasModifier(KtTokens.ANNOTATION_KEYWORD) + } + + override fun doMove(project: Project, elements: Array, 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() + while (ktClassOrObject != null) { + typeParameters.addAll(ktClassOrObject.typeParameters) + ktClassOrObject = if (ktClassOrObject.hasModifier(KtTokens.INNER_KEYWORD)) ktClassOrObject.containingClassOrObject else null + } + collectDescendantsOfType().forEach { userType -> + if (userType.referenceExpression?.mainReference?.resolve() in typeParameters) return true + } + return false + } + + override fun getActionName(elements: Array): String = "${KotlinBundle.message("text.move.method")}.." +} diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveMethod/MoveKotlinMethodProcessor.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveMethod/MoveKotlinMethodProcessor.kt new file mode 100644 index 00000000000..448153338a6 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveMethod/MoveKotlinMethodProcessor.kt @@ -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, + 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() + + override fun createUsageViewDescriptor(usages: Array): UsageViewDescriptor { + return MoveMultipleElementsViewDescriptor( + arrayOf(method), (targetClassOrObject.fqName ?: UsageViewBundle.message("default.package.presentable.name")).toString() + ) + } + + override fun preprocessUsages(refUsages: Ref>): Boolean { + return showConflicts(conflicts, refUsages.get()) + } + + override fun findUsages(): Array { + 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() + val methodCallUsages = mutableSetOf() + + 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) { + val usagesToProcess = mutableListOf() + + 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() + val oldInternalUsages = mutableListOf() + + 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(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> { + val context = method.analyzeWithContent() + val containingClassOrObject = method.containingClassOrObject ?: return LinkedHashMap() + val descriptor = containingClassOrObject.unsafeResolveToDescriptor() + + fun getClassOrObjectAndMemberReferencedBy(reference: KtExpression): Pair { + 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>() + method.bodyExpression?.forEachDescendantOfType { 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) diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/after/a/A.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/after/a/A.kt new file mode 100644 index 00000000000..3f9bb2f1202 --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/after/a/A.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/after/b/B.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/after/b/B.kt new file mode 100644 index 00000000000..ae6cb29f391 --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/after/b/B.kt @@ -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 + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/after/usages/JavaUsage.java b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/after/usages/JavaUsage.java new file mode 100644 index 00000000000..aa3aec867fb --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/after/usages/JavaUsage.java @@ -0,0 +1,10 @@ +package usages; + +import a.A; +import b.B; + +public class JavaUsage { + void javaUsage(A a, B b) { + b.foo(); + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/after/usages/kotlinUsage.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/after/usages/kotlinUsage.kt new file mode 100644 index 00000000000..88db436b677 --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/after/usages/kotlinUsage.kt @@ -0,0 +1,6 @@ +import a.A +import b.B + +fun kotlinUsage(a: A, b: B) { + b.foo() +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/before/a/A.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/before/a/A.kt new file mode 100644 index 00000000000..f66e26250f4 --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/before/a/A.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/before/b/B.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/before/b/B.kt new file mode 100644 index 00000000000..68d91b180ab --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/before/b/B.kt @@ -0,0 +1,9 @@ +package b + +import a.A + +class B (val a: A) { + fun baz(b: B) { + a.foo(b) + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/before/usages/JavaUsage.java b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/before/usages/JavaUsage.java new file mode 100644 index 00000000000..9c486e9ae98 --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/before/usages/JavaUsage.java @@ -0,0 +1,10 @@ +package usages; + +import a.A; +import b.B; + +public class JavaUsage { + void javaUsage(A a, B b) { + a.foo(b); + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/before/usages/kotlinUsage.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/before/usages/kotlinUsage.kt new file mode 100644 index 00000000000..92f9ca146f5 --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/before/usages/kotlinUsage.kt @@ -0,0 +1,6 @@ +import a.A +import b.B + +fun kotlinUsage(a: A, b: B) { + a.foo(b) +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/parameterAsReference.test b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/parameterAsReference.test new file mode 100644 index 00000000000..8ecfc019f75 --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/parameterAsReference/parameterAsReference.test @@ -0,0 +1,6 @@ +{ + "mainFile": "a/A.kt", + "type": "MOVE_KOTLIN_METHOD", + "methodToMove": "foo", + "methodParameter": "b" +} diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/after/a/A.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/after/a/A.kt new file mode 100644 index 00000000000..49c624d5b6a --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/after/a/A.kt @@ -0,0 +1,8 @@ +package a + +import b.B + +class A(val b: B) { + + fun bar() {} +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/after/b/B.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/after/b/B.kt new file mode 100644 index 00000000000..8bc32110ef1 --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/after/b/B.kt @@ -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() + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/after/usages/JavaUsage.java b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/after/usages/JavaUsage.java new file mode 100644 index 00000000000..547894d12cb --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/after/usages/JavaUsage.java @@ -0,0 +1,9 @@ +package usages; + +import a.A; + +public class JavaUsage { + void javaUsage(A a) { + a.getB().foo(a); + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/after/usages/kotlinUsage.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/after/usages/kotlinUsage.kt new file mode 100644 index 00000000000..74b8a9db988 --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/after/usages/kotlinUsage.kt @@ -0,0 +1,5 @@ +import a.A + +fun kotlinUsage(a: A) { + a.b.foo(a) +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/before/a/A.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/before/a/A.kt new file mode 100644 index 00000000000..e5f57eb6096 --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/before/a/A.kt @@ -0,0 +1,12 @@ +package a + +import b.B + +class A(val b: B) { + fun foo() { + val a1 = b.a + bar() + } + + fun bar() {} +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/before/b/B.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/before/b/B.kt new file mode 100644 index 00000000000..a85c627a6e7 --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/before/b/B.kt @@ -0,0 +1,9 @@ +package b + +import a.A + +class B (val a: A) { + fun baz() { + a.foo() + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/before/usages/JavaUsage.java b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/before/usages/JavaUsage.java new file mode 100644 index 00000000000..6ebf27052a5 --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/before/usages/JavaUsage.java @@ -0,0 +1,9 @@ +package usages; + +import a.A; + +public class JavaUsage { + void javaUsage(A a) { + a.foo(); + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/before/usages/kotlinUsage.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/before/usages/kotlinUsage.kt new file mode 100644 index 00000000000..1ab7afb6c83 --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/before/usages/kotlinUsage.kt @@ -0,0 +1,5 @@ +import a.A + +fun kotlinUsage(a: A) { + a.foo() +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/propertyAsReference.test b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/propertyAsReference.test new file mode 100644 index 00000000000..ad468d5c78c --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToClass/propertyAsReference/propertyAsReference.test @@ -0,0 +1,7 @@ +{ + "mainFile": "a/A.kt", + "type": "MOVE_KOTLIN_METHOD", + "methodToMove": "foo", + "sourceProperty": "b", + "outerInstanceParameter": "a" +} diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/after/a/A.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/after/a/A.kt new file mode 100644 index 00000000000..49c624d5b6a --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/after/a/A.kt @@ -0,0 +1,8 @@ +package a + +import b.B + +class A(val b: B) { + + fun bar() {} +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/after/b/B.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/after/b/B.kt new file mode 100644 index 00000000000..1c28f1806da --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/after/b/B.kt @@ -0,0 +1,13 @@ +package b + +import a.A + +object B { + fun baz(a: A) { + B.foo(a) + } + + fun foo(a: A) { + a.bar() + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/after/usages/JavaUsage.java b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/after/usages/JavaUsage.java new file mode 100644 index 00000000000..93cb3853f94 --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/after/usages/JavaUsage.java @@ -0,0 +1,10 @@ +package usages; + +import a.A; +import b.B; + +public class JavaUsage { + void javaUsage(A a) { + B.INSTANCE.foo(a); + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/after/usages/kotlinUsage.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/after/usages/kotlinUsage.kt new file mode 100644 index 00000000000..4f38dd0c5ad --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/after/usages/kotlinUsage.kt @@ -0,0 +1,6 @@ +import a.A +import b.B + +fun kotlinUsage(a: A) { + B.foo(a) +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/before/a/A.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/before/a/A.kt new file mode 100644 index 00000000000..ff9428b8bdf --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/before/a/A.kt @@ -0,0 +1,11 @@ +package a + +import b.B + +class A(val b: B) { + fun foo() { + bar() + } + + fun bar() {} +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/before/b/B.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/before/b/B.kt new file mode 100644 index 00000000000..98f05de681e --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/before/b/B.kt @@ -0,0 +1,9 @@ +package b + +import a.A + +object B { + fun baz(a: A) { + a.foo() + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/before/usages/JavaUsage.java b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/before/usages/JavaUsage.java new file mode 100644 index 00000000000..d4d380bc282 --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/before/usages/JavaUsage.java @@ -0,0 +1,10 @@ +package usages; + +import a.A; +import b.B; + +public class JavaUsage { + void javaUsage(A a) { + a.foo(); + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/before/usages/kotlinUsage.kt b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/before/usages/kotlinUsage.kt new file mode 100644 index 00000000000..1ab7afb6c83 --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/before/usages/kotlinUsage.kt @@ -0,0 +1,5 @@ +import a.A + +fun kotlinUsage(a: A) { + a.foo() +} \ No newline at end of file diff --git a/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/moveToObject.test b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/moveToObject.test new file mode 100644 index 00000000000..5448f08f611 --- /dev/null +++ b/idea/testData/refactoring/move/kotlin/moveMethod/moveToObject/moveToObject.test @@ -0,0 +1,7 @@ +{ + "mainFile": "a/A.kt", + "type": "MOVE_KOTLIN_METHOD", + "methodToMove": "foo", + "targetObject": "b.B", + "outerInstanceParameter": "a" +} diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/move/AbstractMoveTest.kt b/idea/tests/org/jetbrains/kotlin/idea/refactoring/move/AbstractMoveTest.kt index dd3e1c3e33e..7b1295df63d 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/move/AbstractMoveTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/move/AbstractMoveTest.kt @@ -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, 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() + val outerInstanceParameter = config.getNullableString("outerInstanceParameter") + if (outerInstanceParameter != null) { + oldClassParameterNames[method.containingClassOrObject as KtClass] = outerInstanceParameter + } + MoveKotlinMethodProcessor(method, targetVariable, oldClassParameterNames).run() + } }; } diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/move/MoveTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/refactoring/move/MoveTestGenerated.java index a040e7b76e7..2c7c841155a 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/move/MoveTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/move/MoveTestGenerated.java @@ -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");