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
) {
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)
if (annotated is KtCallableDeclaration) {
annotated.typeReference?.let { check(it, trace) }
@@ -64,7 +64,7 @@ class AnnotationChecker(
}
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) {
for (parameter in expression.valueParameters) {
parameter.typeReference?.let { check(it, trace) }
@@ -197,15 +197,15 @@ class AnnotationChecker(
}.toSet()
}
fun getDeclarationSiteActualTargetList(annotated: KtElement, descriptor: ClassDescriptor?, trace: BindingTrace):
fun getDeclarationSiteActualTargetList(annotated: KtElement, descriptor: ClassDescriptor?, context: BindingContext):
List<KotlinTarget> {
return getActualTargetList(annotated, descriptor, trace).defaultTargets
return getActualTargetList(annotated, descriptor, context).defaultTargets
}
private fun DeclarationDescriptor?.hasBackingField(bindingTrace: BindingTrace) =
(this as? PropertyDescriptor)?.let { bindingTrace.get(BindingContext.BACKING_FIELD_REQUIRED, it) } ?: false
private fun DeclarationDescriptor?.hasBackingField(context: BindingContext) =
(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) {
is KtClassOrObject ->
(descriptor as? ClassDescriptor)?.let { TargetList(KotlinTarget.classActualTargets(it)) } ?: TargetLists.T_CLASSIFIER
@@ -213,8 +213,8 @@ class AnnotationChecker(
is KtProperty -> {
when {
annotated.isLocal -> TargetLists.T_LOCAL_VARIABLE
annotated.isMember -> TargetLists.T_MEMBER_PROPERTY(descriptor.hasBackingField(trace), annotated.hasDelegate())
else -> TargetLists.T_TOP_LEVEL_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(context), annotated.hasDelegate())
}
}
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
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>()
?: 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)