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
@@ -34,7 +34,7 @@ class AnnotationChecker(
private val languageVersionSettings: LanguageVersionSettings private val languageVersionSettings: LanguageVersionSettings
) { ) {
fun check(annotated: KtAnnotated, trace: BindingTrace, descriptor: DeclarationDescriptor? = null) { fun check(annotated: KtAnnotated, trace: BindingTrace, descriptor: DeclarationDescriptor? = null) {
val actualTargets = getActualTargetList(annotated, descriptor, trace) val actualTargets = getActualTargetList(annotated, descriptor, trace.bindingContext)
checkEntries(annotated.annotationEntries, actualTargets, trace, annotated) checkEntries(annotated.annotationEntries, actualTargets, trace, annotated)
if (annotated is KtCallableDeclaration) { if (annotated is KtCallableDeclaration) {
annotated.typeReference?.let { check(it, trace) } annotated.typeReference?.let { check(it, trace) }
@@ -64,7 +64,7 @@ class AnnotationChecker(
} }
fun checkExpression(expression: KtExpression, trace: BindingTrace) { fun checkExpression(expression: KtExpression, trace: BindingTrace) {
checkEntries(expression.getAnnotationEntries(), getActualTargetList(expression, null, trace), trace) checkEntries(expression.getAnnotationEntries(), getActualTargetList(expression, null, trace.bindingContext), trace)
if (expression is KtLambdaExpression) { if (expression is KtLambdaExpression) {
for (parameter in expression.valueParameters) { for (parameter in expression.valueParameters) {
parameter.typeReference?.let { check(it, trace) } parameter.typeReference?.let { check(it, trace) }
@@ -197,15 +197,15 @@ class AnnotationChecker(
}.toSet() }.toSet()
} }
fun getDeclarationSiteActualTargetList(annotated: KtElement, descriptor: ClassDescriptor?, trace: BindingTrace): fun getDeclarationSiteActualTargetList(annotated: KtElement, descriptor: ClassDescriptor?, context: BindingContext):
List<KotlinTarget> { List<KotlinTarget> {
return getActualTargetList(annotated, descriptor, trace).defaultTargets return getActualTargetList(annotated, descriptor, context).defaultTargets
} }
private fun DeclarationDescriptor?.hasBackingField(bindingTrace: BindingTrace) = private fun DeclarationDescriptor?.hasBackingField(context: BindingContext) =
(this as? PropertyDescriptor)?.let { bindingTrace.get(BindingContext.BACKING_FIELD_REQUIRED, it) } ?: false (this as? PropertyDescriptor)?.let { context.get(BindingContext.BACKING_FIELD_REQUIRED, it) } ?: false
fun getActualTargetList(annotated: KtElement, descriptor: DeclarationDescriptor?, trace: BindingTrace): TargetList { fun getActualTargetList(annotated: KtElement, descriptor: DeclarationDescriptor?, context: BindingContext): TargetList {
return when (annotated) { return when (annotated) {
is KtClassOrObject -> is KtClassOrObject ->
(descriptor as? ClassDescriptor)?.let { TargetList(KotlinTarget.classActualTargets(it)) } ?: TargetLists.T_CLASSIFIER (descriptor as? ClassDescriptor)?.let { TargetList(KotlinTarget.classActualTargets(it)) } ?: TargetLists.T_CLASSIFIER
@@ -213,8 +213,8 @@ class AnnotationChecker(
is KtProperty -> { is KtProperty -> {
when { when {
annotated.isLocal -> TargetLists.T_LOCAL_VARIABLE annotated.isLocal -> TargetLists.T_LOCAL_VARIABLE
annotated.isMember -> TargetLists.T_MEMBER_PROPERTY(descriptor.hasBackingField(trace), annotated.hasDelegate()) annotated.isMember -> TargetLists.T_MEMBER_PROPERTY(descriptor.hasBackingField(context), annotated.hasDelegate())
else -> TargetLists.T_TOP_LEVEL_PROPERTY(descriptor.hasBackingField(trace), annotated.hasDelegate()) else -> TargetLists.T_TOP_LEVEL_PROPERTY(descriptor.hasBackingField(context), annotated.hasDelegate())
} }
} }
is KtParameter -> { is KtParameter -> {
@@ -446,7 +446,9 @@ object ModifierCheckerCore {
} }
} }
} }
val actualTargets = AnnotationChecker.getDeclarationSiteActualTargetList(listOwner, descriptor as? ClassDescriptor, trace) val actualTargets = AnnotationChecker.getDeclarationSiteActualTargetList(
listOwner, descriptor as? ClassDescriptor, trace.bindingContext
)
val list = listOwner.modifierList ?: return val list = listOwner.modifierList ?: return
checkModifierList(list, trace, descriptor?.containingDeclaration, actualTargets, languageVersionSettings) checkModifierList(list, trace, descriptor?.containingDeclaration, actualTargets, languageVersionSettings)
} }
@@ -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>() ?: getStrictParentOfType<KtFile>()
?: return emptyList() ?: 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 useSiteTarget = this.useSiteTarget ?: return targetList.defaultTargets
val annotationUseSiteTarget = useSiteTarget.getAnnotationUseSiteTarget() val annotationUseSiteTarget = useSiteTarget.getAnnotationUseSiteTarget()
@@ -6,25 +6,15 @@
package org.jetbrains.kotlin.idea.quickfix package org.jetbrains.kotlin.idea.quickfix
import com.intellij.codeInsight.intention.IntentionAction 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.diagnostics.Diagnostic
import org.jetbrains.kotlin.idea.util.addAnnotation
import org.jetbrains.kotlin.psi.KtCallableDeclaration import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedFunction import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.resolve.annotations.JVM_DEFAULT_FQ_NAME import org.jetbrains.kotlin.resolve.annotations.JVM_DEFAULT_FQ_NAME
class AddJvmDefaultAnnotation(declaration: KtCallableDeclaration) : KotlinQuickFixAction<KtCallableDeclaration>(declaration) { class AddJvmDefaultAnnotation(declaration: KtCallableDeclaration) : AddAnnotationFix(declaration, JVM_DEFAULT_FQ_NAME) {
override fun getText(): String = "Add '@JvmDefault' annotation"
override fun getFamilyName(): String = text override fun getFamilyName(): String = text
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
element?.addAnnotation(JVM_DEFAULT_FQ_NAME)
}
companion object : KotlinSingleIntentionActionFactory() { companion object : KotlinSingleIntentionActionFactory() {
override fun createAction(diagnostic: Diagnostic): IntentionAction? { override fun createAction(diagnostic: Diagnostic): IntentionAction? {
val psiElement = diagnostic.psiElement 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) JVM_DEFAULT_REQUIRED_FOR_OVERRIDE.registerFactory(AddJvmDefaultAnnotation)
NON_JVM_DEFAULT_OVERRIDES_JAVA_DEFAULT.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") @TestMetadata("idea/testData/quickfix/expressions")
@TestDataPath("$PROJECT_ROOT") @TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class) @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") @TestMetadata("idea/testData/quickfix/expressions")
@TestDataPath("$PROJECT_ROOT") @TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class) @RunWith(JUnit3RunnerWithInners.class)