diff --git a/java/java-analysis-api/src/com/intellij/lang/jvm/actions/JvmElementActionsFactory.kt b/java/java-analysis-api/src/com/intellij/lang/jvm/actions/JvmElementActionsFactory.kt index 82cf2e45359b..bda7ef684d62 100644 --- a/java/java-analysis-api/src/com/intellij/lang/jvm/actions/JvmElementActionsFactory.kt +++ b/java/java-analysis-api/src/com/intellij/lang/jvm/actions/JvmElementActionsFactory.kt @@ -2,10 +2,7 @@ package com.intellij.lang.jvm.actions import com.intellij.codeInsight.intention.IntentionAction -import com.intellij.lang.jvm.JvmClass -import com.intellij.lang.jvm.JvmMethod -import com.intellij.lang.jvm.JvmModifiersOwner -import com.intellij.lang.jvm.JvmParameter +import com.intellij.lang.jvm.* /** * This extension point provides language-abstracted code modifications for JVM-based languages. @@ -24,6 +21,10 @@ abstract class JvmElementActionsFactory { open fun createAddAnnotationActions(target: JvmModifiersOwner, request: AnnotationRequest): List = emptyList() + open fun createChangeAnnotationAttributeActions(annotation: JvmAnnotation, + attributeIndex: Int, + request: AnnotationAttributeRequest): List = emptyList() + open fun createAddFieldActions(targetClass: JvmClass, request: CreateFieldRequest): List = emptyList() open fun createAddMethodActions(targetClass: JvmClass, request: CreateMethodRequest): List = emptyList() diff --git a/java/java-analysis-api/src/com/intellij/lang/jvm/actions/actions.kt b/java/java-analysis-api/src/com/intellij/lang/jvm/actions/actions.kt index 346029ac22d1..5751f834fe03 100644 --- a/java/java-analysis-api/src/com/intellij/lang/jvm/actions/actions.kt +++ b/java/java-analysis-api/src/com/intellij/lang/jvm/actions/actions.kt @@ -4,10 +4,7 @@ package com.intellij.lang.jvm.actions import com.intellij.codeInsight.intention.IntentionAction -import com.intellij.lang.jvm.JvmClass -import com.intellij.lang.jvm.JvmMethod -import com.intellij.lang.jvm.JvmModifiersOwner -import com.intellij.lang.jvm.JvmParameter +import com.intellij.lang.jvm.* import com.intellij.openapi.extensions.ExtensionPointName val EP_NAME: ExtensionPointName = ExtensionPointName.create( @@ -38,6 +35,14 @@ fun createAddAnnotationActions(target: JvmModifiersOwner, request: AnnotationReq } } +fun createChangeAnnotationAttributeActions(annotation: JvmAnnotation, + attributeIndex: Int, + request: AnnotationAttributeRequest): List { + return createActions { + it.createChangeAnnotationAttributeActions(annotation, attributeIndex, request) + } +} + fun createModifierActions(target: JvmModifiersOwner, request: ChangeModifierRequest): List { return createActions { it.createChangeModifierActions(target, request) diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/intention/AddAnnotationPsiFix.java b/java/java-analysis-impl/src/com/intellij/codeInsight/intention/AddAnnotationPsiFix.java index 0bc8e67f969f..51f3269691df 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/intention/AddAnnotationPsiFix.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/intention/AddAnnotationPsiFix.java @@ -35,6 +35,9 @@ import org.jetbrains.annotations.Nullable; import java.lang.annotation.RetentionPolicy; import java.util.List; +import static com.intellij.codeInsight.AnnotationUtil.CHECK_EXTERNAL; +import static com.intellij.codeInsight.AnnotationUtil.CHECK_TYPE; + public class AddAnnotationPsiFix extends LocalQuickFixOnPsiElement implements LocalQuickFix { protected final String myAnnotation; final String[] myAnnotationsToRemove; @@ -166,7 +169,8 @@ public class AddAnnotationPsiFix extends LocalQuickFixOnPsiElement implements Lo PsiModifierList modifierList = modifierListOwner.getModifierList(); return modifierList != null && !(modifierList instanceof LightElement) - && !(modifierListOwner instanceof LightElement); + && !(modifierListOwner instanceof LightElement) + && !AnnotationUtil.isAnnotated(modifierListOwner, annotationFQN, CHECK_EXTERNAL | CHECK_TYPE); } @Override diff --git a/java/java-analysis-impl/src/messages/QuickFixBundle.properties b/java/java-analysis-impl/src/messages/QuickFixBundle.properties index 6c6a0a22c827..78506dfb1604 100644 --- a/java/java-analysis-impl/src/messages/QuickFixBundle.properties +++ b/java/java-analysis-impl/src/messages/QuickFixBundle.properties @@ -448,4 +448,7 @@ qualify.method.call.fix=Qualify the call with ''{0}'' qualify.method.call.family=Qualify method call remove.redundant.nested.patterns.fix.text=Remove redundant nested pattern{0, choice, 1#|2#s} -add.missing.nested.patterns.fix.text=Add missing nested pattern{0, choice, 1#|2#s} \ No newline at end of file +add.missing.nested.patterns.fix.text=Add missing nested pattern{0, choice, 1#|2#s} + +change.annotation.attribute.value.family=Change annotation attribute +change.annotation.attribute.value.text=Change ''{0}'' annotation attribute \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/codeInsight/intention/impl/JavaElementActionsFactory.kt b/java/java-impl/src/com/intellij/codeInsight/intention/impl/JavaElementActionsFactory.kt index 551749f6156b..d0da10f33179 100644 --- a/java/java-impl/src/com/intellij/codeInsight/intention/impl/JavaElementActionsFactory.kt +++ b/java/java-impl/src/com/intellij/codeInsight/intention/impl/JavaElementActionsFactory.kt @@ -85,6 +85,14 @@ class JavaElementActionsFactory : JvmElementActionsFactory() { return listOf(CreateAnnotationAction(declaration, request)) } + override fun createChangeAnnotationAttributeActions(annotation: JvmAnnotation, + attributeIndex: Int, + request: AnnotationAttributeRequest): List { + val psiAnnotation = annotation as? PsiAnnotation ?: return emptyList() + if (psiAnnotation.language != JavaLanguage.INSTANCE) return emptyList() + return listOf(ChangeAnnotationAttributeAction(psiAnnotation, request)) + } + override fun createAddFieldActions(targetClass: JvmClass, request: CreateFieldRequest): List { val javaClass = targetClass.toJavaClassOrNull() ?: return emptyList() diff --git a/java/java-impl/src/com/intellij/lang/java/actions/ChangeAnnotationAttributeAction.kt b/java/java-impl/src/com/intellij/lang/java/actions/ChangeAnnotationAttributeAction.kt new file mode 100644 index 000000000000..0b3e4a35875b --- /dev/null +++ b/java/java-impl/src/com/intellij/lang/java/actions/ChangeAnnotationAttributeAction.kt @@ -0,0 +1,40 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.lang.java.actions + +import com.intellij.codeInsight.daemon.QuickFixBundle +import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement +import com.intellij.lang.jvm.actions.AnnotationAttributeRequest +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementFactory +import com.intellij.psi.PsiFile +import com.intellij.psi.util.PsiTreeUtil + +internal class ChangeAnnotationAttributeAction(annotation: PsiAnnotation, val request: AnnotationAttributeRequest) : + LocalQuickFixAndIntentionActionOnPsiElement(annotation) { + + override fun startInWriteAction(): Boolean = true + + override fun getFamilyName(): String = QuickFixBundle.message("change.annotation.attribute.value.family") + + override fun getText(): String = QuickFixBundle.message("change.annotation.attribute.value.text", request.name) + + override fun generatePreview(project: Project, editor: Editor, file: PsiFile): IntentionPreviewInfo { + val copy = PsiTreeUtil.findSameElementInCopy(startElement as PsiAnnotation, file) + invokeImpl(copy, project) + return IntentionPreviewInfo.DIFF + } + + override fun invoke(project: Project, file: PsiFile, editor: Editor?, startElement: PsiElement, endElement: PsiElement) { + invokeImpl(startElement as PsiAnnotation, project) + } + + private fun invokeImpl(annotation: PsiAnnotation, project: Project) { + val factory = PsiElementFactory.getInstance(project) + val value = CreateAnnotationAction.attributeRequestToValue(request.value, factory, null) + annotation.setDeclaredAttributeValue(request.name, value) + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/lang/java/actions/CreateAnnotationAction.kt b/java/java-impl/src/com/intellij/lang/java/actions/CreateAnnotationAction.kt index 952db7f5c7e3..f5fc89c0d574 100644 --- a/java/java-impl/src/com/intellij/lang/java/actions/CreateAnnotationAction.kt +++ b/java/java-impl/src/com/intellij/lang/java/actions/CreateAnnotationAction.kt @@ -69,15 +69,14 @@ internal class CreateAnnotationAction(target: PsiModifierListOwner, override val psiElementFactory: PsiElementFactory, context: PsiElement?) { for ((name, value) in annotationRequest.attributes) { - val memberValue = attributeRequestToValue(value, psiElementFactory, context, annotationRequest) - annotation.setDeclaredAttributeValue(name.takeIf { name != "value" }, memberValue) + val memberValue = attributeRequestToValue(value, psiElementFactory, context) + annotation.setDeclaredAttributeValue(name.takeIf { name != PsiAnnotation.DEFAULT_REFERENCED_METHOD_NAME }, memberValue) } } - private fun attributeRequestToValue(value: AnnotationAttributeValueRequest, - psiElementFactory: PsiElementFactory, - context: PsiElement?, - annotationRequest: AnnotationRequest): PsiAnnotationMemberValue? = when (value) { + internal fun attributeRequestToValue(value: AnnotationAttributeValueRequest, + psiElementFactory: PsiElementFactory, + context: PsiElement?): PsiAnnotationMemberValue? = when (value) { is AnnotationAttributeValueRequest.PrimitiveValue -> psiElementFactory .createExpressionFromText(value.value.toString(), null) is AnnotationAttributeValueRequest.StringValue -> psiElementFactory @@ -92,7 +91,7 @@ internal class CreateAnnotationAction(target: PsiModifierListOwner, override val } is AnnotationAttributeValueRequest.ArrayValue -> { val arrayExpressionText = value.members.joinToString { - attributeRequestToValue(it, psiElementFactory, context, annotationRequest)?.text ?: "" + attributeRequestToValue(it, psiElementFactory, context)?.text ?: "" } val dummyAnnotation = psiElementFactory.createAnnotationFromText("@dummy({$arrayExpressionText})", context) dummyAnnotation.findAttributeValue(null) diff --git a/plugins/devkit/devkit-core/src/inspections/MismatchedLightServiceLevelAndCtorInspection.kt b/plugins/devkit/devkit-core/src/inspections/MismatchedLightServiceLevelAndCtorInspection.kt index ab6ca80dcf00..d857688bcfcf 100644 --- a/plugins/devkit/devkit-core/src/inspections/MismatchedLightServiceLevelAndCtorInspection.kt +++ b/plugins/devkit/devkit-core/src/inspections/MismatchedLightServiceLevelAndCtorInspection.kt @@ -9,6 +9,7 @@ import com.intellij.lang.jvm.annotation.JvmAnnotationConstantValue import com.intellij.lang.jvm.annotation.JvmAnnotationEnumFieldValue import com.intellij.openapi.components.Service import com.intellij.openapi.project.Project +import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiAnnotation.DEFAULT_REFERENCED_METHOD_NAME import com.intellij.psi.PsiElementFactory import com.intellij.psi.PsiFile @@ -27,17 +28,20 @@ internal class MismatchedLightServiceLevelAndCtorInspection : DevKitJvmInspectio if (!method.isConstructor) return true val file: PsiFile = method.sourceElement?.containingFile ?: return true val containingClass = method.containingClass ?: return true - val serviceAnnotation = containingClass.annotations.find { it.qualifiedName == Service::class.java.canonicalName } ?: return true - val level = getLevel(serviceAnnotation) - if (level !in listOf(Level.PROJECT, Level.APP_AND_PROJECT)) { + val annotation = containingClass.annotations.find { it.qualifiedName == Service::class.java.canonicalName } ?: return true + val annotationName = (annotation as? PsiAnnotation)?.nameReferenceElement + val level = getLevel(annotation) + if (annotationName != null && level !in listOf(Level.PROJECT, Level.APP_AND_PROJECT)) { val isProjectParamCtor = (method.parameters.singleOrNull()?.type as? PsiType)?.canonicalText == Project::class.java.canonicalName if (isProjectParamCtor) { val projectLevelFqn = "${Service.Level::class.java.canonicalName}.${Service.Level.PROJECT}" - val request = annotationRequest(Service::class.java.canonicalName, - constantAttribute(DEFAULT_REFERENCED_METHOD_NAME, projectLevelFqn)) - val actions = createAddAnnotationActions(containingClass, request) + val request = constantAttribute(DEFAULT_REFERENCED_METHOD_NAME, projectLevelFqn) + val actions = createChangeAnnotationAttributeActions(annotation, 0, request) val fixes = IntentionWrapper.wrapToQuickFixes(actions.toTypedArray(), file) - sink.highlight(DevKitBundle.message("inspection.mismatched.light.service.level.and.ctor.project.level.required"), *fixes) + val holder = (sink as HighlightSinkImpl).holder + holder.registerProblem(annotationName, + DevKitBundle.message("inspection.mismatched.light.service.level.and.ctor.project.level.required"), + *fixes) } } if (level == Level.APP || level == Level.APP_AND_PROJECT) { diff --git a/plugins/devkit/devkit-java-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/ChangeParamToCoroutineScope.java b/plugins/devkit/devkit-java-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/ChangeParamToCoroutineScope.java index 14959fba2dcf..2c4ea5c38127 100644 --- a/plugins/devkit/devkit-java-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/ChangeParamToCoroutineScope.java +++ b/plugins/devkit/devkit-java-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/ChangeParamToCoroutineScope.java @@ -1,7 +1,7 @@ import com.intellij.openapi.components.Service; import com.intellij.openapi.project.Project; -@Service(Service.Level.APP) +@Service(Service.Level.APP) final class MyService { - private MyService(Project project) {} + private MyService(Project project) {} } diff --git a/plugins/devkit/devkit-java-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/MakeProjectLevel1.java b/plugins/devkit/devkit-java-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/MakeProjectLevel1.java index d9b59cf15318..a6411b6bc3b2 100644 --- a/plugins/devkit/devkit-java-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/MakeProjectLevel1.java +++ b/plugins/devkit/devkit-java-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/MakeProjectLevel1.java @@ -1,11 +1,11 @@ import com.intellij.openapi.components.Service; import com.intellij.openapi.project.Project; -@Service +@Service final class MyService { private final Project myProject; - public MyService(Project project) { + public MyService(Project project) { myProject = project; } } diff --git a/plugins/devkit/devkit-java-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/MakeProjectLevel2.java b/plugins/devkit/devkit-java-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/MakeProjectLevel2.java index 91296c70b0d4..9c1f7573bdcd 100644 --- a/plugins/devkit/devkit-java-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/MakeProjectLevel2.java +++ b/plugins/devkit/devkit-java-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/MakeProjectLevel2.java @@ -1,11 +1,11 @@ import com.intellij.openapi.components.Service; import com.intellij.openapi.project.Project; -@Service({}) +@Service({}) final class MyService { private final Project myProject; - public MyService(Project project) { + public MyService(Project project) { myProject = project; } } diff --git a/plugins/devkit/devkit-java-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/RemoveProjectParam.java b/plugins/devkit/devkit-java-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/RemoveProjectParam.java index 14959fba2dcf..2c4ea5c38127 100644 --- a/plugins/devkit/devkit-java-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/RemoveProjectParam.java +++ b/plugins/devkit/devkit-java-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/RemoveProjectParam.java @@ -1,7 +1,7 @@ import com.intellij.openapi.components.Service; import com.intellij.openapi.project.Project; -@Service(Service.Level.APP) +@Service(Service.Level.APP) final class MyService { - private MyService(Project project) {} + private MyService(Project project) {} } diff --git a/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/inspections/MismatchedLightServiceLevelAndCtorInspectionTest.kt b/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/inspections/MismatchedLightServiceLevelAndCtorInspectionTest.kt index b563207bf72f..84c6d58fb670 100644 --- a/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/inspections/MismatchedLightServiceLevelAndCtorInspectionTest.kt +++ b/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/inspections/MismatchedLightServiceLevelAndCtorInspectionTest.kt @@ -1,6 +1,7 @@ // Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.idea.devkit.inspections +import com.intellij.codeInsight.daemon.QuickFixBundle import com.intellij.testFramework.TestDataPath import org.jetbrains.idea.devkit.DevkitJavaTestsUtil import org.jetbrains.idea.devkit.inspections.quickfix.MismatchedLightServiceLevelAndCtorInspectionTestBase @@ -8,25 +9,26 @@ import org.jetbrains.idea.devkit.inspections.quickfix.MismatchedLightServiceLeve @TestDataPath("\$CONTENT_ROOT/testData/inspections/mismatchedLightServiceLevelAndCtor") class MismatchedLightServiceLevelAndCtorInspectionTest : MismatchedLightServiceLevelAndCtorInspectionTestBase() { - private val ANNOTATE_AS_SERVICE_FIX_NAME = "Annotate class 'MyService' as '@Service'" + private val NO_ARG_CTOR_FIX_NAME = QuickFixBundle.message("change.method.parameters.text", "()") + private val COROUTINE_SCOPE_PARAM_CTOR_FIX_NAME = QuickFixBundle.message("change.method.parameters.text", "(CoroutineScope scope)") override fun getBasePath() = DevkitJavaTestsUtil.TESTDATA_PATH + "inspections/mismatchedLightServiceLevelAndCtor/" override fun getFileExtension() = "java" fun testMakeProjectLevel1() { - doTest(ANNOTATE_AS_SERVICE_FIX_NAME) + doTest(annotateAsServiceFixName) } fun testMakeProjectLevel2() { - doTest(ANNOTATE_AS_SERVICE_FIX_NAME) + doTest(annotateAsServiceFixName) } fun testRemoveProjectParam() { - doTest("Change method parameters to '()'") + doTest(NO_ARG_CTOR_FIX_NAME) } fun testChangeParamToCoroutineScope() { - doTest("Change method parameters to '(CoroutineScope scope)'") + doTest(COROUTINE_SCOPE_PARAM_CTOR_FIX_NAME) } } diff --git a/plugins/devkit/devkit-kotlin-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/ChangeParamToCoroutineScope.kt b/plugins/devkit/devkit-kotlin-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/ChangeParamToCoroutineScope.kt index 86a3cb4e003f..b11b2da77107 100644 --- a/plugins/devkit/devkit-kotlin-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/ChangeParamToCoroutineScope.kt +++ b/plugins/devkit/devkit-kotlin-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/ChangeParamToCoroutineScope.kt @@ -1,5 +1,5 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.project.Project -@Service(Service.Level.APP) -class MyService(val project: Project) \ No newline at end of file +@Service(Service.Level.APP) +class MyService(val project: Project) \ No newline at end of file diff --git a/plugins/devkit/devkit-kotlin-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/MakeProjectLevel1.kt b/plugins/devkit/devkit-kotlin-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/MakeProjectLevel1.kt index a1e0b530239e..a34c83496f69 100644 --- a/plugins/devkit/devkit-kotlin-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/MakeProjectLevel1.kt +++ b/plugins/devkit/devkit-kotlin-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/MakeProjectLevel1.kt @@ -1,5 +1,5 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.project.Project -@Service -class MyService(val project: Project) \ No newline at end of file +@Service +class MyService(val project: Project) \ No newline at end of file diff --git a/plugins/devkit/devkit-kotlin-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/MakeProjectLevel2.kt b/plugins/devkit/devkit-kotlin-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/MakeProjectLevel2.kt index fb064004e6aa..0a86d87325fd 100644 --- a/plugins/devkit/devkit-kotlin-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/MakeProjectLevel2.kt +++ b/plugins/devkit/devkit-kotlin-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/MakeProjectLevel2.kt @@ -1,5 +1,5 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.project.Project -@Service(*[]) -class MyService(val project: Project) \ No newline at end of file +@Service(*[]) +class MyService(val project: Project) \ No newline at end of file diff --git a/plugins/devkit/devkit-kotlin-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/RemoveProjectParam.kt b/plugins/devkit/devkit-kotlin-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/RemoveProjectParam.kt index 86a3cb4e003f..b11b2da77107 100644 --- a/plugins/devkit/devkit-kotlin-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/RemoveProjectParam.kt +++ b/plugins/devkit/devkit-kotlin-tests/testData/inspections/mismatchedLightServiceLevelAndCtor/RemoveProjectParam.kt @@ -1,5 +1,5 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.project.Project -@Service(Service.Level.APP) -class MyService(val project: Project) \ No newline at end of file +@Service(Service.Level.APP) +class MyService(val project: Project) \ No newline at end of file diff --git a/plugins/devkit/devkit-kotlin-tests/testSrc/org/jetbrains/idea/devkit/kotlin/inspections/KtMismatchedLightServiceLevelAndCtorInspectionTest.kt b/plugins/devkit/devkit-kotlin-tests/testSrc/org/jetbrains/idea/devkit/kotlin/inspections/KtMismatchedLightServiceLevelAndCtorInspectionTest.kt index fa9e1a099d3f..cb788fffc6b1 100644 --- a/plugins/devkit/devkit-kotlin-tests/testSrc/org/jetbrains/idea/devkit/kotlin/inspections/KtMismatchedLightServiceLevelAndCtorInspectionTest.kt +++ b/plugins/devkit/devkit-kotlin-tests/testSrc/org/jetbrains/idea/devkit/kotlin/inspections/KtMismatchedLightServiceLevelAndCtorInspectionTest.kt @@ -1,7 +1,10 @@ // Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.idea.devkit.kotlin.inspections +import com.intellij.codeInsight.daemon.QuickFixBundle +import com.intellij.openapi.project.Project import com.intellij.testFramework.TestDataPath +import kotlinx.coroutines.CoroutineScope import org.jetbrains.idea.devkit.inspections.quickfix.MismatchedLightServiceLevelAndCtorInspectionTestBase import org.jetbrains.idea.devkit.kotlin.DevkitKtTestsUtil @@ -13,18 +16,20 @@ class KtMismatchedLightServiceLevelAndCtorInspectionTest : MismatchedLightServic override fun getFileExtension() = "kt" fun testMakeProjectLevel1() { - doTest("Annotate as @Service") + doTest(annotateAsServiceFixName) } fun testMakeProjectLevel2() { - doTest("Annotate as @Service") + doTest(annotateAsServiceFixName) } fun testRemoveProjectParam() { - doTest("Remove 1st parameter from constructor 'MyService'") + doTest(QuickFixBundle.message("remove.parameter.from.usage.text", 1, "parameter", "constructor", "MyService")) } fun testChangeParamToCoroutineScope() { - doTest("Change 1st parameter of constructor 'MyService' from 'Project' to 'CoroutineScope'") + doTest( + QuickFixBundle.message("change.parameter.from.usage.text", 1, "parameter", "constructor", "MyService", Project::class.java.simpleName, + CoroutineScope::class.java.simpleName)) } } diff --git a/plugins/devkit/devkit-tests/testSrc/org/jetbrains/idea/devkit/inspections/quickfix/MismatchedLightServiceLevelAndCtorInspectionTestBase.kt b/plugins/devkit/devkit-tests/testSrc/org/jetbrains/idea/devkit/inspections/quickfix/MismatchedLightServiceLevelAndCtorInspectionTestBase.kt index 2da55d38ea31..f7259eb7cbd0 100644 --- a/plugins/devkit/devkit-tests/testSrc/org/jetbrains/idea/devkit/inspections/quickfix/MismatchedLightServiceLevelAndCtorInspectionTestBase.kt +++ b/plugins/devkit/devkit-tests/testSrc/org/jetbrains/idea/devkit/inspections/quickfix/MismatchedLightServiceLevelAndCtorInspectionTestBase.kt @@ -1,10 +1,15 @@ // Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.idea.devkit.inspections.quickfix +import com.intellij.codeInsight.daemon.QuickFixBundle +import com.intellij.psi.PsiAnnotation import org.jetbrains.idea.devkit.inspections.MismatchedLightServiceLevelAndCtorInspection abstract class MismatchedLightServiceLevelAndCtorInspectionTestBase : LightDevKitInspectionFixTestBase() { + protected val annotateAsServiceFixName = QuickFixBundle.message("change.annotation.attribute.value.text", + PsiAnnotation.DEFAULT_REFERENCED_METHOD_NAME) + override fun setUp() { super.setUp() myFixture.enableInspections(MismatchedLightServiceLevelAndCtorInspection()) 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 3ed3dd141c76..501d5d89b2d2 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 @@ -21,6 +21,7 @@ import com.intellij.psi.util.PsiTreeUtil import org.jetbrains.kotlin.asJava.classes.KtLightClassForFacade import org.jetbrains.kotlin.asJava.classes.KtLightClassForSourceDeclaration import org.jetbrains.kotlin.asJava.elements.KtLightElement +import org.jetbrains.kotlin.asJava.toLightAnnotation import org.jetbrains.kotlin.asJava.toLightMethods import org.jetbrains.kotlin.asJava.unwrapped import org.jetbrains.kotlin.descriptors.CallableDescriptor @@ -40,7 +41,6 @@ import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.TypeIn import org.jetbrains.kotlin.idea.resolve.ResolutionFacade import org.jetbrains.kotlin.idea.util.CommentSaver import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers -import org.jetbrains.kotlin.idea.util.findAnnotation import org.jetbrains.kotlin.idea.util.resolveToKotlinType import org.jetbrains.kotlin.incremental.components.NoLookupLocation import org.jetbrains.kotlin.lexer.KtModifierKeywordToken @@ -416,6 +416,82 @@ class KotlinElementActionsFactory : JvmElementActionsFactory() { } } + override fun createChangeAnnotationAttributeActions(annotation: JvmAnnotation, + attributeIndex: Int, + request: AnnotationAttributeRequest): List { + val annotationEntry = annotation.safeAs>()?.kotlinOrigin.safeAs().takeIf { + it?.language == KotlinLanguage.INSTANCE + } ?: return emptyList() + return listOf(ChangeAnnotationAction(annotationEntry, attributeIndex, request)) + } + + private class ChangeAnnotationAction(annotationEntry: KtAnnotationEntry, + private val attributeIndex: Int, + private val request: AnnotationAttributeRequest) : IntentionAction { + + private val pointer: SmartPsiElementPointer + private val qualifiedName: String + + override fun startInWriteAction(): Boolean = true + + override fun getFamilyName(): String = QuickFixBundle.message("change.annotation.attribute.value.family") + + override fun getText(): String = QuickFixBundle.message("change.annotation.attribute.value.text", request.name) + + override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean = pointer.element != null + + override fun generatePreview(project: Project, editor: Editor, file: PsiFile): IntentionPreviewInfo { + invokeImpl(PsiTreeUtil.findSameElementInCopy(pointer.element, file), project) + return IntentionPreviewInfo.DIFF + } + + override fun invoke(project: Project, editor: Editor?, file: PsiFile) { + val annotationEntry = pointer.element ?: return + invokeImpl(annotationEntry, project) + } + + private fun invokeImpl(annotationEntry: KtAnnotationEntry, project: Project) { + val facade = JavaPsiFacade.getInstance(annotationEntry.project) + val isKotlinAnnotation = facade.findClass(qualifiedName, annotationEntry.resolveScope)?.language == KotlinLanguage.INSTANCE + val dummyAnnotationRequest = annotationRequest(qualifiedName, request) + val psiFactory = KtPsiFactory(project) + val annotationText = '@' + renderAnnotation(dummyAnnotationRequest, psiFactory) { isKotlinAnnotation } + val dummyArgumentList = psiFactory.createAnnotationEntry(annotationText).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) + } + + private fun findAttribute(annotationEntry: KtAnnotationEntry, name: String, index: Int): IndexedValue? { + val arguments = annotationEntry.valueArgumentList?.arguments ?: return null + arguments.withIndex().find { (_, argument) -> + argument.getArgumentName()?.asName?.identifier == name + }?.let { + return it + } + val valueArgument = arguments.getOrNull(index) ?: return null + return IndexedValue(index, valueArgument) + } + + init { + pointer = annotationEntry.createSmartPointer() + qualifiedName = annotationEntry.toLightAnnotation()?.qualifiedName ?: throw IllegalStateException("r") + } + } + override fun createChangeParametersActions(target: JvmMethod, request: ChangeParametersRequest): List { return when (val kotlinOrigin = (target as? KtLightElement<*, *>)?.kotlinOrigin) { is KtNamedFunction -> listOfNotNull(ChangeMethodParameters.create(kotlinOrigin, request)) @@ -554,11 +630,7 @@ internal fun addAnnotationEntry( val psiFactory = KtPsiFactory(target.project) // could be generated via descriptor when KT-30478 is fixed val annotationText = '@' + annotationUseSiteTargetPrefix + renderAnnotation(target, request, psiFactory) - val annotationEntry = psiFactory.createAnnotationEntry(annotationText) - target.findAnnotation(FqName(request.qualifiedName))?.let { - return it.replace(annotationEntry) as KtAnnotationEntry - } - return target.addAnnotationEntry(annotationEntry) + return target.addAnnotationEntry(psiFactory.createAnnotationEntry(annotationText)) } private fun renderAnnotation(target: PsiElement, request: AnnotationRequest, psiFactory: KtPsiFactory): String {