diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/KotlinElementActionsFactory.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/KotlinElementActionsFactory.kt index ae8e9bd4bb6de..6adc58773f8a4 100644 --- a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/KotlinElementActionsFactory.kt +++ b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/KotlinElementActionsFactory.kt @@ -9,6 +9,7 @@ import com.intellij.codeInsight.intention.QuickFixFactory import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo import com.intellij.codeInspection.util.IntentionFamilyName import com.intellij.codeInspection.util.IntentionName +import com.intellij.lang.java.JavaLanguage import com.intellij.lang.java.beans.PropertyKind import com.intellij.lang.jvm.* import com.intellij.lang.jvm.actions.* @@ -478,27 +479,90 @@ class KotlinElementActionsFactory : JvmElementActionsFactory() { private fun invokeImpl(annotationEntry: KtAnnotationEntry, project: Project) { val facade = JavaPsiFacade.getInstance(annotationEntry.project) - val isKotlinAnnotation = facade.findClass(qualifiedName, annotationEntry.resolveScope)?.language == KotlinLanguage.INSTANCE + val language = facade.findClass(qualifiedName, annotationEntry.resolveScope)?.language val dummyAnnotationRequest = annotationRequest(qualifiedName, request) val psiFactory = KtPsiFactory(project) - val annotationText = '@' + renderAnnotation(dummyAnnotationRequest, psiFactory) { isKotlinAnnotation } - val dummyArgumentList = psiFactory.createAnnotationEntry(annotationText).valueArgumentList!! + val dummyAnnotationText = '@' + renderAnnotation(dummyAnnotationRequest, psiFactory) { language == KotlinLanguage.INSTANCE } + val dummyArgumentList = psiFactory.createAnnotationEntry(dummyAnnotationText).valueArgumentList!! val argumentList = annotationEntry.valueArgumentList + if (argumentList == null) { annotationEntry.add(dummyArgumentList) - } else { - val dummyArgument = dummyArgumentList.arguments[0] - val attribute = findAttribute(annotationEntry, request.name, attributeIndex) - if (attribute != null) { - argumentList.addArgumentBefore(dummyArgument, attribute.value) - argumentList.removeArgument(attribute.index + 1) - } else { - argumentList.addArgument(dummyArgument) - } + ShortenReferences.DEFAULT.process(annotationEntry) + return + } + + when (language) { + JavaLanguage.INSTANCE -> changeJava(annotationEntry, argumentList, dummyArgumentList) + KotlinLanguage.INSTANCE -> changeKotlin(annotationEntry, argumentList, dummyArgumentList) + else -> changeKotlin(annotationEntry, argumentList, dummyArgumentList) } ShortenReferences.DEFAULT.process(annotationEntry) } + private fun changeKotlin(annotationEntry: KtAnnotationEntry, argumentList: KtValueArgumentList, dummyArgumentList: KtValueArgumentList) { + val dummyArgument = dummyArgumentList.arguments[0] + val oldAttribute = findAttribute(annotationEntry, request.name, attributeIndex) + if (oldAttribute == null) { + argumentList.addArgument(dummyArgument) + return + } + + argumentList.addArgumentBefore(dummyArgument, oldAttribute.value) + + if (isAttributeDuplicated(oldAttribute)) { + argumentList.removeArgument(oldAttribute.value) + } + } + + private fun changeJava(annotationEntry: KtAnnotationEntry, argumentList: KtValueArgumentList, dummyArgumentList: KtValueArgumentList) { + if (request.name == "value") { + val anchorAfterVarargs: KtValueArgument? = removeVarargsAttribute(argumentList) + + for (renderedArgument in dummyArgumentList.arguments) { + argumentList.addArgumentBefore(renderedArgument, anchorAfterVarargs) + } + + return + } + + val oldAttribute = findAttribute(annotationEntry, request.name, attributeIndex) + if (oldAttribute != null) { + for (dummyArgument in dummyArgumentList.arguments) { + argumentList.addArgumentBefore(dummyArgument, oldAttribute.value) + } + + if (isAttributeDuplicated(oldAttribute)) { + argumentList.removeArgument(oldAttribute.value) + } + return + } + + for (dummyArgument in dummyArgumentList.arguments) { + argumentList.addArgument(dummyArgument) + } + } + + private fun removeVarargsAttribute(argumentList: KtValueArgumentList): KtValueArgument? { + for (attribute in argumentList.arguments) { + val attributeName = attribute.getArgumentName()?.asName?.identifier + + if (attributeName == null || attributeName == "value") { + argumentList.removeArgument(attribute) + continue + } + + return attribute + } + + return null + } + + private fun isAttributeDuplicated(attribute: IndexedValue): Boolean { + val name = attribute.value.getArgumentName()?.asName?.identifier ?: return true + return name == request.name + } + private fun findAttribute(annotationEntry: KtAnnotationEntry, name: String, index: Int): IndexedValue? { val arguments = annotationEntry.valueArgumentList?.arguments ?: return null arguments.withIndex().find { (_, argument) -> diff --git a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt index c546ec3e7f400..0b137dd949195 100644 --- a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt +++ b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt @@ -497,6 +497,249 @@ class CommonIntentionActionsTest : BasePlatformTestCase() { ) } + fun testChangeAnnotationAttributeActionsWithJavaAnnotation() { + myFixture.addJavaFileToProject( + "pkg/myannotation/JavaAnnotation.java", """ + package pkg.myannotation; + + public @interface JavaAnnotation { + String[] value(); + String description() default ""; + } + """.trimIndent() + ) + + myFixture.configureByText( + "foo.kt", """import pkg.myannotation.JavaAnnotation + | + |class Foo { + | @JavaAnnotation + | fun empty(){} + | @JavaAnnotation(description = "stay") + | fun emptyWithDescription(){} + | + | @JavaAnnotation("before") + | fun single(){} + | @JavaAnnotation(value = ["before"]) + | fun singleExplicit(){} + | + | @JavaAnnotation("before", "before") + | fun multiple(){} + | @JavaAnnotation(value = ["before", "before", "before"]) + | fun multipleExplicit(){} + | + | @JavaAnnotation("before", "before", description = "stay") + | fun multipleDescription(){} + | @JavaAnnotation(value = ["before", "before", "before"], description = "stay") + | fun multipleDescriptionExplicit(){} + |}""".trim().trimMargin(), + ) + + myFixture.launchAction( + createChangeAnnotationAttributeActions( + myFixture.findMethod("empty").annotations.single(), + 0, + arrayAttribute("value", listOf(StringValue("after1"), StringValue("after2"), StringValue("after3"))), + "Change annotation attribute", "Change annotation attribute" + ).single() + ) + myFixture.launchAction( + createChangeAnnotationAttributeActions( + myFixture.findMethod("emptyWithDescription").annotations.single(), + 0, + arrayAttribute("value", listOf(StringValue("after1"), StringValue("after2"), StringValue("after3"))), + "Change annotation attribute", "Change annotation attribute" + ).single() + ) + + myFixture.launchAction( + createChangeAnnotationAttributeActions( + myFixture.findMethod("single").annotations.single(), + 0, + arrayAttribute("value", listOf(StringValue("after1"), StringValue("after2"), StringValue("after3"))), + "Change annotation attribute", "Change annotation attribute" + ).single() + ) + myFixture.launchAction( + createChangeAnnotationAttributeActions( + myFixture.findMethod("singleExplicit").annotations.single(), + 0, + arrayAttribute("value", listOf(StringValue("after1"), StringValue("after2"), StringValue("after3"))), + "Change annotation attribute", "Change annotation attribute" + ).single() + ) + + myFixture.launchAction( + createChangeAnnotationAttributeActions( + myFixture.findMethod("multiple").annotations.single(), + 0, + arrayAttribute("value", listOf(StringValue("after1"), StringValue("after2"), StringValue("after3"))), + "Change annotation attribute", "Change annotation attribute" + ).single() + ) + + myFixture.launchAction( + createChangeAnnotationAttributeActions( + myFixture.findMethod("multipleExplicit").annotations.single(), + 0, + arrayAttribute("value", listOf(StringValue("during"), StringValue("during"), StringValue("during"))), + "Change annotation attribute", "Change annotation attribute" + ).single() + ) + + myFixture.launchAction( + createChangeAnnotationAttributeActions( + myFixture.findMethod("multipleExplicit").annotations.single(), + 0, + arrayAttribute("value", listOf(StringValue("after1"), StringValue("after2"))), + "Change annotation attribute", "Change annotation attribute" + ).single() + ) + + myFixture.launchAction( + createChangeAnnotationAttributeActions( + myFixture.findMethod("multipleDescription").annotations.single(), + 0, + arrayAttribute("value", listOf(StringValue("after1"), StringValue("after2"), StringValue("after3"))), + "Change annotation attribute", "Change annotation attribute" + ).single() + ) + + myFixture.launchAction( + createChangeAnnotationAttributeActions( + myFixture.findMethod("multipleDescriptionExplicit").annotations.single(), + 0, + arrayAttribute("value", listOf(StringValue("during"), StringValue("during"), StringValue("during"))), + "Change annotation attribute", "Change annotation attribute" + ).single() + ) + myFixture.launchAction( + createChangeAnnotationAttributeActions( + myFixture.findMethod("multipleDescriptionExplicit").annotations.single(), + 0, + arrayAttribute("value", listOf(StringValue("after1"), StringValue("after2"))), + "Change annotation attribute", "Change annotation attribute" + ).single() + ) + + myFixture.checkResult( + """import pkg.myannotation.JavaAnnotation + | + |class Foo { + | @JavaAnnotation("after1", "after2", "after3") + | fun empty(){} + | @JavaAnnotation("after1", "after2", "after3", description = "stay") + | fun emptyWithDescription(){} + | + | @JavaAnnotation("after1", "after2", "after3") + | fun single(){} + | @JavaAnnotation("after1", "after2", "after3") + | fun singleExplicit(){} + | + | @JavaAnnotation("after1", "after2", "after3") + | fun multiple(){} + | @JavaAnnotation("after1", "after2") + | fun multipleExplicit(){} + | + | @JavaAnnotation("after1", "after2", "after3", description = "stay") + | fun multipleDescription(){} + | @JavaAnnotation("after1", "after2", description = "stay") + | fun multipleDescriptionExplicit(){} + |}""".trim().trimMargin(), true + ) + } + + fun testChangeAnnotationAttributeActionsWithKotlinAnnotation() { + myFixture.addKotlinFileToProject( + "pkg/myannotation/KotlinAnnotation.kt", """ + package pkg.myannotation + + annotation class KotlinAnnotation( + val value: Array + ) + """.trimIndent() + ) + + myFixture.configureByText( + "foo.kt", """import pkg.myannotation.KotlinAnnotation + | + |class Foo { + | @KotlinAnnotation(["foo1"]) + | fun single(){} + | @KotlinAnnotation(value = ["foo1"]) + | fun singleExplicit(){} + | @KotlinAnnotation(["foo1", "foo2"]) + | fun multiple(){} + | @KotlinAnnotation(value = ["foo1", "foo2", "foo3"]) + | fun multipleExplicit(){} + |}""".trim().trimMargin(), + ) + + myFixture.launchAction( + createChangeAnnotationAttributeActions( + myFixture.findMethod("single").annotations.single(), + 0, + arrayAttribute("value", listOf(StringValue("foo1"), StringValue("foo2"), StringValue("foo3"))), + "Change 'value' attribute of 'JavaAnnotation' annotation", + "Change annotation attribute" + ).single() + ) + myFixture.launchAction( + createChangeAnnotationAttributeActions( + myFixture.findMethod("singleExplicit").annotations.single(), + 0, + arrayAttribute("value", listOf(StringValue("foo1"), StringValue("foo2"), StringValue("foo3"))), + "Change 'value' attribute of 'JavaAnnotation' annotation", + "Change annotation attribute" + ).single() + ) + + myFixture.launchAction( + createChangeAnnotationAttributeActions( + myFixture.findMethod("multiple").annotations.single(), + 0, + arrayAttribute("value", listOf(StringValue("foo1"), StringValue("foo2"), StringValue("foo3"))), + "Change 'value' attribute of 'JavaAnnotation' annotation", + "Change annotation attribute" + ).single() + ) + + myFixture.launchAction( + createChangeAnnotationAttributeActions( + myFixture.findMethod("multipleExplicit").annotations.single(), + 0, + arrayAttribute("value", listOf(StringValue("foo1"), StringValue("foo2"))), + "Change 'value' attribute of 'JavaAnnotation' annotation", + "Change annotation attribute" + ).single() + ) + + myFixture.launchAction( + createChangeAnnotationAttributeActions( + myFixture.findMethod("multipleExplicit").annotations.single(), + 0, + arrayAttribute("value", listOf(StringValue("foo1"), StringValue("foo2"), StringValue("foo3"))), + "Change 'value' attribute of 'JavaAnnotation' annotation", + "Change annotation attribute" + ).single() + ) + + myFixture.checkResult( + """import pkg.myannotation.KotlinAnnotation + | + |class Foo { + | @KotlinAnnotation(value = ["foo1", "foo2", "foo3"]) + | fun single(){} + | @KotlinAnnotation(value = ["foo1", "foo2", "foo3"]) + | fun singleExplicit(){} + | @KotlinAnnotation(value = ["foo1", "foo2", "foo3"]) + | fun multiple(){} + | @KotlinAnnotation(value = ["foo1", "foo2", "foo3"]) + | fun multipleExplicit(){} + |}""".trim().trimMargin(), true + ) + } + fun testChangeMethodType() { myFixture.configureByText( "foo.kt", """class Foo { @@ -1181,6 +1424,9 @@ class CommonIntentionActionsTest : BasePlatformTestCase() { private fun expectedParams(vararg psyTypes: PsiType) = psyTypes.mapIndexed { index, psiType -> expectedParameter(expectedTypes(psiType), "param$index") } + private fun CodeInsightTestFixture.findMethod(name: String): JvmMethod = + this.findElementByText(name, KtModifierListOwner::class.java).toLightElements().single() as JvmMethod + class FieldRequest( private val project: Project, val modifiers: List,