[Parcelize] Allow parcelize to work on common code in multiplatform.

Since parcelize is Android specific, it should only be enabled for
Android compilation. In order to allow parcelize to generate code
for declarations in common code, we make checkers platform checkers
and we allow the registration of an additional annotations to
trigger parcelize processing.

That way, code such as:

```
package my.package

annotation class Parcelize

expect interface MyParcelable

@Parcelize
data class User(name: String): MyParcelable
```

Will work with an Android platform actual of the form:

```
actual typealias MyParcelable = android.os.Parcelable
```

And telling the plugin to trigger parcelize processing with the
additional annotation `my.package.Parcelize`.

Fixes https://issuetracker.google.com/315775835.
This commit is contained in:
Mads Ager
2024-02-26 14:29:55 +01:00
committed by Space Cloud
parent 5f971d6d9d
commit fa0d456850
26 changed files with 177 additions and 87 deletions
@@ -10,25 +10,29 @@ import org.jetbrains.kotlin.fir.analysis.checkers.declaration.*
import org.jetbrains.kotlin.fir.analysis.checkers.expression.ExpressionCheckers
import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirAnnotationCallChecker
import org.jetbrains.kotlin.fir.analysis.extensions.FirAdditionalCheckersExtension
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.parcelize.fir.diagnostics.*
class FirParcelizeCheckersExtension(session: FirSession) : FirAdditionalCheckersExtension(session) {
class FirParcelizeCheckersExtension(
session: FirSession,
val parcelizeAnnotations: List<ClassId>
) : FirAdditionalCheckersExtension(session) {
override val expressionCheckers: ExpressionCheckers = object : ExpressionCheckers() {
override val annotationCallCheckers: Set<FirAnnotationCallChecker>
get() = setOf(FirParcelizeAnnotationChecker)
get() = setOf(FirParcelizeAnnotationChecker(parcelizeAnnotations))
}
override val declarationCheckers: DeclarationCheckers = object : DeclarationCheckers() {
override val classCheckers: Set<FirClassChecker>
get() = setOf(FirParcelizeClassChecker)
get() = setOf(FirParcelizeClassChecker(parcelizeAnnotations))
override val propertyCheckers: Set<FirPropertyChecker>
get() = setOf(FirParcelizePropertyChecker)
get() = setOf(FirParcelizePropertyChecker(parcelizeAnnotations))
override val simpleFunctionCheckers: Set<FirSimpleFunctionChecker>
get() = setOf(FirParcelizeFunctionChecker)
get() = setOf(FirParcelizeFunctionChecker(parcelizeAnnotations))
override val constructorCheckers: Set<FirConstructorChecker>
get() = setOf(FirParcelizeConstructorChecker)
get() = setOf(FirParcelizeConstructorChecker(parcelizeAnnotations))
}
}
@@ -23,25 +23,28 @@ import org.jetbrains.kotlin.fir.resolve.lookupSuperTypes
import org.jetbrains.kotlin.fir.symbols.impl.*
import org.jetbrains.kotlin.fir.types.*
import org.jetbrains.kotlin.name.CallableId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.parcelize.ParcelizeNames.DESCRIBE_CONTENTS_NAME
import org.jetbrains.kotlin.parcelize.ParcelizeNames.DEST_NAME
import org.jetbrains.kotlin.parcelize.ParcelizeNames.FLAGS_NAME
import org.jetbrains.kotlin.parcelize.ParcelizeNames.OLD_PARCELIZE_FQN
import org.jetbrains.kotlin.parcelize.ParcelizeNames.PARCELIZE_FQN
import org.jetbrains.kotlin.parcelize.ParcelizeNames.PARCEL_ID
import org.jetbrains.kotlin.parcelize.ParcelizeNames.WRITE_TO_PARCEL_NAME
import org.jetbrains.kotlin.parcelize.fir.diagnostics.checkParcelizeClassSymbols
import org.jetbrains.kotlin.utils.addToStdlib.runIf
class FirParcelizeDeclarationGenerator(session: FirSession) : FirDeclarationGenerationExtension(session) {
class FirParcelizeDeclarationGenerator(
session: FirSession,
private val annotations: List<FqName>
) : FirDeclarationGenerationExtension(session) {
companion object {
private val PREDICATE = LookupPredicate.create { annotated(PARCELIZE_FQN, OLD_PARCELIZE_FQN) }
private val parcelizeMethodsNames = setOf(DESCRIBE_CONTENTS_NAME, WRITE_TO_PARCEL_NAME)
}
private val predicate = LookupPredicate.create { annotated(annotations) }
private val matchedClasses by lazy {
session.predicateBasedProvider.getSymbolsByPredicate(PREDICATE)
session.predicateBasedProvider.getSymbolsByPredicate(predicate)
.filterIsInstance<FirRegularClassSymbol>()
}
@@ -116,6 +119,6 @@ class FirParcelizeDeclarationGenerator(session: FirSession) : FirDeclarationGene
get() = ParcelizePluginKey
override fun FirDeclarationPredicateRegistrar.registerPredicates() {
register(PREDICATE)
register(predicate)
}
}
@@ -6,10 +6,12 @@
package org.jetbrains.kotlin.parcelize.fir
import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrar
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
class FirParcelizeExtensionRegistrar : FirExtensionRegistrar() {
class FirParcelizeExtensionRegistrar(val parcelizeAnnotationFqNames: List<FqName>) : FirExtensionRegistrar() {
override fun ExtensionRegistrarContext.configurePlugin() {
+::FirParcelizeDeclarationGenerator
+::FirParcelizeCheckersExtension
+::FirParcelizeDeclarationGenerator.bind(parcelizeAnnotationFqNames)
+::FirParcelizeCheckersExtension.bind(parcelizeAnnotationFqNames.map { ClassId.topLevel(it) })
}
}
@@ -23,13 +23,13 @@ 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
import org.jetbrains.kotlin.parcelize.ParcelizeNames.RAW_VALUE_ANNOTATION_CLASS_IDS
import org.jetbrains.kotlin.parcelize.ParcelizeNames.TYPE_PARCELER_CLASS_IDS
import org.jetbrains.kotlin.parcelize.ParcelizeNames.WRITE_WITH_CLASS_IDS
// TODO: extract common checker for expect interfaces
object FirParcelizeAnnotationChecker : FirAnnotationCallChecker(MppCheckerKind.Platform) {
class FirParcelizeAnnotationChecker(private val parcelizeAnnotationClassIds: List<ClassId>) :
FirAnnotationCallChecker(MppCheckerKind.Platform) {
override fun check(expression: FirAnnotationCall, context: CheckerContext, reporter: DiagnosticReporter) {
val annotationType = expression.annotationTypeRef.coneType.fullyExpandedType(context.session) as? ConeClassLikeType ?: return
val resolvedAnnotationSymbol = annotationType.lookupTag.toFirRegularClassSymbol(context.session) ?: return
@@ -47,7 +47,7 @@ object FirParcelizeAnnotationChecker : FirAnnotationCallChecker(MppCheckerKind.P
in IGNORED_ON_PARCEL_CLASS_IDS -> {
checkDeprecatedAnnotations(expression, annotationClassId, context, reporter, isForbidden = false)
}
in PARCELIZE_CLASS_CLASS_IDS, in RAW_VALUE_ANNOTATION_CLASS_IDS -> {
in parcelizeAnnotationClassIds, in RAW_VALUE_ANNOTATION_CLASS_IDS -> {
checkDeprecatedAnnotations(expression, annotationClassId, context, reporter, isForbidden = false)
}
}
@@ -148,7 +148,7 @@ object FirParcelizeAnnotationChecker : FirAnnotationCallChecker(MppCheckerKind.P
private fun checkIfTheContainingClassIsParcelize(annotationCall: FirAnnotationCall, context: CheckerContext, reporter: DiagnosticReporter) {
val enclosingClass = context.findClosestClassOrObject() ?: return
if (!enclosingClass.symbol.isParcelize(context.session)) {
if (!enclosingClass.symbol.isParcelize(context.session, parcelizeAnnotationClassIds)) {
val reportElement = annotationCall.calleeReference.source ?: annotationCall.source
reporter.reportOn(reportElement, KtErrorsParcelize.CLASS_SHOULD_BE_PARCELIZE, enclosingClass.symbol, context)
}
@@ -21,16 +21,16 @@ import org.jetbrains.kotlin.fir.resolve.lookupSuperTypes
import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol
import org.jetbrains.kotlin.fir.types.*
import org.jetbrains.kotlin.fir.types.impl.ConeClassLikeTypeImpl
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.SpecialNames
import org.jetbrains.kotlin.parcelize.ParcelizeNames.CREATOR_NAME
import org.jetbrains.kotlin.parcelize.ParcelizeNames.OLD_PARCELER_ID
import org.jetbrains.kotlin.parcelize.ParcelizeNames.PARCELABLE_ID
import org.jetbrains.kotlin.parcelize.ParcelizeNames.PARCELER_CLASS_IDS
import org.jetbrains.kotlin.parcelize.ParcelizeNames.PARCELIZE_CLASS_CLASS_IDS
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
object FirParcelizeClassChecker : FirClassChecker(MppCheckerKind.Common) {
class FirParcelizeClassChecker(private val parcelizeAnnotations: List<ClassId>) : FirClassChecker(MppCheckerKind.Platform) {
override fun check(declaration: FirClass, context: CheckerContext, reporter: DiagnosticReporter) {
checkParcelableClass(declaration, context, reporter)
checkParcelerClass(declaration, context, reporter)
@@ -38,7 +38,7 @@ object FirParcelizeClassChecker : FirClassChecker(MppCheckerKind.Common) {
private fun checkParcelableClass(klass: FirClass, context: CheckerContext, reporter: DiagnosticReporter) {
val symbol = klass.symbol
if (!symbol.isParcelize(context.session)) return
if (!symbol.isParcelize(context.session, parcelizeAnnotations)) return
val source = klass.source ?: return
val classKind = klass.classKind
@@ -110,14 +110,14 @@ object FirParcelizeClassChecker : FirClassChecker(MppCheckerKind.Common) {
}
@OptIn(ExperimentalContracts::class)
fun FirClassSymbol<*>?.isParcelize(session: FirSession): Boolean {
fun FirClassSymbol<*>?.isParcelize(session: FirSession, parcelizeAnnotations: List<ClassId>): Boolean {
contract {
returns(true) implies (this@isParcelize != null)
}
if (this == null) return false
return checkParcelizeClassSymbols(this, session) { symbol ->
symbol.annotations.any { it.toAnnotationClassId(session) in PARCELIZE_CLASS_CLASS_IDS }
symbol.annotations.any { it.toAnnotationClassId(session) in parcelizeAnnotations }
}
}
@@ -16,16 +16,20 @@ import org.jetbrains.kotlin.fir.correspondingProperty
import org.jetbrains.kotlin.fir.declarations.FirConstructor
import org.jetbrains.kotlin.fir.declarations.FirRegularClass
import org.jetbrains.kotlin.fir.declarations.toAnnotationClassId
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.parcelize.ParcelizeNames
object FirParcelizeConstructorChecker : FirConstructorChecker(MppCheckerKind.Common) {
class FirParcelizeConstructorChecker(private val parcelizeAnnotations: List<ClassId>) : FirConstructorChecker(MppCheckerKind.Platform) {
override fun check(declaration: FirConstructor, context: CheckerContext, reporter: DiagnosticReporter) {
if (!declaration.isPrimary) return
val source = declaration.source ?: return
if (source.kind == KtFakeSourceElementKind.ImplicitConstructor) return
val containingClass = context.containingDeclarations.last() as? FirRegularClass ?: return
val containingClassSymbol = containingClass.symbol
if (!containingClassSymbol.isParcelize(context.session) || containingClass.hasCustomParceler(context.session)) return
if (!containingClassSymbol.isParcelize(context.session, parcelizeAnnotations)
|| containingClass.hasCustomParceler(context.session)) {
return
}
if (declaration.valueParameters.isEmpty()) {
reporter.reportOn(containingClass.source, KtErrorsParcelize.PARCELABLE_PRIMARY_CONSTRUCTOR_IS_EMPTY, context)
@@ -17,11 +17,12 @@ import org.jetbrains.kotlin.fir.types.coneType
import org.jetbrains.kotlin.fir.types.isInt
import org.jetbrains.kotlin.fir.types.isUnit
import org.jetbrains.kotlin.fir.types.toRegularClassSymbol
import org.jetbrains.kotlin.name.ClassId
object FirParcelizeFunctionChecker : FirSimpleFunctionChecker(MppCheckerKind.Common) {
class FirParcelizeFunctionChecker(private val parcelizeAnnotations: List<ClassId>) : FirSimpleFunctionChecker(MppCheckerKind.Platform) {
override fun check(declaration: FirSimpleFunction, context: CheckerContext, reporter: DiagnosticReporter) {
val containingClassSymbol = declaration.dispatchReceiverType?.toRegularClassSymbol(context.session)
if (!containingClassSymbol.isParcelize(context.session)) return
if (!containingClassSymbol.isParcelize(context.session, parcelizeAnnotations)) return
if (declaration.origin != FirDeclarationOrigin.Source) return
if (declaration.isWriteToParcel() && declaration.isOverride) {
reporter.reportOn(declaration.source, KtErrorsParcelize.OVERRIDING_WRITE_TO_PARCEL_IS_NOT_ALLOWED, context)
@@ -26,18 +26,19 @@ 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.*
import org.jetbrains.kotlin.name.ClassId
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
object FirParcelizePropertyChecker : FirPropertyChecker(MppCheckerKind.Common) {
class FirParcelizePropertyChecker(private val parcelizeAnnotations: List<ClassId>) : FirPropertyChecker(MppCheckerKind.Platform) {
override fun check(declaration: FirProperty, context: CheckerContext, reporter: DiagnosticReporter) {
val session = context.session
val containingClassSymbol = declaration.dispatchReceiverType?.toRegularClassSymbol(session) ?: return
if (containingClassSymbol.isParcelize(session)) {
if (containingClassSymbol.isParcelize(session, parcelizeAnnotations)) {
val fromPrimaryConstructor = declaration.fromPrimaryConstructor ?: false
if (
!fromPrimaryConstructor &&
@@ -54,7 +55,7 @@ object FirParcelizePropertyChecker : FirPropertyChecker(MppCheckerKind.Common) {
if (declaration.name == CREATOR_NAME && containingClassSymbol.isCompanion && declaration.hasJvmFieldAnnotation(session)) {
val outerClass = context.containingDeclarations.asReversed().getOrNull(1) as? FirRegularClass
if (outerClass != null && outerClass.symbol.isParcelize(session)) {
if (outerClass != null && outerClass.symbol.isParcelize(session, parcelizeAnnotations)) {
reporter.reportOn(declaration.source, KtErrorsParcelize.CREATOR_DEFINITION_IS_NOT_ALLOWED, context)
}
}