Experimental fixes: introduce "add annotation" (KT-22760)

This commit is contained in:
Mikhail Glukhikh
2018-05-04 15:45:43 +03:00
parent 721d9df4e0
commit f404d6c97a
15 changed files with 266 additions and 22 deletions
@@ -0,0 +1,27 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.quickfix
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.idea.util.addAnnotation
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtFile
open class AddAnnotationFix(
element: KtDeclaration,
private val annotationFqName: FqName,
private val suffix: String = ""
) : KotlinQuickFixAction<KtDeclaration>(element) {
override fun getText(): String = "Add '@${annotationFqName.shortName()}' annotation$suffix"
override fun getFamilyName(): String = "Add annotation"
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
element?.addAnnotation(annotationFqName)
}
}
@@ -122,7 +122,7 @@ private fun KtAnnotationEntry.getActualTargetList(): List<KotlinTarget> {
?: getStrictParentOfType<KtFile>()
?: return emptyList()
val targetList = AnnotationChecker.getActualTargetList(annotatedElement, null, BindingTraceContext())
val targetList = AnnotationChecker.getActualTargetList(annotatedElement, null, BindingTraceContext().bindingContext)
val useSiteTarget = this.useSiteTarget ?: return targetList.defaultTargets
val annotationUseSiteTarget = useSiteTarget.getAnnotationUseSiteTarget()
@@ -6,25 +6,15 @@
package org.jetbrains.kotlin.idea.quickfix
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.idea.util.addAnnotation
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.resolve.annotations.JVM_DEFAULT_FQ_NAME
class AddJvmDefaultAnnotation(declaration: KtCallableDeclaration) : KotlinQuickFixAction<KtCallableDeclaration>(declaration) {
override fun getText(): String = "Add '@JvmDefault' annotation"
class AddJvmDefaultAnnotation(declaration: KtCallableDeclaration) : AddAnnotationFix(declaration, JVM_DEFAULT_FQ_NAME) {
override fun getFamilyName(): String = text
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
element?.addAnnotation(JVM_DEFAULT_FQ_NAME)
}
companion object : KotlinSingleIntentionActionFactory() {
override fun createAction(diagnostic: Diagnostic): IntentionAction? {
val psiElement = diagnostic.psiElement
@@ -0,0 +1,73 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.quickfix
import com.intellij.codeInsight.intention.IntentionAction
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.annotations.KotlinTarget
import org.jetbrains.kotlin.descriptors.resolveClassByFqName
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.diagnostics.Errors.*
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.core.toDescriptor
import org.jetbrains.kotlin.incremental.components.NoLookupLocation
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.resolve.AnnotationChecker
import org.jetbrains.kotlin.resolve.descriptorUtil.module
object ExperimentalFixesFactory : KotlinIntentionActionsFactory() {
override fun doCreateActions(diagnostic: Diagnostic): List<IntentionAction> {
val element = diagnostic.psiElement
val containingDeclaration: KtDeclaration = element.getStrictParentOfType() ?: return emptyList()
val factory = diagnostic.factory
val annotationFqName = when (factory) {
EXPERIMENTAL_API_USAGE -> EXPERIMENTAL_API_USAGE.cast(diagnostic).a
EXPERIMENTAL_API_USAGE_ERROR -> EXPERIMENTAL_API_USAGE_ERROR.cast(diagnostic).a
EXPERIMENTAL_OVERRIDE -> EXPERIMENTAL_OVERRIDE.cast(diagnostic).a
EXPERIMENTAL_OVERRIDE_ERROR -> EXPERIMENTAL_OVERRIDE_ERROR.cast(diagnostic).a
else -> null
} ?: return emptyList()
val moduleDescriptor = containingDeclaration.resolveToDescriptorIfAny()?.module ?: return emptyList()
val annotationClassDescriptor = moduleDescriptor.resolveClassByFqName(
annotationFqName, NoLookupLocation.FROM_IDE
) ?: return emptyList()
val applicableTargets = AnnotationChecker.applicableTargetSet(annotationClassDescriptor) ?: KotlinTarget.DEFAULT_TARGET_SET
val context = when (element) {
is KtElement -> element.analyze()
else -> containingDeclaration.analyze()
}
fun isApplicableTo(declaration: KtDeclaration, applicableTargets: Set<KotlinTarget>): Boolean {
val actualTargetList = AnnotationChecker.getDeclarationSiteActualTargetList(
declaration, declaration.toDescriptor() as? ClassDescriptor, context
)
return actualTargetList.any { it in applicableTargets }
}
val result = mutableListOf<IntentionAction>()
if (isApplicableTo(containingDeclaration, applicableTargets)) {
result.add(AddAnnotationFix(containingDeclaration, annotationFqName, " to '${containingDeclaration.name}'"))
}
if (containingDeclaration is KtCallableDeclaration) {
val containingClassOrObject = containingDeclaration.containingClassOrObject
if (containingClassOrObject != null && isApplicableTo(containingClassOrObject, applicableTargets)) {
result.add(
AddAnnotationFix(containingClassOrObject, annotationFqName, " to containing class '${containingClassOrObject.name}'")
)
}
}
return result
}
}
@@ -546,5 +546,10 @@ class QuickFixRegistrar : QuickFixContributor {
JVM_DEFAULT_REQUIRED_FOR_OVERRIDE.registerFactory(AddJvmDefaultAnnotation)
NON_JVM_DEFAULT_OVERRIDES_JAVA_DEFAULT.registerFactory(AddJvmDefaultAnnotation)
EXPERIMENTAL_API_USAGE.registerFactory(ExperimentalFixesFactory)
EXPERIMENTAL_API_USAGE_ERROR.registerFactory(ExperimentalFixesFactory)
EXPERIMENTAL_OVERRIDE.registerFactory(ExperimentalFixesFactory)
EXPERIMENTAL_OVERRIDE_ERROR.registerFactory(ExperimentalFixesFactory)
}
}
+15
View File
@@ -0,0 +1,15 @@
// "Add '@MyExperimentalAPI' annotation to 'bar'" "true"
// COMPILER_ARGUMENTS: -Xuse-experimental=kotlin.Experimental
// WITH_RUNTIME
@Experimental
annotation class MyExperimentalAPI
@MyExperimentalAPI
fun foo() {}
class Bar {
fun bar() {
foo<caret>()
}
}
@@ -0,0 +1,16 @@
// "Add '@MyExperimentalAPI' annotation to 'bar'" "true"
// COMPILER_ARGUMENTS: -Xuse-experimental=kotlin.Experimental
// WITH_RUNTIME
@Experimental
annotation class MyExperimentalAPI
@MyExperimentalAPI
fun foo() {}
class Bar {
@MyExperimentalAPI
fun bar() {
foo()
}
}
@@ -0,0 +1,15 @@
// "Add '@MyExperimentalAPI' annotation to containing class 'Bar'" "true"
// COMPILER_ARGUMENTS: -Xuse-experimental=kotlin.Experimental
// WITH_RUNTIME
@Experimental
annotation class MyExperimentalAPI
@MyExperimentalAPI
fun foo() {}
class Bar {
fun bar() {
foo<caret>()
}
}
@@ -0,0 +1,16 @@
// "Add '@MyExperimentalAPI' annotation to containing class 'Bar'" "true"
// COMPILER_ARGUMENTS: -Xuse-experimental=kotlin.Experimental
// WITH_RUNTIME
@Experimental
annotation class MyExperimentalAPI
@MyExperimentalAPI
fun foo() {}
@MyExperimentalAPI
class Bar {
fun bar() {
foo()
}
}
@@ -0,0 +1,21 @@
// "Add '@MyExperimentalAPI' annotation to 'bar'" "false"
// COMPILER_ARGUMENTS: -Xuse-experimental=kotlin.Experimental
// WITH_RUNTIME
// ACTION: Add '@MyExperimentalAPI' annotation to containing class 'Bar'
// ERROR: This declaration is experimental and its usage must be marked with '@MyExperimentalAPI' or '@UseExperimental(MyExperimentalAPI::class)'
// ERROR: This declaration is experimental and its usage must be marked with '@MyExperimentalAPI' or '@UseExperimental(MyExperimentalAPI::class)'
@Experimental
@Target(AnnotationTarget.CLASS)
annotation class MyExperimentalAPI
@MyExperimentalAPI
class Some {
fun foo() {}
}
class Bar {
fun bar() {
Some().foo<caret>()
}
}
+18
View File
@@ -0,0 +1,18 @@
// "Add '@MyExperimentalAPI' annotation to containing class 'Derived'" "false"
// COMPILER_ARGUMENTS: -Xuse-experimental=kotlin.Experimental
// WITH_RUNTIME
// ACTION: Add '@MyExperimentalAPI' annotation to 'foo'
// ERROR: This declaration overrides experimental member of supertype 'Base' and must be annotated with '@MyExperimentalAPI'
@Experimental
@Target(AnnotationTarget.FUNCTION)
annotation class MyExperimentalAPI
open class Base {
@MyExperimentalAPI
open fun foo() {}
}
class Derived : Base() {
override fun foo<caret>() {}
}
@@ -2455,6 +2455,19 @@ public class QuickFixMultiFileTestGenerated extends AbstractQuickFixMultiFileTes
}
}
@TestMetadata("idea/testData/quickfix/experimental")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class Experimental extends AbstractQuickFixMultiFileTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTestWithExtraFile, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInExperimental() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/experimental"), Pattern.compile("^(\\w+)\\.((before\\.Main\\.\\w+)|(test))$"), TargetBackend.ANY, true);
}
}
@TestMetadata("idea/testData/quickfix/expressions")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@@ -6026,6 +6026,39 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
}
}
@TestMetadata("idea/testData/quickfix/experimental")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class Experimental extends AbstractQuickFixTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInExperimental() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/experimental"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("basicFunction.kt")
public void testBasicFunction() throws Exception {
runTest("idea/testData/quickfix/experimental/basicFunction.kt");
}
@TestMetadata("basicFunctionContainingClass.kt")
public void testBasicFunctionContainingClass() throws Exception {
runTest("idea/testData/quickfix/experimental/basicFunctionContainingClass.kt");
}
@TestMetadata("basicFunctionNotApplicable.kt")
public void testBasicFunctionNotApplicable() throws Exception {
runTest("idea/testData/quickfix/experimental/basicFunctionNotApplicable.kt");
}
@TestMetadata("override.kt")
public void testOverride() throws Exception {
runTest("idea/testData/quickfix/experimental/override.kt");
}
}
@TestMetadata("idea/testData/quickfix/expressions")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)