[KxSerialization] Added inspection on abstract custom serializer

Serialization requires an instance of the serializer, which cannot be obtained with the passed interface, abstract or sealed class.
Therefore, the specifying of such classes in `Serializable` annotation must be prohibited.

Fixes https://github.com/Kotlin/kotlinx.serialization/issues/2173
Relates #KT-58036

Merge-request: KT-MR-10753
Merged-by: Sergey Shanshin <Sergey.Shanshin@jetbrains.com>
This commit is contained in:
Sergey.Shanshin
2023-07-04 18:31:35 +00:00
committed by Space Team
parent 8602ed2d21
commit 3629a9db30
11 changed files with 123 additions and 2 deletions
@@ -36,6 +36,7 @@ public interface SerializationErrors {
DiagnosticFactory3<PsiElement, KotlinType, String, String> DUPLICATE_SERIAL_NAME_ENUM = DiagnosticFactory3.create(ERROR); DiagnosticFactory3<PsiElement, KotlinType, String, String> DUPLICATE_SERIAL_NAME_ENUM = DiagnosticFactory3.create(ERROR);
DiagnosticFactory1<PsiElement, KotlinType> SERIALIZER_NOT_FOUND = DiagnosticFactory1.create(ERROR); DiagnosticFactory1<PsiElement, KotlinType> SERIALIZER_NOT_FOUND = DiagnosticFactory1.create(ERROR);
DiagnosticFactory2<PsiElement, KotlinType, KotlinType> SERIALIZER_NULLABILITY_INCOMPATIBLE = DiagnosticFactory2.create(ERROR); DiagnosticFactory2<PsiElement, KotlinType, KotlinType> SERIALIZER_NULLABILITY_INCOMPATIBLE = DiagnosticFactory2.create(ERROR);
DiagnosticFactory2<PsiElement, KotlinType, KotlinType> ABSTRACT_SERIALIZER_TYPE = DiagnosticFactory2.create(ERROR);
DiagnosticFactory3<PsiElement, KotlinType, KotlinType, KotlinType> SERIALIZER_TYPE_INCOMPATIBLE = DiagnosticFactory3.create(WARNING); DiagnosticFactory3<PsiElement, KotlinType, KotlinType, KotlinType> SERIALIZER_TYPE_INCOMPATIBLE = DiagnosticFactory3.create(WARNING);
DiagnosticFactory1<PsiElement, KotlinType> LOCAL_SERIALIZER_USAGE = DiagnosticFactory1.create(ERROR); DiagnosticFactory1<PsiElement, KotlinType> LOCAL_SERIALIZER_USAGE = DiagnosticFactory1.create(ERROR);
DiagnosticFactory0<PsiElement> TRANSIENT_MISSING_INITIALIZER = DiagnosticFactory0.create(ERROR); DiagnosticFactory0<PsiElement> TRANSIENT_MISSING_INITIALIZER = DiagnosticFactory0.create(ERROR);
@@ -325,6 +325,7 @@ open class SerializationPluginDeclarationChecker : DeclarationChecker {
val annotationPsi = descriptor.findSerializableOrMetaAnnotationDeclaration() val annotationPsi = descriptor.findSerializableOrMetaAnnotationDeclaration()
checkCustomSerializerMatch(descriptor.module, descriptor.defaultType, descriptor, annotationPsi, trace, declaration) checkCustomSerializerMatch(descriptor.module, descriptor.defaultType, descriptor, annotationPsi, trace, declaration)
checkCustomSerializerIsNotLocal(descriptor.module, descriptor, trace, declaration) checkCustomSerializerIsNotLocal(descriptor.module, descriptor, trace, declaration)
checkCustomSerializerNotAbstract(descriptor.module, descriptor.defaultType, descriptor, annotationPsi, trace, declaration)
} }
private val ClassDescriptor.isAnonymousObjectOrContained: Boolean private val ClassDescriptor.isAnonymousObjectOrContained: Boolean
@@ -440,6 +441,14 @@ open class SerializationPluginDeclarationChecker : DeclarationChecker {
if (serializer != null) { if (serializer != null) {
val element = ktType?.typeElement val element = ktType?.typeElement
checkCustomSerializerMatch(it.module, it.type, it.descriptor, element, trace, propertyPsi) checkCustomSerializerMatch(it.module, it.type, it.descriptor, element, trace, propertyPsi)
checkCustomSerializerNotAbstract(
it.module,
it.type,
it.descriptor,
it.descriptor.findSerializableOrMetaAnnotationDeclaration(),
trace,
propertyPsi
)
checkCustomSerializerIsNotLocal(it.module, it.descriptor, trace, propertyPsi) checkCustomSerializerIsNotLocal(it.module, it.descriptor, trace, propertyPsi)
checkSerializerNullability(it.type, serializer.defaultType, element, trace, propertyPsi) checkSerializerNullability(it.type, serializer.defaultType, element, trace, propertyPsi)
generatorContextForAnalysis.checkTypeArguments(it.module, it.type, element, trace, propertyPsi) generatorContextForAnalysis.checkTypeArguments(it.module, it.type, element, trace, propertyPsi)
@@ -519,6 +528,26 @@ open class SerializationPluginDeclarationChecker : DeclarationChecker {
} }
} }
private fun checkCustomSerializerNotAbstract(
module: ModuleDescriptor,
classType: KotlinType,
descriptor: Annotated,
element: KtElement?,
trace: BindingTrace,
fallbackElement: PsiElement
) {
val serializerType = descriptor.annotations.serializableWith(module) ?: return
if (serializerType.toClassDescriptor?.isAbstractOrSealedOrInterface == true) {
trace.report(
SerializationErrors.ABSTRACT_SERIALIZER_TYPE.on(
element ?: fallbackElement,
classType,
serializerType
)
)
}
}
private fun checkCustomSerializerMatch( private fun checkCustomSerializerMatch(
module: ModuleDescriptor, module: ModuleDescriptor,
classType: KotlinType, classType: KotlinType,
@@ -107,6 +107,12 @@ object SerializationPluginErrorsRendering : DefaultErrorMessages.Extension {
Renderers.RENDER_TYPE, Renderers.RENDER_TYPE,
Renderers.RENDER_TYPE Renderers.RENDER_TYPE
) )
MAP.put(
SerializationErrors.ABSTRACT_SERIALIZER_TYPE,
"Custom serializer ''{1}'' on serializable type ''{0}'' can not be instantiated. It is not allowed to specify the interface, abstract or sealed class as a custom serializer.",
Renderers.RENDER_TYPE,
Renderers.RENDER_TYPE
)
MAP.put( MAP.put(
SerializationErrors.LOCAL_SERIALIZER_USAGE, SerializationErrors.LOCAL_SERIALIZER_USAGE,
"Class ''{0}'' can't be used as a serializer since it is local", "Class ''{0}'' can't be used as a serializer since it is local",
@@ -124,6 +124,9 @@ val ClassDescriptor.isInternallySerializableObject: Boolean
val ClassDescriptor.isSealedSerializableInterface: Boolean val ClassDescriptor.isSealedSerializableInterface: Boolean
get() = kind == ClassKind.INTERFACE && modality == Modality.SEALED && hasSerializableOrMetaAnnotation get() = kind == ClassKind.INTERFACE && modality == Modality.SEALED && hasSerializableOrMetaAnnotation
val ClassDescriptor.isAbstractOrSealedOrInterface: Boolean
get() = kind == ClassKind.INTERFACE || modality == Modality.SEALED || modality == Modality.ABSTRACT
val ClassDescriptor.isInternalSerializable: Boolean //todo normal checking val ClassDescriptor.isInternalSerializable: Boolean //todo normal checking
get() { get() {
if (kind != ClassKind.CLASS) return false if (kind != ClassKind.CLASS) return false
@@ -222,6 +222,11 @@ context(FirSession)
val ConeKotlinType.isGeneratedSerializableObject: Boolean val ConeKotlinType.isGeneratedSerializableObject: Boolean
get() = toRegularClassSymbol(this@FirSession)?.let { it.classKind.isObject && it.hasSerializableOrMetaAnnotationWithoutArgs } ?: false get() = toRegularClassSymbol(this@FirSession)?.let { it.classKind.isObject && it.hasSerializableOrMetaAnnotationWithoutArgs } ?: false
context(FirSession)
val ConeKotlinType.isAbstractOrSealedOrInterface: Boolean
get() = toRegularClassSymbol(this@FirSession)?.let { it.classKind.isInterface || it.rawStatus.modality == Modality.ABSTRACT || it.rawStatus.modality == Modality.SEALED }
?: false
context(FirExtension) context(FirExtension)
fun FirAnnotationContainer.excludeFromJsExport() { fun FirAnnotationContainer.excludeFromJsExport() {
@@ -34,6 +34,7 @@ object FirSerializationErrors {
val SERIALIZER_NOT_FOUND by error1<PsiElement, ConeKotlinType>() val SERIALIZER_NOT_FOUND by error1<PsiElement, ConeKotlinType>()
val SERIALIZER_NULLABILITY_INCOMPATIBLE by error2<PsiElement, ConeKotlinType, ConeKotlinType>() val SERIALIZER_NULLABILITY_INCOMPATIBLE by error2<PsiElement, ConeKotlinType, ConeKotlinType>()
val SERIALIZER_TYPE_INCOMPATIBLE by warning3<PsiElement, ConeKotlinType, ConeKotlinType, ConeKotlinType>() 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 LOCAL_SERIALIZER_USAGE by error1<PsiElement, ConeKotlinType>()
val GENERIC_ARRAY_ELEMENT_NOT_SUPPORTED by error0<PsiElement>() val GENERIC_ARRAY_ELEMENT_NOT_SUPPORTED by error0<PsiElement>()
val TRANSIENT_MISSING_INITIALIZER by error0<PsiElement>() val TRANSIENT_MISSING_INITIALIZER by error0<PsiElement>()
@@ -363,6 +363,7 @@ object FirSerializationPluginClassChecker : FirClassChecker() {
val serializerType = classSymbol.getSerializableWith(session)?.fullyExpandedType(session) ?: return val serializerType = classSymbol.getSerializableWith(session)?.fullyExpandedType(session) ?: return
checkCustomSerializerMatch(classSymbol, source = null, classSymbol.defaultType(), serializerType, reporter) checkCustomSerializerMatch(classSymbol, source = null, classSymbol.defaultType(), serializerType, reporter)
checkCustomSerializerIsNotLocal(source = null, classSymbol, serializerType, reporter) checkCustomSerializerIsNotLocal(source = null, classSymbol, serializerType, reporter)
checkCustomSerializerNotAbstract(classSymbol, source = null, serializerType, reporter)
} }
context(CheckerContext) context(CheckerContext)
@@ -471,6 +472,12 @@ object FirSerializationPluginClassChecker : FirClassChecker() {
customSerializerType, customSerializerType,
reporter reporter
) )
checkCustomSerializerNotAbstract(
classSymbol,
source = propertySymbol.serializableAnnotation(needArguments = false, session)?.source,
customSerializerType,
reporter
)
checkCustomSerializerIsNotLocal( checkCustomSerializerIsNotLocal(
source = propertySymbol.serializableAnnotation(needArguments = false, session)?.source, source = propertySymbol.serializableAnnotation(needArguments = false, session)?.source,
classSymbol, classSymbol,
@@ -566,6 +573,22 @@ object FirSerializationPluginClassChecker : FirClassChecker() {
) )
} }
} }
context(CheckerContext)
private fun checkCustomSerializerNotAbstract(
containingClassSymbol: FirClassSymbol<*>,
source: KtSourceElement?,
serializerType: ConeKotlinType,
reporter: DiagnosticReporter
) {
if (with(session) { serializerType.isAbstractOrSealedOrInterface }) {
reporter.reportOn(
source ?: containingClassSymbol.serializableOrMetaAnnotationSource,
FirSerializationErrors.ABSTRACT_SERIALIZER_TYPE,
containingClassSymbol.defaultType(),
serializerType
)
}
}
context(CheckerContext) context(CheckerContext)
private fun checkCustomSerializerIsNotLocal( private fun checkCustomSerializerIsNotLocal(
@@ -5,9 +5,7 @@
package org.jetbrains.kotlinx.serialization.compiler.fir.checkers package org.jetbrains.kotlinx.serialization.compiler.fir.checkers
import org.jetbrains.kotlin.diagnostics.KtDiagnostic
import org.jetbrains.kotlin.diagnostics.KtDiagnosticFactoryToRendererMap import org.jetbrains.kotlin.diagnostics.KtDiagnosticFactoryToRendererMap
import org.jetbrains.kotlin.diagnostics.KtDiagnosticRenderer
import org.jetbrains.kotlin.diagnostics.rendering.BaseDiagnosticRendererFactory import org.jetbrains.kotlin.diagnostics.rendering.BaseDiagnosticRendererFactory
import org.jetbrains.kotlin.diagnostics.rendering.CommonRenderers import org.jetbrains.kotlin.diagnostics.rendering.CommonRenderers
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirDiagnosticRenderers import org.jetbrains.kotlin.fir.analysis.diagnostics.FirDiagnosticRenderers
@@ -106,6 +104,12 @@ object KtDefaultErrorMessagesSerialization : BaseDiagnosticRendererFactory() {
FirDiagnosticRenderers.RENDER_TYPE, FirDiagnosticRenderers.RENDER_TYPE,
FirDiagnosticRenderers.RENDER_TYPE FirDiagnosticRenderers.RENDER_TYPE
) )
put(
FirSerializationErrors.ABSTRACT_SERIALIZER_TYPE,
"Custom serializer ''{1}'' on serializable type ''{0}'' can not be instantiated. It is not allowed to specify the interface, abstract or sealed class as a custom serializer.",
FirDiagnosticRenderers.RENDER_TYPE,
FirDiagnosticRenderers.RENDER_TYPE
)
put( put(
FirSerializationErrors.LOCAL_SERIALIZER_USAGE, FirSerializationErrors.LOCAL_SERIALIZER_USAGE,
"Class ''{0}'' can't be used as a serializer since it is local", "Class ''{0}'' can't be used as a serializer since it is local",
@@ -0,0 +1,37 @@
// FIR_DISABLE_LAZY_RESOLVE_CHECKS
// FIR_IDENTICAL
// SKIP_TXT
// WITH_STDLIB
// FILE: test.kt
import kotlinx.serialization.*
interface InterfaceSerializer: KSerializer<WithInterfaceSerializer>
<!ABSTRACT_SERIALIZER_TYPE!>@Serializable(InterfaceSerializer::class)<!>
class WithInterfaceSerializer(val i: Int)
abstract class AbstractSerializer: KSerializer<WithAbstract>
<!ABSTRACT_SERIALIZER_TYPE!>@Serializable(AbstractSerializer::class)<!>
class WithAbstract(val i: Int)
sealed class SealedSerializer: KSerializer<WithSealed>
<!ABSTRACT_SERIALIZER_TYPE!>@Serializable(SealedSerializer::class)<!>
class WithSealed(val i: Int)
@Serializable
class Holder (
<!ABSTRACT_SERIALIZER_TYPE!>@Serializable(InterfaceSerializer::class)<!>
val withInterface: WithInterfaceSerializer,
<!ABSTRACT_SERIALIZER_TYPE!>@Serializable(AbstractSerializer::class)<!>
val withAbstract: WithAbstract,
<!ABSTRACT_SERIALIZER_TYPE!>@Serializable(SealedSerializer::class)<!>
val withSealed: WithSealed
)
@@ -21,6 +21,12 @@ public class SerializationFirPsiDiagnosticTestGenerated extends AbstractSerializ
@TestMetadata("plugins/kotlinx-serialization/testData/diagnostics") @TestMetadata("plugins/kotlinx-serialization/testData/diagnostics")
@TestDataPath("$PROJECT_ROOT") @TestDataPath("$PROJECT_ROOT")
public class Diagnostics { public class Diagnostics {
@Test
@TestMetadata("abstractCustomSerializer.kt")
public void testAbstractCustomSerializer() throws Exception {
runTest("plugins/kotlinx-serialization/testData/diagnostics/abstractCustomSerializer.kt");
}
@Test @Test
public void testAllFilesPresentInDiagnostics() throws Exception { public void testAllFilesPresentInDiagnostics() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("plugins/kotlinx-serialization/testData/diagnostics"), Pattern.compile("^(.+)\\.kt$"), Pattern.compile("^(.+)\\.fir\\.kts?$"), true); KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("plugins/kotlinx-serialization/testData/diagnostics"), Pattern.compile("^(.+)\\.kt$"), Pattern.compile("^(.+)\\.fir\\.kts?$"), true);
@@ -19,6 +19,12 @@ import java.util.regex.Pattern;
@TestMetadata("plugins/kotlinx-serialization/testData/diagnostics") @TestMetadata("plugins/kotlinx-serialization/testData/diagnostics")
@TestDataPath("$PROJECT_ROOT") @TestDataPath("$PROJECT_ROOT")
public class SerializationPluginDiagnosticTestGenerated extends AbstractSerializationPluginDiagnosticTest { public class SerializationPluginDiagnosticTestGenerated extends AbstractSerializationPluginDiagnosticTest {
@Test
@TestMetadata("abstractCustomSerializer.kt")
public void testAbstractCustomSerializer() throws Exception {
runTest("plugins/kotlinx-serialization/testData/diagnostics/abstractCustomSerializer.kt");
}
@Test @Test
public void testAllFilesPresentInDiagnostics() throws Exception { public void testAllFilesPresentInDiagnostics() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("plugins/kotlinx-serialization/testData/diagnostics"), Pattern.compile("^(.+)\\.kt$"), Pattern.compile("^(.+)\\.fir\\.kts?$"), true); KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("plugins/kotlinx-serialization/testData/diagnostics"), Pattern.compile("^(.+)\\.kt$"), Pattern.compile("^(.+)\\.fir\\.kts?$"), true);