Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[kotlin][idea] Fixed cross-language ChangeAnnotationAction for java annotations used in kotlin code. #2852

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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<KtValueArgument>): Boolean {
val name = attribute.value.getArgumentName()?.asName?.identifier ?: return true
return name == request.name
}

private fun findAttribute(annotationEntry: KtAnnotationEntry, name: String, index: Int): IndexedValue<KtValueArgument>? {
val arguments = annotationEntry.valueArgumentList?.arguments ?: return null
arguments.withIndex().find { (_, argument) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>
)
""".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 {
Expand Down Expand Up @@ -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<JvmModifier>,
Expand Down