From 9ea9ea100867a19e111781dc1eae45702b1188f4 Mon Sep 17 00:00:00 2001 From: Toshiaki Kameyama Date: Fri, 6 Oct 2017 13:14:47 +0300 Subject: [PATCH] Add quick fix to add required target to annotation #KT-20484 Fixed --- .../idea/quickfix/AddAnnotationTargetFix.kt | 118 ++++++++++++++++++ .../kotlin/idea/quickfix/QuickFixRegistrar.kt | 1 + .../quickfix/addAnnotationTarget/basic1.kt | 7 ++ .../addAnnotationTarget/basic1.kt.after | 7 ++ .../quickfix/addAnnotationTarget/basic2.kt | 7 ++ .../addAnnotationTarget/basic2.kt.after | 7 ++ .../quickfix/addAnnotationTarget/basic3.kt | 7 ++ .../addAnnotationTarget/basic3.kt.after | 7 ++ .../quickfix/addAnnotationTarget/basic4.kt | 9 ++ .../addAnnotationTarget/basic4.kt.after | 9 ++ .../quickfix/addAnnotationTarget/fromLib.kt | 9 ++ .../hasAnnotationTarget1.kt | 8 ++ .../hasAnnotationTarget1.kt.after | 8 ++ .../hasAnnotationTarget2.kt | 10 ++ .../hasAnnotationTarget2.kt.after | 10 ++ .../addAnnotationTarget/noBackingField.kt | 7 ++ .../noBackingField.kt.after | 7 ++ .../idea/quickfix/QuickFixTestGenerated.java | 57 +++++++++ 18 files changed, 295 insertions(+) create mode 100644 idea/src/org/jetbrains/kotlin/idea/quickfix/AddAnnotationTargetFix.kt create mode 100644 idea/testData/quickfix/addAnnotationTarget/basic1.kt create mode 100644 idea/testData/quickfix/addAnnotationTarget/basic1.kt.after create mode 100644 idea/testData/quickfix/addAnnotationTarget/basic2.kt create mode 100644 idea/testData/quickfix/addAnnotationTarget/basic2.kt.after create mode 100644 idea/testData/quickfix/addAnnotationTarget/basic3.kt create mode 100644 idea/testData/quickfix/addAnnotationTarget/basic3.kt.after create mode 100644 idea/testData/quickfix/addAnnotationTarget/basic4.kt create mode 100644 idea/testData/quickfix/addAnnotationTarget/basic4.kt.after create mode 100644 idea/testData/quickfix/addAnnotationTarget/fromLib.kt create mode 100644 idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget1.kt create mode 100644 idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget1.kt.after create mode 100644 idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget2.kt create mode 100644 idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget2.kt.after create mode 100644 idea/testData/quickfix/addAnnotationTarget/noBackingField.kt create mode 100644 idea/testData/quickfix/addAnnotationTarget/noBackingField.kt.after diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/AddAnnotationTargetFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/AddAnnotationTargetFix.kt new file mode 100644 index 00000000000..70cc9dcd9ff --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/AddAnnotationTargetFix.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2010-2017 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.quickfix + +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.search.searches.ReferencesSearch +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.descriptors.annotations.KotlinTarget +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.search.restrictToKotlinSources +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType +import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType +import org.jetbrains.kotlin.resolve.AnnotationChecker +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.BindingTraceContext +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode + +class AddAnnotationTargetFix(annotationEntry: KtAnnotationEntry) : KotlinQuickFixAction(annotationEntry) { + + override fun getText() = "Add annotation target" + + override fun getFamilyName() = text + + override fun invoke(project: Project, editor: Editor?, file: KtFile) { + val annotationEntry = element ?: return + + val annotationClass = annotationEntry.toAnnotationClass() ?: return + + val requiredAnnotationTargets = annotationEntry.getRequiredAnnotationTargets(annotationClass, project) + if (requiredAnnotationTargets.isEmpty()) return + + val psiFactory = KtPsiFactory(annotationEntry) + annotationClass.addAnnotationTargets(requiredAnnotationTargets, psiFactory) + } + + companion object : KotlinSingleIntentionActionFactory() { + private fun KtAnnotationEntry.toAnnotationClass(): KtClass? { + val context = analyze(BodyResolveMode.PARTIAL) + val annotationDescriptor = context[BindingContext.ANNOTATION, this] ?: return null + val annotationTypeDescriptor = annotationDescriptor.type.constructor.declarationDescriptor ?: return null + return (DescriptorToSourceUtils.descriptorToDeclaration(annotationTypeDescriptor) as? KtClass)?.takeIf { + it.isAnnotation() && it.isWritable + } + } + + override fun createAction(diagnostic: Diagnostic): KotlinQuickFixAction? { + val entry = diagnostic.psiElement as? KtAnnotationEntry ?: return null + if (entry.toAnnotationClass() == null) return null + + return AddAnnotationTargetFix(entry) + } + } +} + +private fun KtAnnotationEntry.getRequiredAnnotationTargets(annotationClass: KtClass, project: Project): List { + val requiredTargets = getActualTargetList() + if (requiredTargets.isEmpty()) return emptyList() + + val searchScope = GlobalSearchScope.allScope(project).restrictToKotlinSources() + val otherReferenceRequiredTargets = ReferencesSearch.search(annotationClass, searchScope).mapNotNull { reference -> + reference.element.getNonStrictParentOfType()?.takeIf { it != this }?.getActualTargetList() + }.flatten().toSet() + + val annotationTargetValueNames = AnnotationTarget.values().map { it.name } + return (requiredTargets + otherReferenceRequiredTargets).filter { it.name in annotationTargetValueNames } +} + +private fun KtAnnotationEntry.getActualTargetList(): List { + val annotatedElement = getStrictParentOfType()?.owner as? KtElement ?: return emptyList() + return AnnotationChecker.getDeclarationSiteActualTargetList(annotatedElement, null, BindingTraceContext()) +} + +private fun KtClass.addAnnotationTargets(annotationTargets: List, psiFactory: KtPsiFactory) { + val targetAnnotationName = KotlinBuiltIns.FQ_NAMES.target.shortName().asString() + + val targetAnnotationEntry = annotationEntries.find { it.typeReference?.text == targetAnnotationName } ?: run { + val text = "@$targetAnnotationName${annotationTargets.toArgumentListString()}" + addAnnotationEntry(psiFactory.createAnnotationEntry(text)) + return + } + val valueArgumentList = targetAnnotationEntry.valueArgumentList + if (valueArgumentList == null) { + val text = annotationTargets.toArgumentListString() + targetAnnotationEntry.add(psiFactory.createCallArguments(text)) + } + else { + val arguments = targetAnnotationEntry.valueArguments.mapNotNull { it.getArgumentExpression()?.text } + for (target in annotationTargets) { + val text = target.asNameString() + if (text !in arguments) valueArgumentList.addArgument(psiFactory.createArgument(text)) + } + } +} + +private fun List.toArgumentListString() = + joinToString(separator = ", ", prefix = "(", postfix = ")") { it.asNameString() } + +private fun KotlinTarget.asNameString() = + "${KotlinBuiltIns.FQ_NAMES.annotationTarget.shortName().asString()}.$name" diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt index dfe8cfc0a91..eb6d45514ad 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt @@ -514,6 +514,7 @@ class QuickFixRegistrar : QuickFixContributor { RETURN_NOT_ALLOWED.registerFactory(ChangeToLabeledReturnFix) + WRONG_ANNOTATION_TARGET.registerFactory(AddAnnotationTargetFix) WRONG_ANNOTATION_TARGET_WITH_USE_SITE_TARGET.registerFactory(MoveReceiverAnnotationFix) NO_CONSTRUCTOR.registerFactory(RemoveNoConstructorFix) diff --git a/idea/testData/quickfix/addAnnotationTarget/basic1.kt b/idea/testData/quickfix/addAnnotationTarget/basic1.kt new file mode 100644 index 00000000000..27e203178d5 --- /dev/null +++ b/idea/testData/quickfix/addAnnotationTarget/basic1.kt @@ -0,0 +1,7 @@ +// "Add annotation target" "true" + +annotation class Foo + +class Test { + fun foo(): @Foo Int = 1 +} \ No newline at end of file diff --git a/idea/testData/quickfix/addAnnotationTarget/basic1.kt.after b/idea/testData/quickfix/addAnnotationTarget/basic1.kt.after new file mode 100644 index 00000000000..f75a533e401 --- /dev/null +++ b/idea/testData/quickfix/addAnnotationTarget/basic1.kt.after @@ -0,0 +1,7 @@ +// "Add annotation target" "true" + +@Target(AnnotationTarget.TYPE) annotation class Foo + +class Test { + fun foo(): @Foo Int = 1 +} \ No newline at end of file diff --git a/idea/testData/quickfix/addAnnotationTarget/basic2.kt b/idea/testData/quickfix/addAnnotationTarget/basic2.kt new file mode 100644 index 00000000000..add508efe7a --- /dev/null +++ b/idea/testData/quickfix/addAnnotationTarget/basic2.kt @@ -0,0 +1,7 @@ +// "Add annotation target" "true" + +@Target +annotation class Foo + +@Foo +class Test \ No newline at end of file diff --git a/idea/testData/quickfix/addAnnotationTarget/basic2.kt.after b/idea/testData/quickfix/addAnnotationTarget/basic2.kt.after new file mode 100644 index 00000000000..70c630c413c --- /dev/null +++ b/idea/testData/quickfix/addAnnotationTarget/basic2.kt.after @@ -0,0 +1,7 @@ +// "Add annotation target" "true" + +@Target(AnnotationTarget.CLASS) +annotation class Foo + +@Foo +class Test \ No newline at end of file diff --git a/idea/testData/quickfix/addAnnotationTarget/basic3.kt b/idea/testData/quickfix/addAnnotationTarget/basic3.kt new file mode 100644 index 00000000000..0fd024e6047 --- /dev/null +++ b/idea/testData/quickfix/addAnnotationTarget/basic3.kt @@ -0,0 +1,7 @@ +// "Add annotation target" "true" + +@Target() +annotation class Foo + +@Foo +class Test \ No newline at end of file diff --git a/idea/testData/quickfix/addAnnotationTarget/basic3.kt.after b/idea/testData/quickfix/addAnnotationTarget/basic3.kt.after new file mode 100644 index 00000000000..70c630c413c --- /dev/null +++ b/idea/testData/quickfix/addAnnotationTarget/basic3.kt.after @@ -0,0 +1,7 @@ +// "Add annotation target" "true" + +@Target(AnnotationTarget.CLASS) +annotation class Foo + +@Foo +class Test \ No newline at end of file diff --git a/idea/testData/quickfix/addAnnotationTarget/basic4.kt b/idea/testData/quickfix/addAnnotationTarget/basic4.kt new file mode 100644 index 00000000000..736d7ccd91a --- /dev/null +++ b/idea/testData/quickfix/addAnnotationTarget/basic4.kt @@ -0,0 +1,9 @@ +// "Add annotation target" "true" + +annotation class Foo + +@Foo +class Test { + @Foo + fun foo(): @Foo Int = 1 +} \ No newline at end of file diff --git a/idea/testData/quickfix/addAnnotationTarget/basic4.kt.after b/idea/testData/quickfix/addAnnotationTarget/basic4.kt.after new file mode 100644 index 00000000000..fa19d1955f5 --- /dev/null +++ b/idea/testData/quickfix/addAnnotationTarget/basic4.kt.after @@ -0,0 +1,9 @@ +// "Add annotation target" "true" + +@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) annotation class Foo + +@Foo +class Test { + @Foo + fun foo(): @Foo Int = 1 +} \ No newline at end of file diff --git a/idea/testData/quickfix/addAnnotationTarget/fromLib.kt b/idea/testData/quickfix/addAnnotationTarget/fromLib.kt new file mode 100644 index 00000000000..90e19e17f4e --- /dev/null +++ b/idea/testData/quickfix/addAnnotationTarget/fromLib.kt @@ -0,0 +1,9 @@ +// "Add annotation target" "false" +// WITH_RUNTIME +// ACTION: Make internal +// ACTION: Make private +// ACTION: Specify type explicitly +// ERROR: This annotation is not applicable to target 'top level property without backing field or delegate' + +@JvmField +val x get() = 42 diff --git a/idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget1.kt b/idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget1.kt new file mode 100644 index 00000000000..fdbb47ae7b6 --- /dev/null +++ b/idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget1.kt @@ -0,0 +1,8 @@ +// "Add annotation target" "true" + +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +annotation class Foo + +class Test { + fun foo(): @Foo Int = 1 +} \ No newline at end of file diff --git a/idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget1.kt.after b/idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget1.kt.after new file mode 100644 index 00000000000..c5c0cb192e8 --- /dev/null +++ b/idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget1.kt.after @@ -0,0 +1,8 @@ +// "Add annotation target" "true" + +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPE) +annotation class Foo + +class Test { + fun foo(): @Foo Int = 1 +} \ No newline at end of file diff --git a/idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget2.kt b/idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget2.kt new file mode 100644 index 00000000000..04196cb4cb8 --- /dev/null +++ b/idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget2.kt @@ -0,0 +1,10 @@ +// "Add annotation target" "true" + +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +annotation class Foo + +@Foo +class Test { + @Foo + fun foo(): @Foo Int = 1 +} \ No newline at end of file diff --git a/idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget2.kt.after b/idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget2.kt.after new file mode 100644 index 00000000000..fe6389e13b6 --- /dev/null +++ b/idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget2.kt.after @@ -0,0 +1,10 @@ +// "Add annotation target" "true" + +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPE) +annotation class Foo + +@Foo +class Test { + @Foo + fun foo(): @Foo Int = 1 +} \ No newline at end of file diff --git a/idea/testData/quickfix/addAnnotationTarget/noBackingField.kt b/idea/testData/quickfix/addAnnotationTarget/noBackingField.kt new file mode 100644 index 00000000000..32485f8684c --- /dev/null +++ b/idea/testData/quickfix/addAnnotationTarget/noBackingField.kt @@ -0,0 +1,7 @@ +// "Add annotation target" "true" + +@Target(AnnotationTarget.FIELD) +annotation class FieldAnn + +@FieldAnn +val x get() = 42 \ No newline at end of file diff --git a/idea/testData/quickfix/addAnnotationTarget/noBackingField.kt.after b/idea/testData/quickfix/addAnnotationTarget/noBackingField.kt.after new file mode 100644 index 00000000000..5b2756e0808 --- /dev/null +++ b/idea/testData/quickfix/addAnnotationTarget/noBackingField.kt.after @@ -0,0 +1,7 @@ +// "Add annotation target" "true" + +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +annotation class FieldAnn + +@FieldAnn +val x get() = 42 \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java index cf9c7b3c86d..1cd5ba20f2a 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java @@ -237,6 +237,63 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest { } } + @TestMetadata("idea/testData/quickfix/addAnnotationTarget") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class AddAnnotationTarget extends AbstractQuickFixTest { + public void testAllFilesPresentInAddAnnotationTarget() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/addAnnotationTarget"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true); + } + + @TestMetadata("basic1.kt") + public void testBasic1() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/addAnnotationTarget/basic1.kt"); + doTest(fileName); + } + + @TestMetadata("basic2.kt") + public void testBasic2() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/addAnnotationTarget/basic2.kt"); + doTest(fileName); + } + + @TestMetadata("basic3.kt") + public void testBasic3() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/addAnnotationTarget/basic3.kt"); + doTest(fileName); + } + + @TestMetadata("basic4.kt") + public void testBasic4() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/addAnnotationTarget/basic4.kt"); + doTest(fileName); + } + + @TestMetadata("fromLib.kt") + public void testFromLib() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/addAnnotationTarget/fromLib.kt"); + doTest(fileName); + } + + @TestMetadata("hasAnnotationTarget1.kt") + public void testHasAnnotationTarget1() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget1.kt"); + doTest(fileName); + } + + @TestMetadata("hasAnnotationTarget2.kt") + public void testHasAnnotationTarget2() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/addAnnotationTarget/hasAnnotationTarget2.kt"); + doTest(fileName); + } + + @TestMetadata("noBackingField.kt") + public void testNoBackingField() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/addAnnotationTarget/noBackingField.kt"); + doTest(fileName); + } + } + @TestMetadata("idea/testData/quickfix/addCrossinline") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)