Add quick fix to add required target to annotation #KT-20484 Fixed

This commit is contained in:
Toshiaki Kameyama
2017-10-06 13:14:47 +03:00
committed by Mikhail Glukhikh
parent 204d9e3423
commit 9ea9ea1008
18 changed files with 295 additions and 0 deletions
@@ -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<KtAnnotationEntry>(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<KtAnnotationEntry>? {
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<KotlinTarget> {
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<KtAnnotationEntry>()?.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<KotlinTarget> {
val annotatedElement = getStrictParentOfType<KtModifierList>()?.owner as? KtElement ?: return emptyList()
return AnnotationChecker.getDeclarationSiteActualTargetList(annotatedElement, null, BindingTraceContext())
}
private fun KtClass.addAnnotationTargets(annotationTargets: List<KotlinTarget>, 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<KotlinTarget>.toArgumentListString() =
joinToString(separator = ", ", prefix = "(", postfix = ")") { it.asNameString() }
private fun KotlinTarget.asNameString() =
"${KotlinBuiltIns.FQ_NAMES.annotationTarget.shortName().asString()}.$name"
@@ -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)
+7
View File
@@ -0,0 +1,7 @@
// "Add annotation target" "true"
annotation class Foo
class Test {
fun foo(): <caret>@Foo Int = 1
}
@@ -0,0 +1,7 @@
// "Add annotation target" "true"
@Target(AnnotationTarget.TYPE) annotation class Foo
class Test {
fun foo(): @Foo Int = 1
}
+7
View File
@@ -0,0 +1,7 @@
// "Add annotation target" "true"
@Target
annotation class Foo
<caret>@Foo
class Test
@@ -0,0 +1,7 @@
// "Add annotation target" "true"
@Target(AnnotationTarget.CLASS)
annotation class Foo
@Foo
class Test
+7
View File
@@ -0,0 +1,7 @@
// "Add annotation target" "true"
@Target()
annotation class Foo
<caret>@Foo
class Test
@@ -0,0 +1,7 @@
// "Add annotation target" "true"
@Target(AnnotationTarget.CLASS)
annotation class Foo
@Foo
class Test
+9
View File
@@ -0,0 +1,9 @@
// "Add annotation target" "true"
annotation class Foo
@Foo
class Test {
@Foo
fun foo(): <caret>@Foo Int = 1
}
@@ -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
}
+9
View File
@@ -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'
<caret>@JvmField
val x get() = 42
@@ -0,0 +1,8 @@
// "Add annotation target" "true"
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class Foo
class Test {
fun foo(): <caret>@Foo Int = 1
}
@@ -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
}
@@ -0,0 +1,10 @@
// "Add annotation target" "true"
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class Foo
@Foo
class Test {
@Foo
fun foo(): <caret>@Foo Int = 1
}
@@ -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
}
@@ -0,0 +1,7 @@
// "Add annotation target" "true"
@Target(AnnotationTarget.FIELD)
annotation class FieldAnn
<caret>@FieldAnn
val x get() = 42
@@ -0,0 +1,7 @@
// "Add annotation target" "true"
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
annotation class FieldAnn
@FieldAnn
val x get() = 42
@@ -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)