Added inspection for duplicating serial names in enumeration entries

Resolves Kotlin/kotlinx.serialization#1852
This commit is contained in:
Sergey.Shanshin
2022-08-17 20:17:22 +00:00
committed by Space
parent 6c4777dcdd
commit 2cb6483e8a
7 changed files with 80 additions and 6 deletions
@@ -25,6 +25,7 @@ public interface SerializationErrors {
DiagnosticFactory0<KtAnnotationEntry> NON_SERIALIZABLE_PARENT_MUST_HAVE_NOARG_CTOR = DiagnosticFactory0.create(ERROR);
DiagnosticFactory0<KtAnnotationEntry> PRIMARY_CONSTRUCTOR_PARAMETER_IS_NOT_A_PROPERTY = DiagnosticFactory0.create(ERROR);
DiagnosticFactory1<KtAnnotationEntry, String> DUPLICATE_SERIAL_NAME = DiagnosticFactory1.create(ERROR);
DiagnosticFactory3<PsiElement, KotlinType, String, String> DUPLICATE_SERIAL_NAME_ENUM = DiagnosticFactory3.create(ERROR);
DiagnosticFactory1<PsiElement, KotlinType> SERIALIZER_NOT_FOUND = DiagnosticFactory1.create(ERROR);
DiagnosticFactory2<PsiElement, KotlinType, KotlinType> SERIALIZER_NULLABILITY_INCOMPATIBLE = DiagnosticFactory2.create(ERROR);
DiagnosticFactory3<PsiElement, KotlinType, KotlinType, KotlinType> SERIALIZER_TYPE_INCOMPATIBLE = DiagnosticFactory3.create(WARNING);
@@ -27,9 +27,8 @@ import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.typeUtil.supertypes
import org.jetbrains.kotlin.util.slicedMap.Slices
import org.jetbrains.kotlin.util.slicedMap.WritableSlice
import org.jetbrains.kotlinx.serialization.compiler.backend.common.AbstractSerialGenerator
import org.jetbrains.kotlinx.serialization.compiler.backend.common.*
import org.jetbrains.kotlinx.serialization.compiler.backend.common.bodyPropertiesDescriptorsMap
import org.jetbrains.kotlinx.serialization.compiler.backend.common.findTypeSerializerOrContextUnchecked
import org.jetbrains.kotlinx.serialization.compiler.backend.common.primaryConstructorPropertiesDescriptorsMap
import org.jetbrains.kotlinx.serialization.compiler.resolve.*
@@ -41,6 +40,7 @@ open class SerializationPluginDeclarationChecker : DeclarationChecker {
final override fun check(declaration: KtDeclaration, descriptor: DeclarationDescriptor, context: DeclarationCheckerContext) {
if (descriptor !is ClassDescriptor) return
checkEnum(descriptor, declaration, context.trace)
checkExternalSerializer(descriptor, declaration, context.trace)
if (!canBeSerializedInternally(descriptor, declaration, context.trace)) return
@@ -164,7 +164,7 @@ open class SerializationPluginDeclarationChecker : DeclarationChecker {
private fun canBeSerializedInternally(descriptor: ClassDescriptor, declaration: KtDeclaration, trace: BindingTrace): Boolean {
// if enum has meta or SerialInfo annotation on a class or entries and used plugin-generated serializer
if (descriptor.isSerializableEnumWithMissingSerializer() && descriptor.useLegacyGeneratedEnumSerializer()) {
if (descriptor.useLegacyGeneratedEnumSerializer() && descriptor.isSerializableEnumWithMissingSerializer()) {
val declarationToReport = declaration.modifierList ?: declaration
trace.report(SerializationErrors.EXPLICIT_SERIALIZABLE_IS_REQUIRED.on(declarationToReport))
return false
@@ -239,6 +239,30 @@ open class SerializationPluginDeclarationChecker : DeclarationChecker {
return false
}
private fun checkEnum(descriptor: ClassDescriptor, declaration: KtDeclaration, trace: BindingTrace) {
if (descriptor.kind != ClassKind.ENUM_CLASS) return
val entryBySerialName = mutableMapOf<String, ClassDescriptor?>()
descriptor.enumEntries().forEach { entryDescriptor ->
val serialNameAnnotation = entryDescriptor.annotations.serialNameAnnotation
val serialName = entryDescriptor.annotations.serialNameValue ?: entryDescriptor.name.asString()
val firstEntry = entryBySerialName[serialName]
if (firstEntry != null) {
trace.report(
SerializationErrors.DUPLICATE_SERIAL_NAME_ENUM.on(
serialNameAnnotation?.findAnnotationEntry() ?: firstEntry.annotations.serialNameAnnotation?.findAnnotationEntry()
?: declaration,
descriptor.defaultType,
serialName,
entryDescriptor.name.asString()
)
)
} else {
entryBySerialName[serialName] = entryDescriptor
}
}
}
private fun ClassDescriptor.isSerializableEnumWithMissingSerializer(): Boolean {
if (kind != ClassKind.ENUM_CLASS) return false
if (hasSerializableOrMetaAnnotation) return false
@@ -56,6 +56,13 @@ object SerializationPluginErrorsRendering : DefaultErrorMessages.Extension {
"Serializable class has duplicate serial name of property ''{0}'', either in the class itself or its supertypes",
CommonRenderers.STRING
)
MAP.put(
SerializationErrors.DUPLICATE_SERIAL_NAME_ENUM,
"Enum class ''{0}'' has duplicate serial name ''{1}'' in entry ''{2}''",
Renderers.RENDER_TYPE,
CommonRenderers.STRING,
CommonRenderers.STRING
)
MAP.put(
SerializationErrors.SERIALIZER_NOT_FOUND,
"Serializer has not been found for type ''{0}''. " +
@@ -88,6 +88,9 @@ internal val ClassDescriptor.isInheritableSerialInfoAnnotation: Boolean
internal val Annotations.serialNameValue: String?
get() = findAnnotationConstantValue(SerializationAnnotations.serialNameAnnotationFqName, "value")
internal val Annotations.serialNameAnnotation: AnnotationDescriptor?
get() = findAnnotation(SerializationAnnotations.serialNameAnnotationFqName)
internal val Annotations.serialRequired: Boolean
get() = hasAnnotation(SerializationAnnotations.requiredAnnotationFqName)
@@ -7,10 +7,12 @@ package org.jetbrains.kotlinx.serialization.compiler.resolve
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.annotations.Annotated
import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor
import org.jetbrains.kotlin.descriptors.annotations.Annotations
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.ValueArgument
import org.jetbrains.kotlin.resolve.constants.KClassValue
import org.jetbrains.kotlin.resolve.descriptorUtil.classId
@@ -46,10 +48,13 @@ internal fun findSerializerConstructorForTypeArgumentsSerializers(
return if (!onlyIfSynthetic) ctor else ctor?.takeIf { it.kind == CallableMemberDescriptor.Kind.SYNTHESIZED }
}
fun AnnotationDescriptor.findAnnotationEntry(): KtAnnotationEntry? = (this as? LazyAnnotationDescriptor)?.annotationEntry
inline fun <reified R> Annotations.findAnnotationConstantValue(annotationFqName: FqName, property: String): R? =
findAnnotation(annotationFqName)?.let { annotation ->
annotation.allValueArguments.entries.singleOrNull { it.key.asString() == property }?.value?.value
} as? R
findAnnotation(annotationFqName)?.findConstantValue(property)
inline fun <reified R> AnnotationDescriptor.findConstantValue(property: String): R? =
allValueArguments.entries.singleOrNull { it.key.asString() == property }?.value?.value as? R
internal fun Annotations.findAnnotationKotlinTypeValue(
annotationFqName: FqName,
@@ -34,6 +34,11 @@ public class SerializationPluginDiagnosticTestGenerated extends AbstractSerializ
runTest("plugins/kotlin-serialization/kotlin-serialization-compiler/testData/diagnostics/DuplicateSerialName.kt");
}
@TestMetadata("EnumDuplicateSerialName.kt")
public void testEnumDuplicateSerialName() throws Exception {
runTest("plugins/kotlin-serialization/kotlin-serialization-compiler/testData/diagnostics/EnumDuplicateSerialName.kt");
}
@TestMetadata("ExternalSerializers.kt")
public void testExternalSerializers() throws Exception {
runTest("plugins/kotlin-serialization/kotlin-serialization-compiler/testData/diagnostics/ExternalSerializers.kt");
@@ -0,0 +1,29 @@
// !DIAGNOSTICS: -UNUSED_PARAMETER,-UNUSED_VARIABLE
// WITH_STDLIB
// SKIP_TXT
import kotlinx.serialization.*
@Serializable
enum class ImplicitlyDuplicated {
@SerialName("foo")
FIRST,
<!DUPLICATE_SERIAL_NAME_ENUM!>@SerialName("foo")<!>
SECOND
}
@Serializable
enum class ExplicitlyDuplicated {
FIRST,
SECOND,
<!DUPLICATE_SERIAL_NAME_ENUM!>@SerialName("FIRST")<!>
THIRD
}
@Serializable
enum class ReversedExplicitlyDuplicated {
<!DUPLICATE_SERIAL_NAME_ENUM!>@SerialName("THIRD")<!>
FIRST,
SECOND,
THIRD
}