diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationErrors.java b/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationErrors.java index 657508fba87..2eb5afdd21c 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationErrors.java +++ b/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationErrors.java @@ -39,6 +39,8 @@ public interface SerializationErrors { DiagnosticFactory2 ABSTRACT_SERIALIZER_TYPE = DiagnosticFactory2.create(ERROR); DiagnosticFactory3 SERIALIZER_TYPE_INCOMPATIBLE = DiagnosticFactory3.create(WARNING); DiagnosticFactory1 LOCAL_SERIALIZER_USAGE = DiagnosticFactory1.create(ERROR); + DiagnosticFactory3 CUSTOM_SERIALIZER_PARAM_ILLEGAL_COUNT = DiagnosticFactory3.create(ERROR); + DiagnosticFactory3 CUSTOM_SERIALIZER_PARAM_ILLEGAL_TYPE = DiagnosticFactory3.create(ERROR); DiagnosticFactory0 TRANSIENT_MISSING_INITIALIZER = DiagnosticFactory0.create(ERROR); DiagnosticFactory0 GENERIC_ARRAY_ELEMENT_NOT_SUPPORTED = DiagnosticFactory0.create(ERROR); @@ -61,6 +63,8 @@ public interface SerializationErrors { DiagnosticFactory2 EXTERNAL_CLASS_NOT_SERIALIZABLE = DiagnosticFactory2.create(ERROR); DiagnosticFactory2 EXTERNAL_CLASS_IN_ANOTHER_MODULE = DiagnosticFactory2.create(ERROR); + DiagnosticFactory3 EXTERNAL_SERIALIZER_NO_SUITABLE_CONSTRUCTOR = DiagnosticFactory3.create(ERROR); + @SuppressWarnings("UnusedDeclaration") Object _initializer = new Object() { diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginDeclarationChecker.kt b/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginDeclarationChecker.kt index e95ee9fbf84..0587987b4e7 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginDeclarationChecker.kt +++ b/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginDeclarationChecker.kt @@ -27,12 +27,14 @@ import org.jetbrains.kotlin.resolve.lazy.descriptors.LazyAnnotationDescriptor import org.jetbrains.kotlin.resolve.source.getPsi import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.typeUtil.isEnum +import org.jetbrains.kotlin.types.typeUtil.isTypeParameter 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.* import org.jetbrains.kotlinx.serialization.compiler.backend.common.bodyPropertiesDescriptorsMap import org.jetbrains.kotlinx.serialization.compiler.backend.common.primaryConstructorPropertiesDescriptorsMap +import org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationErrors.EXTERNAL_SERIALIZER_NO_SUITABLE_CONSTRUCTOR import org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationErrors.EXTERNAL_SERIALIZER_USELESS import org.jetbrains.kotlinx.serialization.compiler.resolve.* import org.jetbrains.kotlinx.serialization.compiler.resolve.SerialEntityNames.LOAD_NAME @@ -89,6 +91,25 @@ open class SerializationPluginDeclarationChecker : DeclarationChecker { val serializableDescriptor = serializableKType.toClassDescriptor ?: return val props = SerializableProperties(serializableDescriptor, trace.bindingContext) + val parametersCount = serializableKType.arguments.size + if (parametersCount > 0) { + val hasSuitableConstructor = classDescriptor.constructors.any { constructor -> + constructor.valueParameters.size == parametersCount + && constructor.valueParameters.all { param -> isKSerializer(param.type) } + } + + if (!hasSuitableConstructor) { + trace.report( + EXTERNAL_SERIALIZER_NO_SUITABLE_CONSTRUCTOR.on( + declaration, + classDescriptor.defaultType, + serializableKType, + parametersCount.toString() + ) + ) + } + } + val descriptorOverridden = classDescriptor.unsubstitutedMemberScope .getContributedVariables(SERIAL_DESC_FIELD_NAME, NoLookupLocation.FROM_BACKEND).singleOrNull { it.kind != CallableMemberDescriptor.Kind.SYNTHESIZED @@ -325,6 +346,7 @@ open class SerializationPluginDeclarationChecker : DeclarationChecker { val annotationPsi = descriptor.findSerializableOrMetaAnnotationDeclaration() checkCustomSerializerMatch(descriptor.module, descriptor.defaultType, descriptor, annotationPsi, trace, declaration) checkCustomSerializerIsNotLocal(descriptor.module, descriptor, trace, declaration) + checkCustomSerializerParameters(descriptor.module, descriptor, descriptor.defaultType, annotationPsi, declaration, trace) checkCustomSerializerNotAbstract(descriptor.module, descriptor.defaultType, descriptor, annotationPsi, trace, declaration) } @@ -441,14 +463,9 @@ open class SerializationPluginDeclarationChecker : DeclarationChecker { if (serializer != null) { val element = ktType?.typeElement checkCustomSerializerMatch(it.module, it.type, it.descriptor, element, trace, propertyPsi) - checkCustomSerializerNotAbstract( - it.module, - it.type, - it.descriptor, - it.descriptor.findSerializableOrMetaAnnotationDeclaration(), - trace, - propertyPsi - ) + val annotationPsi = it.descriptor.findSerializableOrMetaAnnotationDeclaration() + checkCustomSerializerNotAbstract(it.module, it.type, it.descriptor, annotationPsi, trace, propertyPsi) + checkCustomSerializerParameters(it.module, it.descriptor, it.type, annotationPsi, propertyPsi, trace) checkCustomSerializerIsNotLocal(it.module, it.descriptor, trace, propertyPsi) checkSerializerNullability(it.type, serializer.defaultType, element, trace, propertyPsi) generatorContextForAnalysis.checkTypeArguments(it.module, it.type, element, trace, propertyPsi) @@ -516,8 +533,14 @@ open class SerializationPluginDeclarationChecker : DeclarationChecker { } val serializer = findTypeSerializerOrContextUnchecked(module, type) if (serializer != null) { - checkCustomSerializerMatch(module, type, type, element, trace, fallbackElement) - checkCustomSerializerIsNotLocal(module, type, trace, fallbackElement) + type.annotations.serializableWith(module)?.let { + checkCustomSerializerMatch(module, type, type, element, trace, fallbackElement) + checkCustomSerializerIsNotLocal(module, type, trace, fallbackElement) + + val annotationElement = type.findSerializableAnnotationDeclaration() + checkCustomSerializerParameters(module, type, type, annotationElement, fallbackElement, trace) + checkCustomSerializerNotAbstract(module, type, type, annotationElement, trace, fallbackElement) + } checkSerializerNullability(type, serializer.defaultType, element, trace, fallbackElement) checkTypeArguments(module, type, element, trace, fallbackElement) } else { @@ -591,6 +614,63 @@ open class SerializationPluginDeclarationChecker : DeclarationChecker { } } + private fun checkCustomSerializerParameters( + module: ModuleDescriptor, + declaration: Annotated, + serializableType: KotlinType, + element: KtElement?, + fallbackElement: PsiElement, + trace: BindingTrace, + ) { + val serializerType = declaration.annotations.serializableWith(module) ?: return + val serializerDescriptor = serializerType.toClassDescriptor ?: return + + if (serializerDescriptor.classId in SerializersClassIds.setOfSpecialSerializers) { + return + } + + val primaryConstructor = serializerDescriptor.constructors.singleOrNull { constructor -> constructor.isPrimary } ?: return + + val targetElement = element ?: fallbackElement + + val isExternalSerializer = serializerDescriptor.serializerForClass != null + if ( // for external serializer, the verification will be carried out at the definition + !isExternalSerializer + // it is allowed that parameters are not passed to regular serializers at all + && primaryConstructor.valueParameters.isNotEmpty() + // if the parameters are still specified, then their number must match in the serializable class and constructor + && primaryConstructor.valueParameters.size != serializableType.arguments.size + ) { + val message = if (serializableType.arguments.isNotEmpty()) { + "expected no parameters or ${serializableType.arguments.size}, but has ${primaryConstructor.valueParameters.size} parameters" + } else { + "expected no parameters but has ${primaryConstructor.valueParameters.size} parameters" + } + + trace.report( + SerializationErrors.CUSTOM_SERIALIZER_PARAM_ILLEGAL_COUNT.on( + targetElement, + serializerType, + serializableType, + message + ) + ) + } + + primaryConstructor.valueParameters.forEach { param -> + if (!isKSerializer(param.type)) { + trace.report( + SerializationErrors.CUSTOM_SERIALIZER_PARAM_ILLEGAL_TYPE.on( + targetElement, + serializerType, + serializableType, + param.name.asString() + ) + ) + } + } + } + private fun checkSerializerNullability( classType: KotlinType, serializerType: KotlinType, diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginErrorsRendering.kt b/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginErrorsRendering.kt index f0fca2453ec..778c680b716 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginErrorsRendering.kt +++ b/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/diagnostic/SerializationPluginErrorsRendering.kt @@ -118,6 +118,20 @@ object SerializationPluginErrorsRendering : DefaultErrorMessages.Extension { "Class ''{0}'' can't be used as a serializer since it is local", Renderers.RENDER_TYPE ) + MAP.put( + SerializationErrors.CUSTOM_SERIALIZER_PARAM_ILLEGAL_COUNT, + "Custom serializer ''{0}'' can not be used for ''{1}'' since it has an invalid number of parameters in primary constructor: {2}", + Renderers.RENDER_TYPE, + Renderers.RENDER_TYPE, + CommonRenderers.STRING + ) + MAP.put( + SerializationErrors.CUSTOM_SERIALIZER_PARAM_ILLEGAL_TYPE, + "Custom serializer ''{0}'' can not be used for ''{1}'', type of parameter ''{2}'' in serializer's primary constructor should be ''KSerializer''", + Renderers.RENDER_TYPE, + Renderers.RENDER_TYPE, + CommonRenderers.STRING + ) MAP.put( SerializationErrors.TRANSIENT_MISSING_INITIALIZER, "This property is marked as @Transient and therefore must have an initializing expression" @@ -194,5 +208,13 @@ object SerializationPluginErrorsRendering : DefaultErrorMessages.Extension { Renderers.RENDER_TYPE, Renderers.RENDER_TYPE ) + + MAP.put( + SerializationErrors.EXTERNAL_SERIALIZER_NO_SUITABLE_CONSTRUCTOR, + "Cannot generate external serializer ''{0}'': it must have a constructor with {2} value parameters, because class ''{1}'' has type parameters", + Renderers.RENDER_TYPE, + Renderers.RENDER_TYPE, + CommonRenderers.STRING + ) } } diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/resolve/KSerializationUtil.kt b/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/resolve/KSerializationUtil.kt index e5e77d44004..3db89a89935 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/resolve/KSerializationUtil.kt +++ b/plugins/kotlinx-serialization/kotlinx-serialization.k1/src/org/jetbrains/kotlinx/serialization/compiler/resolve/KSerializationUtil.kt @@ -212,7 +212,7 @@ private val ClassDescriptor.hasSerializableAnnotationWithArgs: Boolean return psi.valueArguments.isNotEmpty() } -private fun Annotated.findSerializableAnnotationDeclaration(): KtAnnotationEntry? { +internal fun Annotated.findSerializableAnnotationDeclaration(): KtAnnotationEntry? { val lazyDesc = annotations.findAnnotation(serializableAnnotationFqName) as? LazyAnnotationDescriptor return lazyDesc?.annotationEntry } diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/checkers/FirSerializationErrors.kt b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/checkers/FirSerializationErrors.kt index 76f02762508..6579b838984 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/checkers/FirSerializationErrors.kt +++ b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/checkers/FirSerializationErrors.kt @@ -36,6 +36,9 @@ object FirSerializationErrors { val SERIALIZER_TYPE_INCOMPATIBLE by warning3() val ABSTRACT_SERIALIZER_TYPE by error2() val LOCAL_SERIALIZER_USAGE by error1() + val CUSTOM_SERIALIZER_PARAM_ILLEGAL_COUNT by error3() + val CUSTOM_SERIALIZER_PARAM_ILLEGAL_TYPE by error3() + val GENERIC_ARRAY_ELEMENT_NOT_SUPPORTED by error0() val TRANSIENT_MISSING_INITIALIZER by error0() @@ -52,6 +55,7 @@ object FirSerializationErrors { val EXTERNAL_SERIALIZER_USELESS by warning1>() val EXTERNAL_CLASS_NOT_SERIALIZABLE by error2, ConeKotlinType>() val EXTERNAL_CLASS_IN_ANOTHER_MODULE by error2, ConeKotlinType>() + val EXTERNAL_SERIALIZER_NO_SUITABLE_CONSTRUCTOR by error3, ConeKotlinType, String>() init { RootDiagnosticRendererFactory.registerFactory(KtDefaultErrorMessagesSerialization) diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/checkers/FirSerializationPluginClassChecker.kt b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/checkers/FirSerializationPluginClassChecker.kt index 5a9fd20c234..11d31cbd3e4 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/checkers/FirSerializationPluginClassChecker.kt +++ b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/checkers/FirSerializationPluginClassChecker.kt @@ -14,6 +14,9 @@ import org.jetbrains.kotlin.diagnostics.reportOn import org.jetbrains.kotlin.fir.analysis.checkers.* import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext import org.jetbrains.kotlin.fir.analysis.checkers.declaration.FirClassChecker +import org.jetbrains.kotlin.fir.analysis.checkers.declaration.primaryConstructorSymbol +import org.jetbrains.kotlin.fir.analysis.checkers.isSingleFieldValueClass +import org.jetbrains.kotlin.fir.analysis.checkers.toRegularClassSymbol import org.jetbrains.kotlin.fir.declarations.* import org.jetbrains.kotlin.fir.declarations.annotationPlatformSupport import org.jetbrains.kotlin.fir.declarations.utils.* @@ -30,6 +33,7 @@ import org.jetbrains.kotlin.resolve.jvm.annotations.TRANSIENT_ANNOTATION_CLASS_I import org.jetbrains.kotlin.types.AbstractTypeChecker import org.jetbrains.kotlinx.serialization.compiler.diagnostic.RuntimeVersions import org.jetbrains.kotlinx.serialization.compiler.fir.* +import org.jetbrains.kotlinx.serialization.compiler.fir.checkers.FirSerializationErrors.EXTERNAL_SERIALIZER_NO_SUITABLE_CONSTRUCTOR import org.jetbrains.kotlinx.serialization.compiler.fir.checkers.FirSerializationErrors.EXTERNAL_SERIALIZER_USELESS import org.jetbrains.kotlinx.serialization.compiler.fir.services.dependencySerializationInfoProvider import org.jetbrains.kotlinx.serialization.compiler.fir.services.findTypeSerializerOrContextUnchecked @@ -91,6 +95,25 @@ object FirSerializationPluginClassChecker : FirClassChecker(MppCheckerKind.Commo val serializableClassSymbol = serializableKType.toRegularClassSymbol(session) ?: return val declarations = classSymbol.declarationSymbols + + val parametersCount = serializableKType.typeArguments.size + if (parametersCount > 0) { + val hasSuitableConstructor = declarations.filterIsInstance().any { constructor -> + constructor.valueParameterSymbols.size == parametersCount + && constructor.valueParameterSymbols.all { param -> param.resolvedReturnType.isKSerializer } + } + + if (!hasSuitableConstructor) { + reporter.reportOn( + classSymbol.source, + EXTERNAL_SERIALIZER_NO_SUITABLE_CONSTRUCTOR, + classSymbol, + serializableKType, + parametersCount.toString() + ) + } + } + val descriptorOverridden = declarations.filterIsInstance().singleOrNull { it.name == SerialEntityNames.SERIAL_DESC_FIELD_NAME && it.isOverride @@ -361,8 +384,12 @@ object FirSerializationPluginClassChecker : FirClassChecker(MppCheckerKind.Commo context(CheckerContext) private fun checkClassWithCustomSerializer(classSymbol: FirClassSymbol<*>, reporter: DiagnosticReporter) { val serializerType = classSymbol.getSerializableWith(session)?.fullyExpandedType(session) ?: return - checkCustomSerializerMatch(classSymbol, source = null, classSymbol.defaultType(), serializerType, reporter) + + val serializerForType = serializerType.serializerForType(session)?.fullyExpandedType(session) + + checkCustomSerializerMatch(classSymbol, source = null, classSymbol.defaultType(), serializerType, serializerForType, reporter) checkCustomSerializerIsNotLocal(source = null, classSymbol, serializerType, reporter) + checkCustomSerializerParameters(classSymbol, null, serializerType, serializerForType, reporter) checkCustomSerializerNotAbstract(classSymbol, source = null, serializerType, reporter) } @@ -467,25 +494,20 @@ object FirSerializationPluginClassChecker : FirClassChecker(MppCheckerKind.Commo // and would not be compatible on direct comparison if (customSerializerType.classId in SerializersClassIds.setOfSpecialSerializers) return + val serializerForType = customSerializerType.serializerForType(session)?.fullyExpandedType(session) + checkCustomSerializerMatch( classSymbol, source = typeRef.source ?: propertySymbol.source, propertyType, customSerializerType, + serializerForType, reporter ) - checkCustomSerializerNotAbstract( - classSymbol, - source = propertySymbol.serializableAnnotation(needArguments = false, session)?.source, - customSerializerType, - reporter - ) - checkCustomSerializerIsNotLocal( - source = propertySymbol.serializableAnnotation(needArguments = false, session)?.source, - classSymbol, - customSerializerType, - reporter - ) + val annotationElement = propertySymbol.serializableAnnotation(needArguments = false, session)?.source + checkCustomSerializerNotAbstract(classSymbol, source = annotationElement, customSerializerType, reporter) + checkCustomSerializerIsNotLocal(source = annotationElement, classSymbol, customSerializerType, reporter) + checkCustomSerializerParameters(classSymbol, annotationElement, customSerializerType, serializerForType, reporter) checkSerializerNullability(propertyType, customSerializerType, source, reporter) } else { checkType(typeRef, source, reporter) @@ -543,8 +565,14 @@ object FirSerializationPluginClassChecker : FirClassChecker(MppCheckerKind.Commo if (serializer != null) { val classSymbol = type.toRegularClassSymbol(session) ?: return type.serializableWith?.fullyExpandedType(session)?.let { serializerType -> - checkCustomSerializerMatch(classSymbol, typeSource, type, serializerType, reporter) + val serializerForType = serializerType.serializerForType(session)?.fullyExpandedType(session) + + checkCustomSerializerMatch(classSymbol, typeSource, type, serializerType, serializerForType, reporter) checkCustomSerializerIsNotLocal(typeSource, classSymbol, serializerType, reporter) + + val annotationElement = type.customAnnotations.serializableAnnotation(session)?.source ?: typeSource + checkCustomSerializerParameters(classSymbol, annotationElement, serializerType, serializerForType, reporter) + checkCustomSerializerNotAbstract(classSymbol, annotationElement, serializerType, reporter) checkSerializerNullability(type, serializerType, typeSource, reporter) } checkTypeArguments(typeRef, typeSource, reporter) @@ -562,9 +590,10 @@ object FirSerializationPluginClassChecker : FirClassChecker(MppCheckerKind.Commo source: KtSourceElement?, declarationType: ConeKotlinType, serializerType: ConeKotlinType, + serializerForType: ConeKotlinType?, reporter: DiagnosticReporter ) { - val serializerForType = serializerType.serializerForType(session)?.fullyExpandedType(session) ?: return + serializerForType ?: return val declarationTypeClassId = declarationType.classId if (declarationTypeClassId == null || declarationTypeClassId != serializerForType.classId) { @@ -583,7 +612,7 @@ object FirSerializationPluginClassChecker : FirClassChecker(MppCheckerKind.Commo containingClassSymbol: FirClassSymbol<*>, source: KtSourceElement?, serializerType: ConeKotlinType, - reporter: DiagnosticReporter + reporter: DiagnosticReporter, ) { if (with(session) { serializerType.isAbstractOrSealedOrInterface }) { reporter.reportOn( @@ -595,6 +624,63 @@ object FirSerializationPluginClassChecker : FirClassChecker(MppCheckerKind.Commo } } + context(CheckerContext) + private fun checkCustomSerializerParameters( + containingClassSymbol: FirClassSymbol<*>, + source: KtSourceElement?, + serializerType: ConeKotlinType, + serializerForType: ConeKotlinType?, + reporter: DiagnosticReporter, + ) { + serializerForType ?: return + + // Do not account for @Polymorphic and @Contextual, as they are serializers for T: Any + // and would not be compatible on direct comparison + if (serializerType.classId in SerializersClassIds.setOfSpecialSerializers) { + return + } + + val primaryConstructor = serializerType.toRegularClassSymbol(session)?.primaryConstructorSymbol(session) ?: return + + val targetElement by lazy { source ?: containingClassSymbol.serializableOrMetaAnnotationSource } + + val isExternalSerializer = serializerType.toRegularClassSymbol(session)?.getSerializerAnnotation(session) != null + + if ( // for external serializer, the verification will be carried out at the definition + !isExternalSerializer + // it is allowed that parameters are not passed in regular serializers at all + && primaryConstructor.valueParameterSymbols.isNotEmpty() + // if the parameters are still specified, then their number must match in the serializable class and constructor + && serializerForType.typeArguments.size != primaryConstructor.valueParameterSymbols.size + ) { + val message = if (serializerForType.typeArguments.isNotEmpty()) { + "expected no parameters or ${serializerForType.typeArguments.size}, but has ${primaryConstructor.valueParameterSymbols.size} parameters" + } else { + "expected no parameters but has ${primaryConstructor.valueParameterSymbols.size} parameters" + } + reporter.reportOn( + targetElement, + FirSerializationErrors.CUSTOM_SERIALIZER_PARAM_ILLEGAL_COUNT, + serializerType, + serializerForType, + message + ) + } + + primaryConstructor.valueParameterSymbols.forEach { param -> + val returnType = param.resolvedReturnType + if (!returnType.isKSerializer) { + reporter.reportOn( + targetElement, + FirSerializationErrors.CUSTOM_SERIALIZER_PARAM_ILLEGAL_TYPE, + serializerType, + serializerForType, + param.name.asString() + ) + } + } + } + context(CheckerContext) private fun checkCustomSerializerIsNotLocal( source: KtSourceElement?, diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/checkers/KtDefaultErrorMessagesSerialization.kt b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/checkers/KtDefaultErrorMessagesSerialization.kt index d9fe2af4aac..407b70fce8f 100644 --- a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/checkers/KtDefaultErrorMessagesSerialization.kt +++ b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/checkers/KtDefaultErrorMessagesSerialization.kt @@ -115,6 +115,20 @@ object KtDefaultErrorMessagesSerialization : BaseDiagnosticRendererFactory() { "Class ''{0}'' can't be used as a serializer since it is local", FirDiagnosticRenderers.RENDER_TYPE ) + put( + FirSerializationErrors.CUSTOM_SERIALIZER_PARAM_ILLEGAL_COUNT, + "Custom serializer ''{0}'' can not be used for ''{1}'' since it has an invalid number of parameters in primary constructor: {2}", + FirDiagnosticRenderers.RENDER_TYPE, + FirDiagnosticRenderers.RENDER_TYPE, + CommonRenderers.STRING + ) + put( + FirSerializationErrors.CUSTOM_SERIALIZER_PARAM_ILLEGAL_TYPE, + "Custom serializer ''{0}'' can not be used for ''{1}'', type of parameter ''{2}'' in serializer's primary constructor should be ''KSerializer''", + FirDiagnosticRenderers.RENDER_TYPE, + FirDiagnosticRenderers.RENDER_TYPE, + CommonRenderers.STRING + ) put( FirSerializationErrors.TRANSIENT_MISSING_INITIALIZER, "This property is marked as @Transient and therefore must have an initializing expression" @@ -183,5 +197,13 @@ object KtDefaultErrorMessagesSerialization : BaseDiagnosticRendererFactory() { FirDiagnosticRenderers.SYMBOL, FirDiagnosticRenderers.RENDER_TYPE ) + + put( + FirSerializationErrors.EXTERNAL_SERIALIZER_NO_SUITABLE_CONSTRUCTOR, + "Cannot generate external serializer ''{0}'': it must have a constructor with {2} value parameters, because class ''{1}'' has type parameters", + FirDiagnosticRenderers.SYMBOL, + FirDiagnosticRenderers.RENDER_TYPE, + CommonRenderers.STRING + ) } } diff --git a/plugins/kotlinx-serialization/testData/boxIr/polymorphic.kt b/plugins/kotlinx-serialization/testData/boxIr/polymorphic.kt new file mode 100644 index 00000000000..3c3e70a49cc --- /dev/null +++ b/plugins/kotlinx-serialization/testData/boxIr/polymorphic.kt @@ -0,0 +1,18 @@ +// TARGET_BACKEND: JVM_IR + +// WITH_STDLIB + +import kotlinx.serialization.* + +@Serializable(with = PolymorphicSerializer::class) +interface ExplicitlyPolymorphic + +@Serializable +class Holder( + val poly: ExplicitlyPolymorphic +) +fun box(): String { + val kind = ExplicitlyPolymorphic.serializer().descriptor.kind.toString() + if (kind != "OPEN") return kind + return "OK" +} diff --git a/plugins/kotlinx-serialization/testData/diagnostics/ParametrizedExternalSerializers.kt b/plugins/kotlinx-serialization/testData/diagnostics/ParametrizedExternalSerializers.kt new file mode 100644 index 00000000000..5776f5c374e --- /dev/null +++ b/plugins/kotlinx-serialization/testData/diagnostics/ParametrizedExternalSerializers.kt @@ -0,0 +1,61 @@ +// FIR_IDENTICAL + +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* + + +class Prop1(val t: T) + +class Prop2(val t: T, val r: R) + +@Serializer(forClass = Prop1::class) +class ExternalSerializer0_1 + +@Serializer(forClass = Prop1::class) +class ExternalSerializer1_1(val typeSerial0: KSerializer) + +@Serializer(forClass = Prop1::class) +class ExternalSerializer1_1Secondary() { + var typeSerial0: KSerializer? = null + constructor(typeSerial0: KSerializer) : this() { + this.typeSerial0 = typeSerial0 + } +} + +@Serializer(forClass = Prop2::class) +class ExternalSerializer0_2 + +@Serializer(forClass = Prop2::class) +class ExternalSerializer1_2(val typeSerial0: KSerializer) + +@Serializer(forClass = Prop2::class) +class ExternalSerializer1_2Secondary() { + var typeSerial0: KSerializer? = null + + constructor(typeSerial0: KSerializer) : this() { + this.typeSerial0 = typeSerial0 + } +} + +@Serializer(forClass = Prop2::class) +class ExternalSerializer2_2Secondary() { + var typeSerial0: KSerializer? = null + var typeSerial1: KSerializer? = null + constructor(typeSerial0: KSerializer) : this() { + this.typeSerial0 = typeSerial0 + } + constructor(typeSerial0: KSerializer, typeSerial1: KSerializer) : this() { + this.typeSerial0 = typeSerial0 + this.typeSerial1 = typeSerial1 + } +} + + +@Serializer(forClass = Prop1Err::class) +class ExternalSerializer0_1Err + +// there is no error in the discrepancy between the number of constructor parameters in the serializer +// because the checks take place at the place where the serializer is declared, and if the serializer is declared correctly, there will be no error here +@Serializable(ExternalSerializer0_1Err::class) +class Prop1Err(val t: T) diff --git a/plugins/kotlinx-serialization/testData/diagnostics/abstractCustomSerializer.kt b/plugins/kotlinx-serialization/testData/diagnostics/abstractCustomSerializer.kt index b3b6333bc1e..f26adbe130e 100644 --- a/plugins/kotlinx-serialization/testData/diagnostics/abstractCustomSerializer.kt +++ b/plugins/kotlinx-serialization/testData/diagnostics/abstractCustomSerializer.kt @@ -27,11 +27,17 @@ class WithSealed(val i: Int) @Serializable class Holder ( @Serializable(InterfaceSerializer::class) -val withInterface: WithInterfaceSerializer, + val withInterface: WithInterfaceSerializer, -@Serializable(AbstractSerializer::class) -val withAbstract: WithAbstract, + @Serializable(AbstractSerializer::class) + val withAbstract: WithAbstract, -@Serializable(SealedSerializer::class) -val withSealed: WithSealed + @Serializable(SealedSerializer::class) + val withSealed: WithSealed, + + val ListWithInterface: List<@Serializable(InterfaceSerializer::class) WithInterfaceSerializer>, + + val ListWithAbstract: List<@Serializable(AbstractSerializer::class) WithAbstract>, + + val ListWithSealed: List<@Serializable(SealedSerializer::class) WithSealed> ) diff --git a/plugins/kotlinx-serialization/testData/diagnostics/customSerializers.kt b/plugins/kotlinx-serialization/testData/diagnostics/customSerializers.kt new file mode 100644 index 00000000000..30526073bcc --- /dev/null +++ b/plugins/kotlinx-serialization/testData/diagnostics/customSerializers.kt @@ -0,0 +1,136 @@ +// FIR_DISABLE_LAZY_RESOLVE_CHECKS +// FIR_IDENTICAL +// WITH_STDLIB +// FILE: test.kt +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* + +/* Tests on Serializable(with) on classes */ +object Param0Object: ToDoSerializer() +@Serializable(Param0Object::class) +class Param0WithObject(val i: Int) + +object Param1Object: ToDoSerializer>() +@Serializable(Param1Object::class) +class Param1WithObject(val i: T) + + +class Param0Serializer0(): ToDoSerializer() +@Serializable(Param0Serializer0::class) +class Param0WithCustom0(val i: Int) + +class Param0Serializer1(val serializer: KSerializer<*>): ToDoSerializer() +@Serializable(Param0Serializer1::class) +class Param0WithCustom1(val i: Int) + +class Param0Serializer1Err(val serializer: Any): ToDoSerializer() +@Serializable(Param0Serializer1Err::class) +class Param0WithCustom1Err(val i: Int) + +class Param1Serializer0: ToDoSerializer>() +@Serializable(Param1Serializer0::class) +class Param1WithCustom0(val i: T) + +class Param1Serializer1(val serializer: KSerializer): ToDoSerializer>() +@Serializable(Param1Serializer1::class) +class Param1WithCustom(val i: T) + +class Param1Serializer1Err(val serializer: Any): ToDoSerializer>() +@Serializable(Param1Serializer1Err::class) +class Param1WithCustom1Err(val i: T) + +class Param1Serializer2(val serializer: KSerializer, val serializer2: KSerializer): ToDoSerializer>() +@Serializable(Param1Serializer2::class) +class Param1WithCustom2(val i: T) + +class Param1Serializer2Err(val serializer1: Any, val serializer2: Any): ToDoSerializer>() +@Serializable(Param1Serializer2Err::class) +class Param1WithCustom2Err(val i: T) + +class Param2Serializer1(val serializer: KSerializer): ToDoSerializer>() +@Serializable(Param2Serializer1::class) +class Param2WithCustom1(val i: T, val k: K) + +class Param2Serializer2Err(val serializer1: Any, val serializer2: Any): ToDoSerializer>() +@Serializable(Param2Serializer2Err::class) +class Param2WithCustom2Err(val i: T, val k: K) + +/* Tests on Serializable(with) on properties */ + +class Prop0(val t: Int) + +class Prop0Serializer0(): ToDoSerializer() + +class Prop0Serializer1(val serializer: KSerializer<*>): ToDoSerializer() + +class Prop0Serializer1Err(val serializer: Any): ToDoSerializer() + +object Prop0SerializerObject: ToDoSerializer() + + +class Prop1(val t: T) + +class Prop1Serializer0(): ToDoSerializer>() +class Prop1Serializer1(val serializer: KSerializer): ToDoSerializer>() +class Prop1Serializer2(val serializer1: KSerializer, val serializer2: KSerializer): ToDoSerializer>() + +class Prop1Serializer1Err(val serializer: Any): ToDoSerializer>() +class Prop1Serializer2Err(val serializer1: Any, val serializer2: Any): ToDoSerializer>() + +object Prop1SerializerObject: ToDoSerializer>() + + +@Serializable +class Holder( + @Serializable(Prop0SerializerObject::class) val obj0: Prop0, + + @Serializable(Prop0Serializer0::class) val p0_0: Prop0, + @Serializable(Prop0Serializer1::class) val p0_1: Prop0, + @Serializable(Prop0Serializer1Err::class) val p0_1Err: Prop0, + + @Serializable(Prop1SerializerObject::class) val obj1T: Prop1, + @Serializable(Prop1SerializerObject::class) val obj1: Prop1, + + @Serializable(Prop1Serializer0::class) val p1_0T: Prop1, + @Serializable(Prop1Serializer0::class) val p1_0: Prop1, + @Serializable(Prop1Serializer1::class) val p1_1T: Prop1, + @Serializable(Prop1Serializer1::class) val p1_1: Prop1, + @Serializable(Prop1Serializer2::class) val p1_2T: Prop1, + @Serializable(Prop1Serializer2::class) val p1_2: Prop1, + + @Serializable(Prop1Serializer1Err::class) val p1_1TErr: Prop1, + @Serializable(Prop1Serializer1Err::class) val p1_1Err: Prop1, + @Serializable(Prop1Serializer2Err::class) val p1_2TErr: Prop1, + @Serializable(Prop1Serializer2Err::class) val p1_2Err: Prop1, + + // generic params + val list_obj0: List<@Serializable(Prop0SerializerObject::class) Prop0>, + + val list_p0_0: List<@Serializable(Prop0Serializer0::class) Prop0>, + val list_p0_1: List<@Serializable(Prop0Serializer1::class) Prop0>, + val list_p0_1Err: List<@Serializable(Prop0Serializer1Err::class) Prop0>, + + val list_obj1T: List<@Serializable(Prop1SerializerObject::class) Prop1>, + val list_obj1: List<@Serializable(Prop1SerializerObject::class) Prop1>, + + val list_p1_0T: List<@Serializable(Prop1Serializer0::class) Prop1>, + val list_p1_0: List<@Serializable(Prop1Serializer0::class) Prop1>, + val list_p1_1T: List<@Serializable(Prop1Serializer1::class) Prop1>, + val list_p1_1: List<@Serializable(Prop1Serializer1::class) Prop1>, + val list_p1_2T: List<@Serializable(Prop1Serializer2::class) Prop1>, + val list_p1_2: List<@Serializable(Prop1Serializer2::class) Prop1>, + + val list_p1_1TErr: List<@Serializable(Prop1Serializer1Err::class) Prop1>, + val list_p1_1Err: List<@Serializable(Prop1Serializer1Err::class) Prop1>, + val list_p1_2TErr: List<@Serializable(Prop1Serializer2Err::class) Prop1>, + val list_p1_2Err: List<@Serializable(Prop1Serializer2Err::class) Prop1>, +) + + + +abstract class ToDoSerializer: KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("TODO SERIALIZER", PrimitiveKind.STRING) + override fun deserialize(decoder: Decoder): T = TODO() + override fun serialize(encoder: Encoder, value: T) = TODO() +} diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirLightTreeBlackBoxTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirLightTreeBlackBoxTestGenerated.java index 9d699ece99b..5276f4f4203 100644 --- a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirLightTreeBlackBoxTestGenerated.java +++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirLightTreeBlackBoxTestGenerated.java @@ -225,6 +225,12 @@ public class SerializationFirLightTreeBlackBoxTestGenerated extends AbstractSeri runTest("plugins/kotlinx-serialization/testData/boxIr/namedCompanions.kt"); } + @Test + @TestMetadata("polymorphic.kt") + public void testPolymorphic() throws Exception { + runTest("plugins/kotlinx-serialization/testData/boxIr/polymorphic.kt"); + } + @Test @TestMetadata("privateCustomSerializer.kt") public void testPrivateCustomSerializer() throws Exception { diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirPsiDiagnosticTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirPsiDiagnosticTestGenerated.java index 0154e163e4c..4cd99fe0a74 100644 --- a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirPsiDiagnosticTestGenerated.java +++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirPsiDiagnosticTestGenerated.java @@ -38,6 +38,12 @@ public class SerializationFirPsiDiagnosticTestGenerated extends AbstractSerializ runTest("plugins/kotlinx-serialization/testData/diagnostics/companionObjectSerializers.kt"); } + @Test + @TestMetadata("customSerializers.kt") + public void testCustomSerializers() throws Exception { + runTest("plugins/kotlinx-serialization/testData/diagnostics/customSerializers.kt"); + } + @Test @TestMetadata("DuplicateSerialName.kt") public void testDuplicateSerialName() throws Exception { @@ -128,6 +134,12 @@ public class SerializationFirPsiDiagnosticTestGenerated extends AbstractSerializ runTest("plugins/kotlinx-serialization/testData/diagnostics/ParamIsNotProperty.kt"); } + @Test + @TestMetadata("ParametrizedExternalSerializers.kt") + public void testParametrizedExternalSerializers() throws Exception { + runTest("plugins/kotlinx-serialization/testData/diagnostics/ParametrizedExternalSerializers.kt"); + } + @Test @TestMetadata("repeatableSerialInfo.kt") public void testRepeatableSerialInfo() throws Exception { diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java index 2a8afa9eda0..34062b81a91 100644 --- a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java +++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java @@ -223,6 +223,12 @@ public class SerializationIrBoxTestGenerated extends AbstractSerializationIrBoxT runTest("plugins/kotlinx-serialization/testData/boxIr/namedCompanions.kt"); } + @Test + @TestMetadata("polymorphic.kt") + public void testPolymorphic() throws Exception { + runTest("plugins/kotlinx-serialization/testData/boxIr/polymorphic.kt"); + } + @Test @TestMetadata("privateCustomSerializer.kt") public void testPrivateCustomSerializer() throws Exception { diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationPluginDiagnosticTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationPluginDiagnosticTestGenerated.java index 11e8a41eee6..4d66d31d868 100644 --- a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationPluginDiagnosticTestGenerated.java +++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationPluginDiagnosticTestGenerated.java @@ -36,6 +36,12 @@ public class SerializationPluginDiagnosticTestGenerated extends AbstractSerializ runTest("plugins/kotlinx-serialization/testData/diagnostics/companionObjectSerializers.kt"); } + @Test + @TestMetadata("customSerializers.kt") + public void testCustomSerializers() throws Exception { + runTest("plugins/kotlinx-serialization/testData/diagnostics/customSerializers.kt"); + } + @Test @TestMetadata("DuplicateSerialName.kt") public void testDuplicateSerialName() throws Exception { @@ -126,6 +132,12 @@ public class SerializationPluginDiagnosticTestGenerated extends AbstractSerializ runTest("plugins/kotlinx-serialization/testData/diagnostics/ParamIsNotProperty.kt"); } + @Test + @TestMetadata("ParametrizedExternalSerializers.kt") + public void testParametrizedExternalSerializers() throws Exception { + runTest("plugins/kotlinx-serialization/testData/diagnostics/ParametrizedExternalSerializers.kt"); + } + @Test @TestMetadata("repeatableSerialInfo.kt") public void testRepeatableSerialInfo() throws Exception {