Parcelize: Add missing FIR diagnostics

This commit is contained in:
Steven Schäfer
2022-07-25 20:24:52 +02:00
committed by teamcity
parent d4aa303acb
commit 58e51919bd
9 changed files with 189 additions and 73 deletions
@@ -5,16 +5,24 @@
package org.jetbrains.kotlin.parcelize.fir.diagnostics
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.diagnostics.DiagnosticReporter
import org.jetbrains.kotlin.diagnostics.reportOn
import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext
import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirAnnotationCallChecker
import org.jetbrains.kotlin.fir.analysis.checkers.findClosestClassOrObject
import org.jetbrains.kotlin.fir.correspondingProperty
import org.jetbrains.kotlin.fir.declarations.FirValueParameter
import org.jetbrains.kotlin.fir.declarations.utils.fromPrimaryConstructor
import org.jetbrains.kotlin.fir.expressions.FirAnnotationCall
import org.jetbrains.kotlin.fir.expressions.classId
import org.jetbrains.kotlin.fir.resolve.fullyExpandedType
import org.jetbrains.kotlin.fir.resolve.toFirRegularClassSymbol
import org.jetbrains.kotlin.fir.types.ConeClassLikeType
import org.jetbrains.kotlin.fir.types.coneType
import org.jetbrains.kotlin.fir.symbols.impl.ConeClassLikeLookupTagImpl
import org.jetbrains.kotlin.fir.types.*
import org.jetbrains.kotlin.fir.types.impl.ConeClassLikeTypeImpl
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.parcelize.ParcelizeNames
import org.jetbrains.kotlin.parcelize.ParcelizeNames.DEPRECATED_RUNTIME_PACKAGE
import org.jetbrains.kotlin.parcelize.ParcelizeNames.IGNORED_ON_PARCEL_CLASS_IDS
import org.jetbrains.kotlin.parcelize.ParcelizeNames.PARCELIZE_CLASS_CLASS_IDS
@@ -28,12 +36,14 @@ object FirParcelizeAnnotationChecker : FirAnnotationCallChecker() {
val resolvedAnnotationSymbol = annotationType.lookupTag.toFirRegularClassSymbol(context.session) ?: return
when (val annotationClassId = resolvedAnnotationSymbol.classId) {
in TYPE_PARCELER_CLASS_IDS -> {
checkTypeParcelerUsage(expression, context, reporter)
checkDeprecatedAnnotations(expression, annotationClassId, context, reporter, isForbidden = true)
if (checkDeprecatedAnnotations(expression, annotationClassId, context, reporter, isForbidden = true)) {
checkTypeParcelerUsage(expression, context, reporter)
}
}
in WRITE_WITH_CLASS_IDS -> {
checkWriteWithUsage(expression, context, reporter)
checkDeprecatedAnnotations(expression, annotationClassId, context, reporter, isForbidden = true)
if (checkDeprecatedAnnotations(expression, annotationClassId, context, reporter, isForbidden = true)) {
checkWriteWithUsage(expression, context, reporter)
}
}
in IGNORED_ON_PARCEL_CLASS_IDS -> {
checkDeprecatedAnnotations(expression, annotationClassId, context, reporter, isForbidden = false)
@@ -50,20 +60,83 @@ object FirParcelizeAnnotationChecker : FirAnnotationCallChecker() {
context: CheckerContext,
reporter: DiagnosticReporter,
isForbidden: Boolean
) {
): Boolean {
if (annotationClassId.packageFqName == DEPRECATED_RUNTIME_PACKAGE) {
val factory = if (isForbidden) KtErrorsParcelize.FORBIDDEN_DEPRECATED_ANNOTATION else KtErrorsParcelize.DEPRECATED_ANNOTATION
reporter.reportOn(annotationCall.source, factory, context)
return false
}
return true
}
private fun checkTypeParcelerUsage(annotationCall: FirAnnotationCall, context: CheckerContext, reporter: DiagnosticReporter) {
val thisMappedType = annotationCall.typeArguments.takeIf { it.size == 2 }?.first()?.toConeTypeProjection()?.type
?: return
val annotationContainer = context.annotationContainers.lastOrNull()
val duplicatingAnnotationCount = annotationContainer
?.annotations
?.filter { it.classId in TYPE_PARCELER_CLASS_IDS }
?.mapNotNull { it.typeArguments.takeIf { it.size == 2 }?.first()?.toConeTypeProjection()?.type }
?.count { it == thisMappedType }
if (duplicatingAnnotationCount != null && duplicatingAnnotationCount > 1) {
val reportElement = annotationCall.typeArguments.firstOrNull()?.source ?: annotationCall.source
reporter.reportOn(reportElement, KtErrorsParcelize.DUPLICATING_TYPE_PARCELERS, context)
return
}
checkIfTheContainingClassIsParcelize(annotationCall, context, reporter)
// If we are looking at a parameter of the primary constructor of a class, check that the
// enclosing class doesn't have the same TypeParceler annotation.
if (annotationContainer is FirValueParameter && annotationContainer.correspondingProperty?.fromPrimaryConstructor == true) {
val enclosingClass = context.findClosestClassOrObject() ?: return
val annotationType = annotationCall.typeRef.coneType
if (enclosingClass.annotations.any { it.typeRef.coneType == annotationType }) {
val reportElement = annotationCall.calleeReference.source ?: annotationCall.source
reporter.reportOn(reportElement, KtErrorsParcelize.REDUNDANT_TYPE_PARCELER, enclosingClass.symbol, context)
}
}
}
@Suppress("UNUSED_PARAMETER")
private fun checkTypeParcelerUsage(annotationCall: FirAnnotationCall, context: CheckerContext, reporter: DiagnosticReporter) {
// TODO: this check checks type arguments of annotation which are not supported in FIR
private fun checkWriteWithUsage(annotationCall: FirAnnotationCall, context: CheckerContext, reporter: DiagnosticReporter) {
checkIfTheContainingClassIsParcelize(annotationCall, context, reporter)
// For `@WriteWith<P>` check that `P` is an object.
val parcelerType = annotationCall.typeArguments.singleOrNull()?.toConeTypeProjection()?.type ?: return
if (parcelerType.toRegularClassSymbol(context.session)?.classKind != ClassKind.OBJECT) {
val reportElement = annotationCall.typeArguments.singleOrNull()?.source ?: annotationCall.source
reporter.reportOn(reportElement, KtErrorsParcelize.PARCELER_SHOULD_BE_OBJECT, context)
}
// For `@WriteWith<P> T` check that `P` is a subtype of `Parceler<T>`.
//
// From the perspective of the `WriteWith` annotation call, `T` corresponds to the nearest enclosing annotation container
// stripped of annotations.
//
// It's safe to assume that `Parceler` refers to `kotlinx.parcelize.Parceler` rather than `kotlinx.android.parcel.Parceler`,
// since using the deprecated `WriteWith` annotation is an error.
val targetType = (context.annotationContainers.lastOrNull() as? FirTypeRef)?.coneType?.withAttributes(ConeAttributes.Empty)
?: return
val expectedType = ConeClassLikeTypeImpl(
ConeClassLikeLookupTagImpl(ParcelizeNames.PARCELER_ID),
arrayOf(targetType),
isNullable = false
)
if (!parcelerType.isSubtypeOf(expectedType, context.session)) {
val reportElement = annotationCall.typeArguments.singleOrNull()?.source ?: annotationCall.source
reporter.reportOn(reportElement, KtErrorsParcelize.PARCELER_TYPE_INCOMPATIBLE, parcelerType, targetType, context)
}
}
@Suppress("UNUSED_PARAMETER")
private fun checkWriteWithUsage(annotationCall: FirAnnotationCall, context: CheckerContext, reporter: DiagnosticReporter) {
// TODO: this check checks type arguments of annotation which are not supported in FIR
private fun checkIfTheContainingClassIsParcelize(annotationCall: FirAnnotationCall, context: CheckerContext, reporter: DiagnosticReporter) {
val enclosingClass = context.findClosestClassOrObject() ?: return
if (!enclosingClass.symbol.isParcelize(context.session)) {
val reportElement = annotationCall.calleeReference.source ?: annotationCall.source
reporter.reportOn(reportElement, KtErrorsParcelize.CLASS_SHOULD_BE_PARCELIZE, enclosingClass.symbol, context)
}
}
}
@@ -5,7 +5,9 @@
package org.jetbrains.kotlin.parcelize.fir.diagnostics
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget
import org.jetbrains.kotlin.descriptors.isEnumClass
import org.jetbrains.kotlin.diagnostics.DiagnosticReporter
import org.jetbrains.kotlin.diagnostics.reportOn
import org.jetbrains.kotlin.fir.FirSession
@@ -18,12 +20,13 @@ import org.jetbrains.kotlin.fir.declarations.utils.fromPrimaryConstructor
import org.jetbrains.kotlin.fir.declarations.utils.hasBackingField
import org.jetbrains.kotlin.fir.declarations.utils.isCompanion
import org.jetbrains.kotlin.fir.expressions.FirAnnotation
import org.jetbrains.kotlin.fir.resolve.fqName
import org.jetbrains.kotlin.fir.resolve.fullyExpandedType
import org.jetbrains.kotlin.fir.resolve.lookupSuperTypes
import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol
import org.jetbrains.kotlin.fir.types.ConeErrorType
import org.jetbrains.kotlin.fir.types.classId
import org.jetbrains.kotlin.fir.types.coneType
import org.jetbrains.kotlin.fir.types.toRegularClassSymbol
import org.jetbrains.kotlin.fir.types.*
import org.jetbrains.kotlin.parcelize.BuiltinParcelableTypes
import org.jetbrains.kotlin.parcelize.ParcelizeNames
import org.jetbrains.kotlin.parcelize.ParcelizeNames.CREATOR_NAME
import org.jetbrains.kotlin.parcelize.ParcelizeNames.IGNORED_ON_PARCEL_CLASS_IDS
import org.jetbrains.kotlin.parcelize.ParcelizeNames.PARCELER_ID
@@ -55,7 +58,6 @@ object FirParcelizePropertyChecker : FirPropertyChecker() {
}
}
@Suppress("UNUSED_PARAMETER")
private fun checkParcelableClassProperty(
property: FirProperty,
containingClassSymbol: FirRegularClassSymbol,
@@ -63,18 +65,92 @@ object FirParcelizePropertyChecker : FirPropertyChecker() {
reporter: DiagnosticReporter
) {
val type = property.returnTypeRef.coneType
if (type is ConeErrorType || containingClassSymbol.hasCustomParceler(context.session)) return
/*
* TODO: abstract code from ParcelSerializer or IrParcelSerializerFactory to avoid duplication
* of allowed types checking
*/
if (type is ConeErrorType || containingClassSymbol.hasCustomParceler(context.session) || property.hasIgnoredOnParcel()) {
return
}
val session = context.session
val customParcelerTypes = getCustomParcelerTypes(property.annotations + containingClassSymbol.annotations, session)
if (!checkParcelableType(type, customParcelerTypes, session)) {
reporter.reportOn(property.returnTypeRef.source, KtErrorsParcelize.PARCELABLE_TYPE_NOT_SUPPORTED, context)
}
}
private fun getCustomParcelerTypes(annotations: List<FirAnnotation>, session: FirSession): Set<ConeKotlinType> =
annotations.mapNotNullTo(mutableSetOf()) { annotation ->
if (annotation.fqName(session) in ParcelizeNames.TYPE_PARCELER_FQ_NAMES && annotation.typeArguments.size == 2) {
annotation.typeArguments[0].toConeTypeProjection().type
} else {
null
}
}
private fun checkParcelableType(type: ConeKotlinType, customParcelerTypes: Set<ConeKotlinType>, session: FirSession): Boolean {
if (type.hasParcelerAnnotation(session) || type in customParcelerTypes) {
return true
}
val upperBound = type.getErasedUpperBound(session)
val symbol = upperBound?.toRegularClassSymbol(session)
?: return false
if (symbol.classKind.isSingleton || symbol.classKind.isEnumClass) {
return true
}
val fqName = symbol.classId.asFqNameString()
if (fqName in BuiltinParcelableTypes.PARCELABLE_BASE_TYPE_FQNAMES) {
return true
}
if (fqName in BuiltinParcelableTypes.PARCELABLE_CONTAINER_FQNAMES) {
return upperBound.typeArguments.all { projection ->
projection.type?.let { checkParcelableType(it, customParcelerTypes, session) } ?: false
}
}
return with(session.typeContext) {
type.anySuperTypeConstructor {
it is ConeKotlinType &&
(it.classId?.asFqNameString() in BuiltinParcelableTypes.PARCELABLE_SUPERTYPE_FQNAMES ||
it.isBuiltinFunctionalType(session))
}
}
}
private fun ConeKotlinType.getErasedUpperBound(session: FirSession): ConeClassLikeType? =
when (this) {
is ConeClassLikeType ->
fullyExpandedType(session)
is ConeTypeParameterType -> {
val bounds = lookupTag.typeParameterSymbol.resolvedBounds
val representativeBound = bounds.firstOrNull {
val kind = it.coneType.toRegularClassSymbol(session)?.classKind
?: return@firstOrNull false
kind != ClassKind.INTERFACE && kind != ClassKind.ANNOTATION_CLASS
} ?: bounds.first()
representativeBound.coneType.getErasedUpperBound(session)
}
else ->
null
}
private fun ConeKotlinType.hasParcelerAnnotation(session: FirSession): Boolean {
for (annotation in customAnnotations) {
val fqName = annotation.fqName(session)
if (fqName in ParcelizeNames.RAW_VALUE_ANNOTATION_FQ_NAMES || fqName in ParcelizeNames.WRITE_WITH_FQ_NAMES) {
return true
}
}
return false
}
private fun FirProperty.hasIgnoredOnParcel(): Boolean {
return annotations.hasIgnoredOnParcel() || (getter?.annotations?.hasIgnoredOnParcel() ?: false)
}
private fun List<FirAnnotation>.hasIgnoredOnParcel(): Boolean {
return this.any {
if (it.annotationTypeRef.coneType.classId !in IGNORED_ON_PARCEL_CLASS_IDS) return@any false
@@ -19,8 +19,8 @@ package org.jetbrains.kotlin.parcelize.fir.diagnostics
import org.jetbrains.kotlin.diagnostics.KtDiagnostic
import org.jetbrains.kotlin.diagnostics.KtDiagnosticFactoryToRendererMap
import org.jetbrains.kotlin.diagnostics.KtDiagnosticRenderer
import org.jetbrains.kotlin.diagnostics.rendering.Renderers.RENDER_CLASS_OR_OBJECT
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirDiagnosticRenderers.RENDER_TYPE
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirDiagnosticRenderers.RENDER_CLASS_OR_OBJECT
import org.jetbrains.kotlin.parcelize.fir.diagnostics.KtErrorsParcelize.CLASS_SHOULD_BE_PARCELIZE
import org.jetbrains.kotlin.parcelize.fir.diagnostics.KtErrorsParcelize.CREATOR_DEFINITION_IS_NOT_ALLOWED
import org.jetbrains.kotlin.parcelize.fir.diagnostics.KtErrorsParcelize.DEPRECATED_ANNOTATION
@@ -22,6 +22,8 @@ import org.jetbrains.kotlin.diagnostics.SourceElementPositioningStrategies.DELEG
import org.jetbrains.kotlin.diagnostics.SourceElementPositioningStrategies.INNER_MODIFIER
import org.jetbrains.kotlin.diagnostics.SourceElementPositioningStrategies.NAME_IDENTIFIER
import org.jetbrains.kotlin.diagnostics.SourceElementPositioningStrategies.OVERRIDE_MODIFIER
import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol
import org.jetbrains.kotlin.fir.types.ConeKotlinType
import org.jetbrains.kotlin.psi.KtClassOrObject
@@ -43,8 +45,8 @@ object KtErrorsParcelize {
val PARCELER_SHOULD_BE_OBJECT by error0<PsiElement>()
val PARCELER_TYPE_INCOMPATIBLE by error2<PsiElement, ConeKotlinType, ConeKotlinType>()
val DUPLICATING_TYPE_PARCELERS by error0<PsiElement>()
val REDUNDANT_TYPE_PARCELER by warning1<PsiElement, KtClassOrObject>()
val CLASS_SHOULD_BE_PARCELIZE by error1<PsiElement, KtClassOrObject>()
val REDUNDANT_TYPE_PARCELER by warning1<PsiElement, FirClassSymbol<*>>()
val CLASS_SHOULD_BE_PARCELIZE by error1<PsiElement, FirClassSymbol<*>>()
val FORBIDDEN_DEPRECATED_ANNOTATION by error0<PsiElement>()
val DEPRECATED_ANNOTATION by warning0<PsiElement>()
val DEPRECATED_PARCELER by error0<PsiElement>()
@@ -19,27 +19,27 @@ class StringClassParceler : Parceler<String> {
override fun String.write(parcel: Parcel, flags: Int) = TODO()
}
@TypeParceler<String, StringParceler>
class MissingParcelizeAnnotation(val a: @WriteWith<StringParceler> String)
@<!CLASS_SHOULD_BE_PARCELIZE!>TypeParceler<!><String, StringParceler>
class MissingParcelizeAnnotation(val a: @<!CLASS_SHOULD_BE_PARCELIZE!>WriteWith<!><StringParceler> String)
@Parcelize
@TypeParceler<String, StringClassParceler>
class ShouldBeClass(val a: @WriteWith<StringClassParceler> String) : Parcelable
class ShouldBeClass(val a: @WriteWith<<!PARCELER_SHOULD_BE_OBJECT!>StringClassParceler<!>> String) : Parcelable
@Parcelize
class Test(
val a: @WriteWith<StringParceler> Int,
val a: @WriteWith<<!PARCELER_TYPE_INCOMPATIBLE!>StringParceler<!>> Int,
val b: @WriteWith<StringParceler> String,
val c: @WriteWith<StringParceler> CharSequence,
val d: @WriteWith<CharSequenceParceler> String,
val c: @WriteWith<<!PARCELER_TYPE_INCOMPATIBLE!>StringParceler<!>> CharSequence,
val d: @WriteWith<<!PARCELER_TYPE_INCOMPATIBLE!>CharSequenceParceler<!>> String,
val e: @WriteWith<CharSequenceParceler> CharSequence
) : Parcelable
@Parcelize
@TypeParceler<String, StringParceler>
class Test2(@TypeParceler<String, StringParceler> val a: String) : Parcelable
class Test2(@<!REDUNDANT_TYPE_PARCELER!>TypeParceler<!><String, StringParceler> val a: String) : Parcelable
@Parcelize
@TypeParceler<String, StringParceler>
@TypeParceler<String, CharSequenceParceler>
@TypeParceler<<!DUPLICATING_TYPE_PARCELERS!>String<!>, StringParceler>
@TypeParceler<<!DUPLICATING_TYPE_PARCELERS!>String<!>, CharSequenceParceler>
class Test3(val a: String) : Parcelable
@@ -1,20 +0,0 @@
package test
import kotlinx.parcelize.*
import android.os.*
class Box(val value: String)
@Parcelize
class Foo(val box: Box): Parcelable {
companion object : Parceler<Foo> {
override fun create(parcel: Parcel) = Foo(Box(parcel.readString()))
override fun Foo.write(parcel: Parcel, flags: Int) {
parcel.writeString(box.value)
}
}
}
@Parcelize
class Foo2(val box: Box): Parcelable
@@ -1,3 +1,4 @@
// FIR_IDENTICAL
package test
import kotlinx.parcelize.*
@@ -1,17 +0,0 @@
package test
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
import android.os.Parcelable
@Parcelize
class User(
val a: String,
val b: Any,
val c: Any?,
val d: Map<Any, String>,
val e: @RawValue Any?,
val f: @RawValue Map<String, Any>,
val g: Map<String, @RawValue Any>,
val h: Map<@RawValue Any, List<@RawValue Any>>
) : Parcelable
@@ -1,3 +1,4 @@
// FIR_IDENTICAL
package test
import kotlinx.parcelize.Parcelize