diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/KotlinElementActionsFactory.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/KotlinElementActionsFactory.kt index 675d1a50e76..98b2130b6ea 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/KotlinElementActionsFactory.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/KotlinElementActionsFactory.kt @@ -29,6 +29,7 @@ import com.intellij.openapi.util.text.StringUtilRt import com.intellij.psi.* import com.intellij.psi.codeStyle.SuggestedNameInfo import com.intellij.psi.impl.source.tree.java.PsiReferenceExpressionImpl +import com.intellij.psi.util.PropertyUtil import com.intellij.psi.util.PropertyUtilBase import org.jetbrains.kotlin.asJava.classes.KtLightClassForFacade import org.jetbrains.kotlin.asJava.classes.KtLightClassForSourceDeclaration @@ -36,14 +37,14 @@ import org.jetbrains.kotlin.asJava.elements.KtLightElement import org.jetbrains.kotlin.asJava.toLightMethods import org.jetbrains.kotlin.asJava.unwrapped import org.jetbrains.kotlin.caches.resolve.KotlinCacheService -import org.jetbrains.kotlin.descriptors.ClassKind -import org.jetbrains.kotlin.descriptors.Modality -import org.jetbrains.kotlin.descriptors.SourceElement -import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget +import org.jetbrains.kotlin.descriptors.annotations.KotlinTarget import org.jetbrains.kotlin.descriptors.impl.ClassDescriptorImpl import org.jetbrains.kotlin.descriptors.impl.MutablePackageFragmentDescriptor import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny import org.jetbrains.kotlin.idea.core.ShortenReferences import org.jetbrains.kotlin.idea.core.appendModifier import org.jetbrains.kotlin.idea.quickfix.AddModifierFix @@ -52,6 +53,7 @@ import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.* import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createCallable.CreateCallableFromUsageFix import org.jetbrains.kotlin.idea.resolve.ResolutionFacade import org.jetbrains.kotlin.idea.util.approximateFlexibleTypes +import org.jetbrains.kotlin.incremental.components.NoLookupLocation import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.load.java.JvmAbi.JVM_FIELD_ANNOTATION_FQ_NAME import org.jetbrains.kotlin.load.java.components.TypeUsage @@ -70,7 +72,9 @@ import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer import org.jetbrains.kotlin.psi.psiUtil.visibilityModifierType +import org.jetbrains.kotlin.resolve.AnnotationChecker import org.jetbrains.kotlin.resolve.annotations.JVM_STATIC_ANNOTATION_FQ_NAME +import org.jetbrains.kotlin.resolve.descriptorUtil.module import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform import org.jetbrains.kotlin.storage.LockBasedStorageManager import org.jetbrains.kotlin.types.KotlinType @@ -449,11 +453,21 @@ class KotlinElementActionsFactory : JvmElementActionsFactory() { override fun createAddAnnotationActions(target: JvmModifiersOwner, request: AnnotationRequest): List { val declaration = (target as? KtLightElement<*, *>)?.kotlinOrigin as? KtModifierListOwner ?: return emptyList() if (declaration.language != KotlinLanguage.INSTANCE) return emptyList() - return listOf(CreateAnnotationAction(declaration, request)) + val annotationUseSiteTarget = when (target) { + is JvmField -> AnnotationUseSiteTarget.FIELD + is JvmMethod -> when { + PropertyUtil.isSimplePropertySetter(target as? PsiMethod) -> AnnotationUseSiteTarget.PROPERTY_SETTER + PropertyUtil.isSimplePropertyGetter(target as? PsiMethod) -> AnnotationUseSiteTarget.PROPERTY_GETTER + else -> null + } + else -> null + } + return listOf(CreateAnnotationAction(declaration, annotationUseSiteTarget, request)) } private class CreateAnnotationAction( target: KtModifierListOwner, + val annotationTarget: AnnotationUseSiteTarget?, val request: AnnotationRequest ) : IntentionAction { @@ -471,14 +485,30 @@ class KotlinElementActionsFactory : JvmElementActionsFactory() { override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { val target = pointer.element ?: return - val kotlinAnnotation = JavaPsiFacade.getInstance(project).findClass( - request.qualifiedName, - target.resolveScope - )?.language == KotlinLanguage.INSTANCE + val annotationClass = JavaPsiFacade.getInstance(project).findClass(request.qualifiedName, target.resolveScope) + + val kotlinAnnotation = annotationClass?.language == KotlinLanguage.INSTANCE + + val annotationUseSiteTargetPrefix = run prefixEvaluation@{ + if (annotationTarget == null) return@prefixEvaluation "" + + val moduleDescriptor = (target as? KtDeclaration)?.resolveToDescriptorIfAny()?.module ?: return@prefixEvaluation "" + val annotationClassDescriptor = moduleDescriptor.resolveClassByFqName( + FqName(request.qualifiedName), NoLookupLocation.FROM_IDE + ) ?: return@prefixEvaluation "" + + val applicableTargetSet = + AnnotationChecker.applicableTargetSet(annotationClassDescriptor) ?: KotlinTarget.DEFAULT_TARGET_SET + + if (KotlinTarget.PROPERTY !in applicableTargetSet) return@prefixEvaluation "" + + "${annotationTarget.renderName}:" + } + val entry = target.addAnnotationEntry( KtPsiFactory(target) .createAnnotationEntry( - "@${request.qualifiedName}${ + "@$annotationUseSiteTargetPrefix${request.qualifiedName}${ request.attributes.mapIndexed { i, p -> if (!kotlinAnnotation && i == 0 && p.name == "value") renderAttributeValue(p.value).toString() diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/KotlinElementActionsFactory.kt.182 b/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/KotlinElementActionsFactory.kt.182 index 114eeada7e3..f1ab4f338b8 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/KotlinElementActionsFactory.kt.182 +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/KotlinElementActionsFactory.kt.182 @@ -20,10 +20,7 @@ import com.intellij.codeInsight.daemon.QuickFixBundle import com.intellij.codeInsight.intention.IntentionAction import com.intellij.codeInsight.intention.QuickFixFactory import com.intellij.lang.java.beans.PropertyKind -import com.intellij.lang.jvm.JvmClass -import com.intellij.lang.jvm.JvmElement -import com.intellij.lang.jvm.JvmModifier -import com.intellij.lang.jvm.JvmModifiersOwner +import com.intellij.lang.jvm.* import com.intellij.lang.jvm.actions.* import com.intellij.lang.jvm.types.JvmType import com.intellij.openapi.editor.Editor @@ -32,6 +29,7 @@ import com.intellij.openapi.util.text.StringUtilRt import com.intellij.psi.* import com.intellij.psi.codeStyle.SuggestedNameInfo import com.intellij.psi.impl.source.tree.java.PsiReferenceExpressionImpl +import com.intellij.psi.util.PropertyUtil import com.intellij.psi.util.PropertyUtilBase import org.jetbrains.kotlin.asJava.classes.KtLightClassForFacade import org.jetbrains.kotlin.asJava.classes.KtLightClassForSourceDeclaration @@ -39,14 +37,14 @@ import org.jetbrains.kotlin.asJava.elements.KtLightElement import org.jetbrains.kotlin.asJava.toLightMethods import org.jetbrains.kotlin.asJava.unwrapped import org.jetbrains.kotlin.caches.resolve.KotlinCacheService -import org.jetbrains.kotlin.descriptors.ClassKind -import org.jetbrains.kotlin.descriptors.Modality -import org.jetbrains.kotlin.descriptors.SourceElement -import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget +import org.jetbrains.kotlin.descriptors.annotations.KotlinTarget import org.jetbrains.kotlin.descriptors.impl.ClassDescriptorImpl import org.jetbrains.kotlin.descriptors.impl.MutablePackageFragmentDescriptor import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny import org.jetbrains.kotlin.idea.core.ShortenReferences import org.jetbrains.kotlin.idea.core.appendModifier import org.jetbrains.kotlin.idea.quickfix.AddModifierFix @@ -55,6 +53,7 @@ import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.* import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createCallable.CreateCallableFromUsageFix import org.jetbrains.kotlin.idea.resolve.ResolutionFacade import org.jetbrains.kotlin.idea.util.approximateFlexibleTypes +import org.jetbrains.kotlin.incremental.components.NoLookupLocation import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.load.java.JvmAbi.JVM_FIELD_ANNOTATION_FQ_NAME import org.jetbrains.kotlin.load.java.components.TypeUsage @@ -73,7 +72,9 @@ import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer import org.jetbrains.kotlin.psi.psiUtil.visibilityModifierType +import org.jetbrains.kotlin.resolve.AnnotationChecker import org.jetbrains.kotlin.resolve.annotations.JVM_STATIC_ANNOTATION_FQ_NAME +import org.jetbrains.kotlin.resolve.descriptorUtil.module import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform import org.jetbrains.kotlin.storage.LockBasedStorageManager import org.jetbrains.kotlin.types.KotlinType @@ -452,11 +453,21 @@ class KotlinElementActionsFactory : JvmElementActionsFactory() { override fun createAddAnnotationActions(target: JvmModifiersOwner, request: AnnotationRequest): List { val declaration = (target as? KtLightElement<*, *>)?.kotlinOrigin as? KtModifierListOwner ?: return emptyList() if (declaration.language != KotlinLanguage.INSTANCE) return emptyList() - return listOf(CreateAnnotationAction(declaration, request)) + val annotationUseSiteTarget = when (target) { + is JvmField -> AnnotationUseSiteTarget.FIELD + is JvmMethod -> when { + PropertyUtil.isSimplePropertySetter(target as? PsiMethod) -> AnnotationUseSiteTarget.PROPERTY_SETTER + PropertyUtil.isSimplePropertyGetter(target as? PsiMethod) -> AnnotationUseSiteTarget.PROPERTY_GETTER + else -> null + } + else -> null + } + return listOf(CreateAnnotationAction(declaration, annotationUseSiteTarget, request)) } private class CreateAnnotationAction( target: KtModifierListOwner, + val annotationTarget: AnnotationUseSiteTarget?, val request: AnnotationRequest ) : IntentionAction { @@ -474,14 +485,29 @@ class KotlinElementActionsFactory : JvmElementActionsFactory() { override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { val target = pointer.element ?: return - val kotlinAnnotation = JavaPsiFacade.getInstance(project).findClass( - request.qualifiedName, - target.resolveScope - )?.language == KotlinLanguage.INSTANCE + val annotationClass = JavaPsiFacade.getInstance(project).findClass(request.qualifiedName, target.resolveScope) + + val kotlinAnnotation = annotationClass?.language == KotlinLanguage.INSTANCE + + val annotationUseSiteTargetPrefix = run prefixEvaluation@{ + if (annotationTarget == null) return@prefixEvaluation "" + + val moduleDescriptor = (target as? KtDeclaration)?.resolveToDescriptorIfAny()?.module ?: return@prefixEvaluation "" + val annotationClassDescriptor = moduleDescriptor.resolveClassByFqName( + FqName(request.qualifiedName), NoLookupLocation.FROM_IDE + ) ?: return@prefixEvaluation "" + + val applicableTargetSet = AnnotationChecker.applicableTargetSet(annotationClassDescriptor) ?: KotlinTarget.DEFAULT_TARGET_SET + + if (KotlinTarget.PROPERTY !in applicableTargetSet) return@prefixEvaluation "" + + "${annotationTarget.renderName}:" + } + val entry = target.addAnnotationEntry( KtPsiFactory(target) .createAnnotationEntry( - "@${request.qualifiedName}${ + "@$annotationUseSiteTargetPrefix${request.qualifiedName}${ request.attributes.mapIndexed { i, p -> if (!kotlinAnnotation && i == 0 && p.name == "value") renderAttributeValue(p.value).toString() diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt b/idea/tests/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt index 393aadbf62c..d36e7a8f882 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt @@ -8,7 +8,6 @@ package org.jetbrains.kotlin.idea.quickfix import com.intellij.codeInsight.intention.IntentionAction import com.intellij.lang.jvm.JvmClass import com.intellij.lang.jvm.JvmElement -import com.intellij.lang.jvm.JvmMethod import com.intellij.lang.jvm.JvmModifier import com.intellij.lang.jvm.actions.* import com.intellij.lang.jvm.types.JvmSubstitutor @@ -19,6 +18,7 @@ import com.intellij.psi.* import com.intellij.psi.codeStyle.SuggestedNameInfo import com.intellij.testFramework.fixtures.CodeInsightTestFixture import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase +import junit.framework.TestCase import org.jetbrains.kotlin.asJava.toLightElements import org.jetbrains.kotlin.idea.search.allScope import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor @@ -240,6 +240,113 @@ class CommonIntentionActionsTest : LightPlatformCodeInsightFixtureTestCase() { ) } + + fun testAddJavaAnnotationOnFieldWithoutTarget() { + + myFixture.addFileToProject( + "pkg/myannotation/JavaAnnotation.java", """ + package pkg.myannotation + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + //no @Target + @Retention(RetentionPolicy.RUNTIME) + public @interface JavaAnnotation { + String value(); + int param() default 0; + } + """.trimIndent() + ) + + myFixture.configureByText( + "foo.kt", """class Foo { + | val bar: String = null + |}""".trim().trimMargin() + ) + + myFixture.launchAction( + createAddAnnotationActions( + myFixture.findElementByText("bar", KtModifierListOwner::class.java).toLightElements().single { it is PsiField } as PsiField, + annotationRequest("pkg.myannotation.JavaAnnotation") + ).single() + ) + + myFixture.checkResult( + """ + import pkg.myannotation.JavaAnnotation + + class Foo { + @field:JavaAnnotation() + val bar: String = null + } + """.trimIndent(), true + ) + + TestCase.assertEquals( + "KtLightMethodImpl -> org.jetbrains.annotations.NotNull," + + " KtLightFieldForDeclaration -> pkg.myannotation.JavaAnnotation, org.jetbrains.annotations.NotNull", + annotationsString(myFixture.findElementByText("bar", KtModifierListOwner::class.java)) + ) + } + + + fun testAddJavaAnnotationOnField() { + + myFixture.addFileToProject( + "pkg/myannotation/JavaAnnotation.java", """ + package pkg.myannotation + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + public @interface JavaAnnotation { + String value(); + int param() default 0; + } + """.trimIndent() + ) + + myFixture.configureByText( + "foo.kt", """class Foo { + | val bar: String = null + |}""".trim().trimMargin() + ) + + myFixture.launchAction( + createAddAnnotationActions( + myFixture.findElementByText("bar", KtModifierListOwner::class.java).toLightElements().single { it is PsiField } as PsiField, + annotationRequest("pkg.myannotation.JavaAnnotation") + ).single() + ) + + myFixture.checkResult( + """ + import pkg.myannotation.JavaAnnotation + + class Foo { + @JavaAnnotation() + val bar: String = null + } + """.trimIndent(), true + ) + + TestCase.assertEquals( + "KtLightMethodImpl -> org.jetbrains.annotations.NotNull," + + " KtLightFieldForDeclaration -> pkg.myannotation.JavaAnnotation, org.jetbrains.annotations.NotNull", + annotationsString(myFixture.findElementByText("bar", KtModifierListOwner::class.java)) + ) + } + + private fun annotationsString(findElementByText: KtModifierListOwner) = findElementByText.toLightElements() + .joinToString { elem -> "${elem.javaClass.simpleName} -> ${(elem as PsiModifierListOwner).annotations.joinToString { it.qualifiedName!! }}" } + fun testDontMakePublicPublic() { myFixture.configureByText( "foo.kt", """class Foo { diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt.182 b/idea/tests/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt.182 index 4f58da289fd..09b180047a6 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt.182 +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt.182 @@ -13,13 +13,11 @@ import com.intellij.lang.jvm.actions.* import com.intellij.lang.jvm.types.JvmSubstitutor import com.intellij.openapi.project.Project import com.intellij.openapi.util.Pair.pair -import com.intellij.psi.PsiJvmSubstitutor -import com.intellij.psi.PsiMethod -import com.intellij.psi.PsiSubstitutor -import com.intellij.psi.PsiType +import com.intellij.psi.* import com.intellij.psi.codeStyle.SuggestedNameInfo import com.intellij.testFramework.fixtures.CodeInsightTestFixture import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase +import junit.framework.TestCase import org.jetbrains.kotlin.asJava.toLightElements import org.jetbrains.kotlin.idea.search.allScope import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor @@ -238,6 +236,113 @@ class CommonIntentionActionsTest : LightPlatformCodeInsightFixtureTestCase() { ) } + + fun testAddJavaAnnotationOnFieldWithoutTarget() { + + myFixture.addFileToProject( + "pkg/myannotation/JavaAnnotation.java", """ + package pkg.myannotation + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + //no @Target + @Retention(RetentionPolicy.RUNTIME) + public @interface JavaAnnotation { + String value(); + int param() default 0; + } + """.trimIndent() + ) + + myFixture.configureByText( + "foo.kt", """class Foo { + | val bar: String = null + |}""".trim().trimMargin() + ) + + myFixture.launchAction( + createAddAnnotationActions( + myFixture.findElementByText("bar", KtModifierListOwner::class.java).toLightElements().single { it is PsiField } as PsiField, + annotationRequest("pkg.myannotation.JavaAnnotation") + ).single() + ) + + myFixture.checkResult( + """ + import pkg.myannotation.JavaAnnotation + + class Foo { + @field:JavaAnnotation() + val bar: String = null + } + """.trimIndent(), true + ) + + TestCase.assertEquals( + "KtLightMethodImpl -> org.jetbrains.annotations.NotNull," + + " KtLightFieldForDeclaration -> pkg.myannotation.JavaAnnotation, org.jetbrains.annotations.NotNull", + annotationsString(myFixture.findElementByText("bar", KtModifierListOwner::class.java)) + ) + } + + + fun testAddJavaAnnotationOnField() { + + myFixture.addFileToProject( + "pkg/myannotation/JavaAnnotation.java", """ + package pkg.myannotation + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + public @interface JavaAnnotation { + String value(); + int param() default 0; + } + """.trimIndent() + ) + + myFixture.configureByText( + "foo.kt", """class Foo { + | val bar: String = null + |}""".trim().trimMargin() + ) + + myFixture.launchAction( + createAddAnnotationActions( + myFixture.findElementByText("bar", KtModifierListOwner::class.java).toLightElements().single { it is PsiField } as PsiField, + annotationRequest("pkg.myannotation.JavaAnnotation") + ).single() + ) + + myFixture.checkResult( + """ + import pkg.myannotation.JavaAnnotation + + class Foo { + @JavaAnnotation() + val bar: String = null + } + """.trimIndent(), true + ) + + TestCase.assertEquals( + "KtLightMethodImpl -> org.jetbrains.annotations.NotNull," + + " KtLightFieldForDeclaration -> pkg.myannotation.JavaAnnotation, org.jetbrains.annotations.NotNull", + annotationsString(myFixture.findElementByText("bar", KtModifierListOwner::class.java)) + ) + } + + private fun annotationsString(findElementByText: KtModifierListOwner) = findElementByText.toLightElements() + .joinToString { elem -> "${elem.javaClass.simpleName} -> ${(elem as PsiModifierListOwner).annotations.joinToString { it.qualifiedName!! }}" } + fun testDontMakePublicPublic() { myFixture.configureByText( "foo.kt", """class Foo {