[kotlin] Unify AddAnnotationWithArgumentsFix and AddAnnotationFix

...and also extract AddAnnotationWithClassLiteralArgumentFix

^KTIJ-29564

GitOrigin-RevId: d64aecebf8bc8c64aaaf2662eccbcbcbf53ff90b
This commit is contained in:
Andrey Cherkasov
2024-09-04 22:20:56 +04:00
committed by intellij-monorepo-bot
parent c800ad4282
commit 97879bc539
18 changed files with 109 additions and 160 deletions

View File

@@ -7,9 +7,7 @@ import org.jetbrains.idea.devkit.inspections.getProjectLevelFQN
import org.jetbrains.idea.devkit.inspections.quickfix.AddServiceAnnotationProvider
import org.jetbrains.kotlin.asJava.classes.KtLightClass
import org.jetbrains.kotlin.idea.quickfix.AddAnnotationFix
import org.jetbrains.kotlin.idea.quickfix.AddAnnotationWithArgumentsFix
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtClass
internal class KotlinAddServiceAnnotationProvider : AddServiceAnnotationProvider {
@@ -20,18 +18,14 @@ internal class KotlinAddServiceAnnotationProvider : AddServiceAnnotationProvider
else -> return
}
val file = ktClass?.containingFile ?: return
val annotationFqName = FqName(Service::class.java.canonicalName)
val annotationClassId = ClassId.fromString(Service::class.java.canonicalName)
val fix = when (level) {
Service.Level.APP -> {
val annotationClassId = ClassId.topLevel(annotationFqName)
AddAnnotationFix(ktClass, annotationClassId, AddAnnotationFix.Kind.Self)
}
Service.Level.PROJECT -> {
val kind = AddAnnotationWithArgumentsFix.Kind.Self
val projectLevelFqn = getProjectLevelFQN()
val arguments = listOf(projectLevelFqn)
AddAnnotationWithArgumentsFix(ktClass, annotationFqName, arguments, kind)
AddAnnotationFix(ktClass, annotationClassId, AddAnnotationFix.Kind.Self, listOf(getProjectLevelFQN()))
}
}
fix.invoke(file.project, null, file)

View File

@@ -16,7 +16,6 @@ import org.jetbrains.kotlin.idea.codeinsight.api.classic.quickfixes.PsiElementSu
import org.jetbrains.kotlin.idea.codeinsight.api.classic.quickfixes.QuickFixesPsiBasedFactory
import org.jetbrains.kotlin.idea.util.addAnnotation
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.StandardClassIds
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.renderer.render
@@ -26,26 +25,30 @@ open class AddAnnotationFix(
element: KtElement,
private val annotationClassId: ClassId,
private val kind: Kind = Kind.Self,
private val argumentClassFqName: FqName? = null,
private val arguments: List<String?> = emptyList(),
private val existingAnnotationEntry: SmartPsiElementPointer<KtAnnotationEntry>? = null
) : KotlinQuickFixAction<KtElement>(element) {
override fun getText(): String {
val annotationArguments = (argumentClassFqName?.shortName()?.let { "($it::class)" } ?: "")
val annotationCall = annotationClassId.shortClassName.asString() + annotationArguments
val annotationCall = annotationClassId.shortClassName.render() + renderArgumentsForIntentionName()
return when (kind) {
Kind.Self -> KotlinBundle.message("fix.add.annotation.text.self", annotationCall)
Kind.Constructor -> KotlinBundle.message("fix.add.annotation.text.constructor", annotationCall)
is Kind.Declaration -> KotlinBundle.message("fix.add.annotation.text.declaration", annotationCall, kind.name ?: "?")
is Kind.ContainingClass -> KotlinBundle.message("fix.add.annotation.text.containing.class", annotationCall, kind.name ?: "?")
is Kind.Copy -> KotlinBundle.message("fix.add.annotation.with.arguments.text.copy", annotationCall, kind.source, kind.target)
}
}
open fun renderArgumentsForIntentionName(): String {
return arguments.takeIf { it.isNotEmpty() }?.joinToString(", ", "(", ")") ?: ""
}
override fun getFamilyName(): String = KotlinBundle.message("fix.add.annotation.family")
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
val element = element ?: return
val annotationEntry = existingAnnotationEntry?.element
val annotationInnerText = argumentClassFqName?.let { "${it.render()}::class" }
val annotationInnerText = arguments.takeIf { it.isNotEmpty() }?.joinToString(", ")
if (annotationEntry != null) {
if (annotationInnerText == null) return
val psiFactory = KtPsiFactory(project)
@@ -58,10 +61,11 @@ open class AddAnnotationFix(
}
sealed class Kind {
object Self : Kind()
object Constructor : Kind()
class Declaration(val name: String?) : Kind()
class ContainingClass(val name: String?) : Kind()
data object Self : Kind()
data object Constructor : Kind()
data class Declaration(val name: String?) : Kind()
data class ContainingClass(val name: String?) : Kind()
data class Copy(val source: String, val target: String) : Kind()
}
object TypeVarianceConflictFactory :

View File

@@ -0,0 +1,28 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.quickfix
import com.intellij.psi.SmartPsiElementPointer
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.renderer.render
abstract class AddAnnotationWithClassLiteralArgumentFix(
element: KtElement,
annotationClassId: ClassId,
kind: Kind = Kind.Self,
existingAnnotationEntry: SmartPsiElementPointer<KtAnnotationEntry>? = null,
private val argumentClassFqName: FqName? = null,
) : AddAnnotationFix(
element,
annotationClassId,
kind,
listOfNotNull(argumentClassFqName?.let { "${it.render()}::class" }),
existingAnnotationEntry,
) {
override fun renderArgumentsForIntentionName(): String {
val shortName = argumentClassFqName?.shortName() ?: return ""
return "($shortName::class)"
}
}

View File

@@ -31,7 +31,7 @@ class OptInFixes {
private val kind: Kind,
private val argumentClassFqName: FqName,
existingAnnotationEntry: SmartPsiElementPointer<KtAnnotationEntry>? = null
) : AddAnnotationFix(element, optInClassId, kind, argumentClassFqName, existingAnnotationEntry) {
) : AddAnnotationWithClassLiteralArgumentFix(element, optInClassId, kind, existingAnnotationEntry, argumentClassFqName) {
override fun getText(): String {
val argumentText = argumentClassFqName.shortName().asString()
@@ -87,7 +87,7 @@ class OptInFixes {
private val kind: Kind,
private val argumentClassFqName: FqName? = null,
existingAnnotationEntry: SmartPsiElementPointer<KtAnnotationEntry>? = null
) : AddAnnotationFix(element, annotationClassId, Kind.Self, argumentClassFqName, existingAnnotationEntry) {
) : AddAnnotationWithClassLiteralArgumentFix(element, annotationClassId, Kind.Self, existingAnnotationEntry, argumentClassFqName) {
override fun getText(): String {
val annotationName = annotationClassId.shortClassName.asString()

View File

@@ -2,155 +2,78 @@
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 com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.SmartPsiElementPointer
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor
import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.codeinsight.api.classic.quickfixes.KotlinQuickFixAction
import org.jetbrains.kotlin.idea.util.addAnnotation
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtModifierListOwner
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.name.StandardClassIds
import org.jetbrains.kotlin.renderer.render
import org.jetbrains.kotlin.resolve.constants.*
import org.jetbrains.kotlin.resolve.constants.AnnotationValue
import org.jetbrains.kotlin.resolve.constants.ArrayValue
import org.jetbrains.kotlin.resolve.constants.EnumValue
import org.jetbrains.kotlin.resolve.constants.StringValue
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
// TODO: It might be useful to unify [AddAnnotationWithArgumentsFix] and [AddAnnotationFix] as they do almost the same thing
/**
* A quick fix to add an annotation with arbitrary arguments to a KtModifierListOwner element.
*
* This quick fix is similar to [AddAnnotationFix] but allows to use arbitrary strings as an inner text
* ([AddAnnotationFix] allows a single class argument).
*
* @param element the element to annotate
* @param annotationFqName the fully qualified name of the annotation class
* @param arguments the list of strings that should be added as the annotation arguments
* @param kind the action type that determines the action description text, see [Kind] for details
* @param useSiteTarget the optional use site target of the annotation (e.g., "@file:Annotation")
* @param existingAnnotationEntry existing annotation entry (it is updated if not null instead of creating the new annotation)
* A factory for `OVERRIDE_DEPRECATION` warning. It provides an action that copies the `@Deprecated` annotation
* from the ancestor's deprecated function/property to the overriding function/property in the derived class.
*/
class AddAnnotationWithArgumentsFix(
element: KtModifierListOwner,
private val annotationFqName: FqName,
private val arguments: List<String>,
private val kind: Kind = Kind.Self,
private val useSiteTarget: AnnotationUseSiteTarget? = null,
private val existingAnnotationEntry: SmartPsiElementPointer<KtAnnotationEntry>? = null
) : KotlinQuickFixAction<KtModifierListOwner>(element) {
internal object CopyDeprecatedAnnotation : KotlinIntentionActionsFactory() {
override fun doCreateActions(diagnostic: Diagnostic): List<IntentionAction> {
if (diagnostic.factory != Errors.OVERRIDE_DEPRECATION) return emptyList()
/**
* The way to specify the target (the declaration to which the annotation is added) and the source (the declaration
* from which the annotation, or its template, has been obtained).
*/
sealed class Kind {
/**
* No specification of source and target.
*/
object Self : Kind()
val deprecation = Errors.OVERRIDE_DEPRECATION.cast(diagnostic)
val declaration = deprecation.psiElement
return deprecation.c.mapNotNull {
val annotation = it.target.annotations.findAnnotation(StandardNames.FqNames.deprecated) ?: return@mapNotNull null
val arguments = formatDeprecatedAnnotationArguments(annotation)
val sourceName = renderName(it.target)
val destinationName = renderName(deprecation.b)
/**
* The target is specified, there is no explicit source.
*/
class Target(val target: String) : Kind()
/**
* Both source and target are specified.
*/
class Copy(val source: String, val target: String) : Kind()
}
override fun getText(): String {
val annotationName = annotationFqName.shortName().render()
return when (kind) {
Kind.Self -> KotlinBundle.message("fix.add.annotation.text.self", annotationName)
is Kind.Target -> KotlinBundle.message("fix.add.annotation.text.declaration", annotationName, kind.target)
is Kind.Copy -> KotlinBundle.message("fix.add.annotation.with.arguments.text.copy", annotationName, kind.source, kind.target)
object : AddAnnotationFix(
declaration,
StandardClassIds.Annotations.Deprecated,
Kind.Copy(sourceName, destinationName),
arguments,
) {
override fun renderArgumentsForIntentionName(): String = ""
}
}
}
override fun getFamilyName(): String = KotlinBundle.message("fix.add.annotation.family")
private val MESSAGE_ARGUMENT = Name.identifier("message")
private val REPLACE_WITH_ARGUMENT = Name.identifier("replaceWith")
private val LEVEL_ARGUMENT = Name.identifier("level")
private val EXPRESSION_ARGUMENT = Name.identifier("expression")
private val IMPORTS_ARGUMENT = Name.identifier("imports")
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
val declaration = element ?: return
val innerText = if (arguments.isNotEmpty()) arguments.joinToString() else null
val entry = existingAnnotationEntry?.element
if (entry != null) {
if (innerText != null) {
val psiFactory = KtPsiFactory(project)
entry.valueArgumentList?.addArgument(psiFactory.createArgument(innerText))
?: entry.addAfter(psiFactory.createCallArguments("($innerText)"), entry.lastChild)
// A custom pretty-printer for the `@Deprecated` annotation that deals with optional named arguments and varargs.
private fun formatDeprecatedAnnotationArguments(annotation: AnnotationDescriptor): List<String> {
val arguments = mutableListOf<String>()
annotation.allValueArguments[MESSAGE_ARGUMENT]?.safeAs<StringValue>()
?.toString()
?.removeSurrounding("\"")
?.let { arguments.add("\"${StringUtil.escapeStringCharacters(it)}\"") }
val replaceWith = annotation.allValueArguments[REPLACE_WITH_ARGUMENT]?.safeAs<AnnotationValue>()?.value
if (replaceWith != null) {
val expression = replaceWith.allValueArguments[EXPRESSION_ARGUMENT]?.safeAs<StringValue>()?.toString()
val imports = replaceWith.allValueArguments[IMPORTS_ARGUMENT]?.safeAs<ArrayValue>()?.value
val importsArg = if (imports.isNullOrEmpty()) "" else (", " + imports.joinToString { it.toString() })
if (expression != null) {
arguments.add("replaceWith = ReplaceWith(${expression}${importsArg})")
}
} else {
declaration.addAnnotation(annotationFqName, innerText, useSiteTarget = useSiteTarget, searchForExistingEntry = false)
}
annotation.allValueArguments[LEVEL_ARGUMENT]?.safeAs<EnumValue>()?.let { arguments.add("level = $it") }
return arguments
}
/**
* A factory for `OVERRIDE_DEPRECATION` warning. It provides an action that copies the `@Deprecated` annotation
* from the ancestor's deprecated function/property to the overriding function/property in the derived class.
*/
object CopyDeprecatedAnnotation : KotlinIntentionActionsFactory() {
override fun doCreateActions(diagnostic: Diagnostic): List<IntentionAction> {
if (diagnostic.factory != Errors.OVERRIDE_DEPRECATION) return emptyList()
val deprecation = Errors.OVERRIDE_DEPRECATION.cast(diagnostic)
val declaration = deprecation.psiElement
return deprecation.c.mapNotNull {
val annotation = it.target.annotations.findAnnotation(StandardNames.FqNames.deprecated) ?: return@mapNotNull null
val arguments = formatDeprecatedAnnotationArguments(annotation)
val sourceName = renderName(it.target)
val destinationName = renderName(deprecation.b)
AddAnnotationWithArgumentsFix(
declaration,
StandardNames.FqNames.deprecated,
arguments,
kind = Kind.Copy(sourceName, destinationName)
)
}
}
private val MESSAGE_ARGUMENT = Name.identifier("message")
private val REPLACE_WITH_ARGUMENT = Name.identifier("replaceWith")
private val LEVEL_ARGUMENT = Name.identifier("level")
private val EXPRESSION_ARGUMENT = Name.identifier("expression")
private val IMPORTS_ARGUMENT = Name.identifier("imports")
// A custom pretty-printer for the `@Deprecated` annotation that deals with optional named arguments and varargs.
private fun formatDeprecatedAnnotationArguments(annotation: AnnotationDescriptor): List<String> {
val arguments = mutableListOf<String>()
annotation.allValueArguments[MESSAGE_ARGUMENT]?.safeAs<StringValue>()
?.toString()
?.removeSurrounding("\"")
?.let { arguments.add("\"${StringUtil.escapeStringCharacters(it)}\"") }
val replaceWith = annotation.allValueArguments[REPLACE_WITH_ARGUMENT]?.safeAs<AnnotationValue>()?.value
if (replaceWith != null) {
val expression = replaceWith.allValueArguments[EXPRESSION_ARGUMENT]?.safeAs<StringValue>()?.toString()
val imports = replaceWith.allValueArguments[IMPORTS_ARGUMENT]?.safeAs<ArrayValue>()?.value
val importsArg = if (imports.isNullOrEmpty()) "" else (", " + imports.joinToString { it.toString() })
if (expression != null) {
arguments.add("replaceWith = ReplaceWith(${expression}${importsArg})")
}
}
annotation.allValueArguments[LEVEL_ARGUMENT]?.safeAs<EnumValue>()?.let { arguments.add("level = $it") }
return arguments
}
// A renderer for function/property names: uses qualified names when available to disambiguate names of overrides
private fun renderName(descriptor: DeclarationDescriptor): String {
val containerPrefix = descriptor.containingDeclaration?.let { "${it.name.render()}." } ?: ""
val name = descriptor.name.render()
return containerPrefix + name
}
// A renderer for function/property names: uses qualified names when available to disambiguate names of overrides
private fun renderName(descriptor: DeclarationDescriptor): String {
val containerPrefix = descriptor.containingDeclaration?.let { "${it.name.render()}." } ?: ""
val name = descriptor.name.render()
return containerPrefix + name
}
}

View File

@@ -776,7 +776,7 @@ class QuickFixRegistrar : QuickFixContributor {
IS_ENUM_ENTRY.registerFactory(IsEnumEntryFactory)
OVERRIDE_DEPRECATION.registerFactory(AddAnnotationWithArgumentsFix.CopyDeprecatedAnnotation)
OVERRIDE_DEPRECATION.registerFactory(CopyDeprecatedAnnotation)
NULLABLE_TYPE_PARAMETER_AGAINST_NOT_NULL_TYPE_PARAMETER.registerFactory(MakeUpperBoundNonNullableFix)
WRONG_TYPE_PARAMETER_NULLABILITY_FOR_JAVA_OVERRIDE.registerFactory(MakeUpperBoundNonNullableFix)

View File

@@ -10,4 +10,4 @@ class Derived : Base() {
override fun <caret>foo() {}
}
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.AddAnnotationWithArgumentsFix
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.CopyDeprecatedAnnotation$doCreateActions$1$1

View File

@@ -11,4 +11,4 @@ class Derived : Base() {
override fun foo() {}
}
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.AddAnnotationWithArgumentsFix
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.CopyDeprecatedAnnotation$doCreateActions$1$1

View File

@@ -10,4 +10,4 @@ class Derived : Base() {
override fun <caret>foo() {}
}
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.AddAnnotationWithArgumentsFix
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.CopyDeprecatedAnnotation$doCreateActions$1$1

View File

@@ -11,4 +11,4 @@ class Derived : Base() {
override fun foo() {}
}
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.AddAnnotationWithArgumentsFix
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.CopyDeprecatedAnnotation$doCreateActions$1$1

View File

@@ -12,4 +12,4 @@ class Derived : Base() {
override fun <caret>foo() {}
}
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.AddAnnotationWithArgumentsFix
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.CopyDeprecatedAnnotation$doCreateActions$1$1

View File

@@ -13,4 +13,4 @@ class Derived : Base() {
override fun foo() {}
}
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.AddAnnotationWithArgumentsFix
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.CopyDeprecatedAnnotation$doCreateActions$1$1

View File

@@ -12,4 +12,4 @@ class Derived : Base() {
override fun <caret>foo() {}
}
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.AddAnnotationWithArgumentsFix
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.CopyDeprecatedAnnotation$doCreateActions$1$1

View File

@@ -13,4 +13,4 @@ class Derived : Base() {
override fun foo() {}
}
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.AddAnnotationWithArgumentsFix
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.CopyDeprecatedAnnotation$doCreateActions$1$1

View File

@@ -12,4 +12,4 @@ class Derived : Base() {
set(value) {}
}
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.AddAnnotationWithArgumentsFix
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.CopyDeprecatedAnnotation$doCreateActions$1$1

View File

@@ -13,4 +13,4 @@ class Derived : Base() {
set(value) {}
}
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.AddAnnotationWithArgumentsFix
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.CopyDeprecatedAnnotation$doCreateActions$1$1

View File

@@ -10,4 +10,4 @@ class MyImplementation : MyInterface {
override fun <caret>deprecatedFun() {
}
}
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.AddAnnotationWithArgumentsFix
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.CopyDeprecatedAnnotation$doCreateActions$1$1

View File

@@ -11,4 +11,4 @@ class MyImplementation : MyInterface {
override fun deprecatedFun() {
}
}
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.AddAnnotationWithArgumentsFix
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.quickfix.CopyDeprecatedAnnotation$doCreateActions$1$1