diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 3a6239a9617..395e8941429 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -305,6 +305,7 @@ + diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/rename/AutomaticVariableRenamer.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/rename/AutomaticVariableRenamer.kt new file mode 100644 index 00000000000..6880307c00c --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/rename/AutomaticVariableRenamer.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.refactoring.rename + +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.refactoring.JavaRefactoringSettings +import com.intellij.refactoring.RefactoringBundle +import com.intellij.refactoring.rename.naming.AutomaticRenamer +import com.intellij.refactoring.rename.naming.AutomaticRenamerFactory +import com.intellij.usageView.UsageInfo +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.VariableDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor +import org.jetbrains.kotlin.psi.JetClass +import org.jetbrains.kotlin.psi.JetNamedDeclaration +import org.jetbrains.kotlin.psi.JetParameter +import org.jetbrains.kotlin.psi.JetVariableDeclaration +import org.jetbrains.kotlin.psi.psiUtil.isAncestor +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.types.JetType +import java.util.ArrayList + +public class AutomaticVariableRenamer(klass: JetClass, newClassName: String, usages: Collection) : AutomaticRenamer() { + private val toUnpluralize = ArrayList() + + init { + val classType = (klass.resolveToDescriptor() as ClassDescriptor).getDefaultType() + + for (usage in usages) { + val usageElement = usage.getElement() ?: continue + + val parameterOrVariable = PsiTreeUtil.getParentOfType( + usageElement, + javaClass(), + javaClass() + ) ?: continue + + if (parameterOrVariable.getTypeReference()?.isAncestor(usageElement) != true) continue + val type = (parameterOrVariable.resolveToDescriptor() as VariableDescriptor).getType() + if (type.isCollectionLikeOf(classType)) { + toUnpluralize.add(parameterOrVariable) + } + + myElements.add(parameterOrVariable) + } + + suggestAllNames(klass.getName(), newClassName) + } + + override fun getDialogTitle() = RefactoringBundle.message("rename.variables.title") + + override fun getDialogDescription() = RefactoringBundle.message("rename.variables.with.the.following.names.to") + + override fun entityName() = RefactoringBundle.message("entity.name.variable") + + override fun nameToCanonicalName(name: String, element: PsiNamedElement): String? { + if (element in toUnpluralize) { + val singular = StringUtil.unpluralize(name) + if (singular != null) return singular + toUnpluralize.remove(element) + } + return name + } + + override fun canonicalNameToName(canonicalName: String, element: PsiNamedElement): String? { + return if (element in toUnpluralize) + StringUtil.pluralize(canonicalName) + else + canonicalName + } +} + +private fun JetType.isCollectionLikeOf(elementType: JetType): Boolean { + val klass = this.getConstructor().getDeclarationDescriptor() as? ClassDescriptor ?: return false + if (KotlinBuiltIns.isArray(this) || DescriptorUtils.isSubclass(klass, KotlinBuiltIns.getInstance().getCollection())) { + val typeArgument = this.getArguments().singleOrNull()?.getType() ?: return false + return elementType == typeArgument || typeArgument.isCollectionLikeOf(elementType) + } + return false +} + + +public class AutomaticVariableRenamerFactory: AutomaticRenamerFactory { + override fun isApplicable(element: PsiElement) = element is JetClass + + override fun createRenamer(element: PsiElement, newName: String, usages: Collection) = + AutomaticVariableRenamer(element as JetClass, newName, usages) + + override fun isEnabled() = JavaRefactoringSettings.getInstance().isToRenameVariables() + override fun setEnabled(enabled: Boolean) = JavaRefactoringSettings.getInstance().setRenameVariables(enabled) + + override fun getOptionName() = RefactoringBundle.message("rename.variables") +} \ No newline at end of file diff --git a/idea/testData/refactoring/rename/automaticRenamer/after/lib.kt b/idea/testData/refactoring/rename/automaticRenamer/after/lib.kt new file mode 100644 index 00000000000..f738e66d130 --- /dev/null +++ b/idea/testData/refactoring/rename/automaticRenamer/after/lib.kt @@ -0,0 +1,7 @@ +data class Pair( + public val first: A, + public val second: B +) + + +public fun listOf(): List = throw Error() diff --git a/idea/testData/refactoring/rename/automaticRenamer/after/main.kt b/idea/testData/refactoring/rename/automaticRenamer/after/main.kt new file mode 100644 index 00000000000..60b7edced3e --- /dev/null +++ b/idea/testData/refactoring/rename/automaticRenamer/after/main.kt @@ -0,0 +1,35 @@ +class Bar : Throwable() + +val bar: Bar = Bar() +val bar1: Bar = Bar() + +val bars: List = listOf() +val foos1: Array = array() + +fun main(args: Array) { + val bar: Bar = Bar() + val someVerySpecialBar: Bar = Bar() + val barAnother: Bar = Bar() + + val (bar1: Bar, bars: List) = Pair(Bar(), listOf()) + + try { + for (bar2: Bar in listOf()) { + + } + } catch (bar: Bar) { + + } + + fun local(bar: Bar) { + + } +} + +fun topLevel(bar: Bar) { + +} + +fun collectionLikes(bars: List>, foos: List>) { + +} diff --git a/idea/testData/refactoring/rename/automaticRenamer/before/lib.kt b/idea/testData/refactoring/rename/automaticRenamer/before/lib.kt new file mode 100644 index 00000000000..f738e66d130 --- /dev/null +++ b/idea/testData/refactoring/rename/automaticRenamer/before/lib.kt @@ -0,0 +1,7 @@ +data class Pair( + public val first: A, + public val second: B +) + + +public fun listOf(): List = throw Error() diff --git a/idea/testData/refactoring/rename/automaticRenamer/before/main.kt b/idea/testData/refactoring/rename/automaticRenamer/before/main.kt new file mode 100644 index 00000000000..2f7e7deaae0 --- /dev/null +++ b/idea/testData/refactoring/rename/automaticRenamer/before/main.kt @@ -0,0 +1,35 @@ +class Foo : Throwable() + +val foo: Foo = Foo() +val foo1: Foo = Foo() + +val foos: List = listOf() +val foos1: Array = array() + +fun main(args: Array) { + val foo: Foo = Foo() + val someVerySpecialFoo: Foo = Foo() + val fooAnother: Foo = Foo() + + val (foo1: Foo, foos: List) = Pair(Foo(), listOf()) + + try { + for (foo2: Foo in listOf()) { + + } + } catch (foo: Foo) { + + } + + fun local(foo: Foo) { + + } +} + +fun topLevel(foo: Foo) { + +} + +fun collectionLikes(foos: List>, foos: List>) { + +} diff --git a/idea/testData/refactoring/rename/automaticRenamer/simple.test b/idea/testData/refactoring/rename/automaticRenamer/simple.test new file mode 100644 index 00000000000..e8e0ddd6633 --- /dev/null +++ b/idea/testData/refactoring/rename/automaticRenamer/simple.test @@ -0,0 +1,7 @@ +{ + "type": "KOTLIN_CLASS", + "classId": "/Foo", + "oldName": "Foo", + "newName": "Bar", + "mainFile": "main.kt" +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/rename/AbstractRenameTest.kt b/idea/tests/org/jetbrains/kotlin/idea/refactoring/rename/AbstractRenameTest.kt index 5227a625e76..1cbd199797f 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/rename/AbstractRenameTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/rename/AbstractRenameTest.kt @@ -16,37 +16,42 @@ package org.jetbrains.kotlin.idea.refactoring.rename -import com.intellij.openapi.util.io.FileUtil -import java.io.File -import org.junit.Assert -import com.google.gson.JsonParser import com.google.gson.JsonObject -import com.intellij.openapi.util.text.StringUtil +import com.google.gson.JsonParser +import com.intellij.openapi.extensions.Extensions import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.refactoring.BaseRefactoringProcessor.ConflictsInTestsException import com.intellij.refactoring.rename.RenameProcessor import com.intellij.refactoring.rename.RenamePsiElementProcessor -import org.jetbrains.kotlin.psi.JetFile -import com.intellij.openapi.vfs.VirtualFile -import org.jetbrains.kotlin.idea.test.PluginTestCaseBase -import com.intellij.psi.search.GlobalSearchScope -import com.intellij.psi.JavaPsiFacade -import com.intellij.openapi.project.Project -import com.intellij.openapi.module.Module -import java.util.Collections +import com.intellij.refactoring.rename.naming.AutomaticRenamerFactory +import com.intellij.refactoring.util.CommonRefactoringUtil.RefactoringErrorHintException import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.DeclarationDescriptor -import org.jetbrains.kotlin.idea.refactoring.move.getString -import org.jetbrains.kotlin.idea.refactoring.move.getNullableString -import org.jetbrains.kotlin.idea.search.allScope -import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils -import com.intellij.refactoring.BaseRefactoringProcessor.ConflictsInTestsException -import com.intellij.refactoring.util.CommonRefactoringUtil.RefactoringErrorHintException -import org.jetbrains.kotlin.idea.test.KotlinMultiFileTestCase import org.jetbrains.kotlin.idea.caches.resolve.analyzeFullyAndGetResult -import org.jetbrains.kotlin.name.* -import org.jetbrains.kotlin.resolve.descriptorUtil.resolveTopLevelClass +import org.jetbrains.kotlin.idea.refactoring.move.getNullableString +import org.jetbrains.kotlin.idea.refactoring.move.getString +import org.jetbrains.kotlin.idea.search.allScope +import org.jetbrains.kotlin.idea.test.KotlinMultiFileTestCase +import org.jetbrains.kotlin.idea.test.PluginTestCaseBase +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqNameUnsafe +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.name.isSubpackageOf +import org.jetbrains.kotlin.psi.JetFile +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils import org.jetbrains.kotlin.serialization.deserialization.findClassAcrossModuleDependencies +import org.junit.Assert +import java.io.File +import java.util.Collections private enum class RenameType { JAVA_CLASS @@ -110,7 +115,7 @@ public abstract class AbstractRenameTest : KotlinMultiFileTestCase() { val aClass = context.javaFacade.findClass(classFQN, context.project.allScope())!! val substitution = RenamePsiElementProcessor.forElement(aClass).substituteElementToRename(aClass, null) - RenameProcessor(context.project, substitution, newName, true, true).run() + runRenameProcessor(context, newName, substitution, true, true) } } @@ -128,7 +133,7 @@ public abstract class AbstractRenameTest : KotlinMultiFileTestCase() { if (method == null) throw IllegalStateException("Method with signature '$methodSignature' wasn't found in class $classFQN") val substitution = RenamePsiElementProcessor.forElement(method).substituteElementToRename(method, null) - RenameProcessor(context.project, substitution, newName, false, false).run() + runRenameProcessor(context, newName, substitution, false, false) } } @@ -173,7 +178,7 @@ public abstract class AbstractRenameTest : KotlinMultiFileTestCase() { val psiElement = segmentReference.resolve()!! val substitution = RenamePsiElementProcessor.forElement(psiElement).substituteElementToRename(psiElement, null) - RenameProcessor(context.project, substitution, newName, true, true).run() + runRenameProcessor(context, newName, substitution, true, true) } } @@ -196,10 +201,22 @@ public abstract class AbstractRenameTest : KotlinMultiFileTestCase() { val substitution = RenamePsiElementProcessor.forElement(psiElement).substituteElementToRename(psiElement, null) - RenameProcessor(context.project, substitution, newName, true, true).run() + runRenameProcessor(context, newName, substitution, true, true) } } + private fun runRenameProcessor( + context: TestContext, + newName: String, + substitution: PsiElement?, + isSearchInComments: Boolean, + isSearchTextOccurrences: Boolean + ) { + val renameProcessor = RenameProcessor(context.project, substitution, newName, isSearchInComments, isSearchTextOccurrences) + Extensions.getExtensions(AutomaticRenamerFactory.EP_NAME).forEach { renameProcessor.addRenamerFactory(it) } + renameProcessor.run() + } + protected fun getTestDirName(lowercaseFirstLetter : Boolean) : String { val testName = getTestName(lowercaseFirstLetter) return testName.substring(0, testName.indexOf('_')) diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/rename/RenameTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/refactoring/rename/RenameTestGenerated.java index ff59c894a69..040cebcf291 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/rename/RenameTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/rename/RenameTestGenerated.java @@ -35,6 +35,12 @@ public class RenameTestGenerated extends AbstractRenameTest { JetTestUtils.assertAllTestsPresentInSingleGeneratedClass(this.getClass(), new File("idea/testData/refactoring/rename"), Pattern.compile("^(.+)\\.test$")); } + @TestMetadata("automaticRenamer/simple.test") + public void testAutomaticRenamer_Simple() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/rename/automaticRenamer/simple.test"); + doTest(fileName); + } + @TestMetadata("defaultObject/defaultObject.test") public void testDefaultObject_DefaultObject() throws Exception { String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/rename/defaultObject/defaultObject.test");