[KxSerialization] Added inspections on custom serializer parameters

Added inspections to check:
- custom serializer on class has as many parameters in primary constructor as the serializable class of type arguments
- all parameters in custom serializer has `KSerializer` type
- property in serializable class not parametrized by type parameter
- custom serializer on property of serializable class have no parameters in primary constructor
This commit is contained in:
Sergey.Shanshin
2023-07-28 22:03:31 +02:00
committed by Space Team
parent 9532172a22
commit 84ad12be57
15 changed files with 507 additions and 32 deletions
@@ -36,6 +36,9 @@ object FirSerializationErrors {
val SERIALIZER_TYPE_INCOMPATIBLE by warning3<PsiElement, ConeKotlinType, ConeKotlinType, ConeKotlinType>()
val ABSTRACT_SERIALIZER_TYPE by error2<PsiElement, ConeKotlinType, ConeKotlinType>()
val LOCAL_SERIALIZER_USAGE by error1<PsiElement, ConeKotlinType>()
val CUSTOM_SERIALIZER_PARAM_ILLEGAL_COUNT by error3<PsiElement, ConeKotlinType, ConeKotlinType, String>()
val CUSTOM_SERIALIZER_PARAM_ILLEGAL_TYPE by error3<PsiElement, ConeKotlinType, ConeKotlinType, String>()
val GENERIC_ARRAY_ELEMENT_NOT_SUPPORTED by error0<PsiElement>()
val TRANSIENT_MISSING_INITIALIZER by error0<PsiElement>()
@@ -52,6 +55,7 @@ object FirSerializationErrors {
val EXTERNAL_SERIALIZER_USELESS by warning1<PsiElement, FirClassSymbol<*>>()
val EXTERNAL_CLASS_NOT_SERIALIZABLE by error2<PsiElement, FirClassSymbol<*>, ConeKotlinType>()
val EXTERNAL_CLASS_IN_ANOTHER_MODULE by error2<PsiElement, FirClassSymbol<*>, ConeKotlinType>()
val EXTERNAL_SERIALIZER_NO_SUITABLE_CONSTRUCTOR by error3<PsiElement, FirClassSymbol<*>, ConeKotlinType, String>()
init {
RootDiagnosticRendererFactory.registerFactory(KtDefaultErrorMessagesSerialization)
@@ -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<FirConstructorSymbol>().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<FirPropertySymbol>().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?,
@@ -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
)
}
}