Parcelable: Report error on unsupported parameter types, add @RawValue annotation support

This commit is contained in:
Yan Zhulanow
2017-07-03 23:44:13 +03:00
parent 4200629347
commit cf607a0f14
6 changed files with 110 additions and 16 deletions
@@ -188,7 +188,7 @@ class ParcelableCodegenExtension : ExpressionCodegenExtension {
val asmType = codegen.typeMapper.mapType(type)
asmConstructorParameters.append(asmType.descriptor)
val serializer = ParcelSerializer.get(type, asmType, codegen.typeMapper)
val serializer = ParcelSerializer.get(type, asmType, codegen.typeMapper, forceBoxed = false, strict = false)
v.load(1, parcelAsmType)
serializer.readValue(v)
}
@@ -16,20 +16,27 @@
package org.jetbrains.kotlin.android.parcel
import org.jetbrains.kotlin.android.parcel.serializers.ParcelSerializer
import org.jetbrains.kotlin.android.synthetic.diagnostic.ErrorsAndroid
import org.jetbrains.kotlin.codegen.ClassBuilderMode
import org.jetbrains.kotlin.codegen.state.IncompatibleClassTracker
import org.jetbrains.kotlin.codegen.state.KotlinTypeMapper
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor
import org.jetbrains.kotlin.diagnostics.DiagnosticSink
import org.jetbrains.kotlin.fileClasses.NoResolveFileClassesProvider
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.checkers.SimpleDeclarationChecker
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.resolve.descriptorUtil.module
import org.jetbrains.kotlin.resolve.jvm.annotations.findJvmFieldAnnotation
import org.jetbrains.kotlin.types.TypeUtils
import org.jetbrains.kotlin.types.isError
private val ANDROID_PARCELABLE_CLASS_FQNAME = FqName("android.os.Parcelable")
internal val ANDROID_PARCEL_CLASS_FQNAME = FqName("android.os.Parcel")
@@ -46,7 +53,7 @@ class ParcelableDeclarationChecker : SimpleDeclarationChecker {
bindingContext: BindingContext
) {
when (descriptor) {
is ClassDescriptor -> checkParcelableClass(descriptor, declaration, diagnosticHolder)
is ClassDescriptor -> checkParcelableClass(descriptor, declaration, diagnosticHolder, bindingContext)
is SimpleFunctionDescriptor -> {
val containingClass = descriptor.containingDeclaration as? ClassDescriptor
val ktFunction = declaration as? KtFunction
@@ -102,7 +109,12 @@ class ParcelableDeclarationChecker : SimpleDeclarationChecker {
}
}
private fun checkParcelableClass(descriptor: ClassDescriptor, declaration: KtDeclaration, diagnosticHolder: DiagnosticSink) {
private fun checkParcelableClass(
descriptor: ClassDescriptor,
declaration: KtDeclaration,
diagnosticHolder: DiagnosticSink,
bindingContext: BindingContext
) {
if (!descriptor.isMagicParcelable) return
if (declaration !is KtClass || (declaration.isAnnotation() || declaration.isInterface() || declaration.isEnum())) {
@@ -135,10 +147,39 @@ class ParcelableDeclarationChecker : SimpleDeclarationChecker {
diagnosticHolder.report(ErrorsAndroid.PARCELABLE_SHOULD_HAVE_PRIMARY_CONSTRUCTOR.on(declaration.nameIdentifier ?: declaration))
}
val typeMapper = KotlinTypeMapper(
bindingContext,
ClassBuilderMode.full(false),
NoResolveFileClassesProvider,
IncompatibleClassTracker.DoNothing,
descriptor.module.name.asString(),
/* isJvm8Target */ false,
/* isJvm8TargetWithDefaults */ false)
for (parameter in primaryConstructor?.valueParameters.orEmpty()) {
if (!parameter.hasValOrVar()) {
diagnosticHolder.report(ErrorsAndroid.PARCELABLE_CONSTRUCTOR_PARAMETER_SHOULD_BE_VAL_OR_VAR.on(
parameter.nameIdentifier ?: parameter))
checkParcelableClassProperty(parameter, diagnosticHolder, typeMapper)
}
}
private fun checkParcelableClassProperty(parameter: KtParameter, diagnosticHolder: DiagnosticSink, typeMapper: KotlinTypeMapper) {
if (!parameter.hasValOrVar()) {
diagnosticHolder.report(ErrorsAndroid.PARCELABLE_CONSTRUCTOR_PARAMETER_SHOULD_BE_VAL_OR_VAR.on(
parameter.nameIdentifier ?: parameter))
}
val descriptor = typeMapper.bindingContext[BindingContext.PRIMARY_CONSTRUCTOR_PARAMETER, parameter] ?: return
val type = descriptor.type
if (!type.isError) {
val asmType = typeMapper.mapType(type)
try {
ParcelSerializer.get(type, asmType, typeMapper, strict = true)
}
catch (e: IllegalArgumentException) {
// get() throws IllegalArgumentException on unknown types
diagnosticHolder.report(ErrorsAndroid.PARCELABLE_TYPE_NOT_SUPPORTED.on(
parameter.typeReference ?: parameter.nameIdentifier ?: parameter))
}
}
}
@@ -22,18 +22,22 @@ import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.incremental.components.NoLookupLocation
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.synthetic.isVisibleOutside
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeUtils
import org.jetbrains.kotlin.types.isError
import org.jetbrains.kotlin.types.typeUtil.builtIns
import org.jetbrains.org.objectweb.asm.Type
import org.jetbrains.org.objectweb.asm.commons.InstructionAdapter
import java.util.*
import java.util.concurrent.ConcurrentHashMap
private val RAWVALUE_ANNOTATION_FQNAME = FqName("kotlinx.android.parcel.RawValue")
interface ParcelSerializer {
val asmType: Type
@@ -41,16 +45,26 @@ interface ParcelSerializer {
fun readValue(v: InstructionAdapter)
companion object {
fun get(type: KotlinType, asmType: Type, typeMapper: KotlinTypeMapper, forceBoxed: Boolean = false): ParcelSerializer {
private fun KotlinTypeMapper.mapTypeSafe(type: KotlinType): Type {
return if (type.isError) Type.getObjectType("java/lang/Object") else mapType(type)
}
fun get(
type: KotlinType,
asmType: Type,
typeMapper: KotlinTypeMapper,
forceBoxed: Boolean = false,
strict: Boolean = false
): ParcelSerializer {
val className = asmType.className
fun strict() = strict && !type.annotations.hasAnnotation(RAWVALUE_ANNOTATION_FQNAME)
return when {
asmType.sort == Type.ARRAY -> {
val elementType = type.builtIns.getArrayElementType(type)
val elementSerializer = get(elementType, typeMapper.mapTypeSafe(elementType), typeMapper, strict = strict())
wrapToNullAwareIfNeeded(
type,
ArrayParcelSerializer(asmType, get(elementType, typeMapper.mapType(elementType), typeMapper)))
wrapToNullAwareIfNeeded(type, ArrayParcelSerializer(asmType, elementSerializer))
}
asmType.isPrimitive() -> {
@@ -73,7 +87,8 @@ interface ParcelSerializer {
|| className == TreeSet::class.java.canonicalName
-> {
val elementType = type.arguments.single().type
val elementSerializer = get(elementType, typeMapper.mapType(elementType), typeMapper, forceBoxed = true)
val elementSerializer = get(
elementType, typeMapper.mapTypeSafe(elementType), typeMapper, forceBoxed = true, strict = strict())
wrapToNullAwareIfNeeded(type, ListSetParcelSerializer(asmType, elementSerializer))
}
@@ -84,8 +99,10 @@ interface ParcelSerializer {
|| className == ConcurrentHashMap::class.java.canonicalName
-> {
val (keyType, valueType) = type.arguments.apply { assert(this.size == 2) }
val keySerializer = get(keyType.type, typeMapper.mapType(keyType.type), typeMapper, forceBoxed = true)
val valueSerializer = get(valueType.type, typeMapper.mapType(valueType.type), typeMapper, forceBoxed = true)
val keySerializer = get(
keyType.type, typeMapper.mapTypeSafe(keyType.type), typeMapper, forceBoxed = true, strict = strict())
val valueSerializer = get(
valueType.type, typeMapper.mapTypeSafe(valueType.type), typeMapper, forceBoxed = true, strict = strict())
wrapToNullAwareIfNeeded(type, MapParcelSerializer(asmType, keySerializer, valueSerializer))
}
@@ -131,7 +148,8 @@ interface ParcelSerializer {
asmType.isSparseArray() -> {
val elementType = type.arguments.single().type
val elementSerializer = get(elementType, typeMapper.mapType(elementType), typeMapper, forceBoxed = true)
val elementSerializer = get(
elementType, typeMapper.mapTypeSafe(elementType), typeMapper, forceBoxed = true, strict = strict())
wrapToNullAwareIfNeeded(type, SparseArrayParcelSerializer(asmType, elementSerializer))
}
@@ -158,7 +176,7 @@ interface ParcelSerializer {
Name.identifier("CREATOR"), NoLookupLocation.WHEN_GET_ALL_DESCRIPTORS).firstOrNull()
val creatorAsmType = when {
creatorVar != null -> typeMapper.mapType(creatorVar.type)
creatorVar != null -> typeMapper.mapTypeSafe(creatorVar.type)
clazz.isMagicParcelable -> Type.getObjectType(asmType.internalName + "\$Creator")
else -> null
}
@@ -175,7 +193,12 @@ interface ParcelSerializer {
Method("writeSerializable"),
Method("readSerializable"))
else -> GenericParcelSerializer
else -> {
if (strict && !type.annotations.hasAnnotation(RAWVALUE_ANNOTATION_FQNAME))
throw IllegalArgumentException("Illegal type")
else
GenericParcelSerializer
}
}
}
private fun wrapToNullAwareIfNeeded(type: KotlinType, serializer: ParcelSerializer) = when {
@@ -69,6 +69,10 @@ class DefaultErrorMessagesAndroid : DefaultErrorMessages.Extension {
MAP.put(ErrorsAndroid.CREATOR_DEFINITION_IS_FORBIDDEN,
"'CREATOR' definition is forbidden. Use 'Parceler' companion object instead.")
MAP.put(ErrorsAndroid.PARCELABLE_TYPE_NOT_SUPPORTED,
"Type is not supported by 'Parcelable'. " +
"Annotate the parameter type with '@RawValue' if you want it to be serialized using 'writeValue()'")
}
}
@@ -41,6 +41,7 @@ public interface ErrorsAndroid {
DiagnosticFactory0<PsiElement> PROPERTY_WONT_BE_SERIALIZED = DiagnosticFactory0.create(WARNING);
DiagnosticFactory0<PsiElement> OVERRIDING_WRITE_TO_PARCEL_IS_FORBIDDEN = DiagnosticFactory0.create(ERROR);
DiagnosticFactory0<PsiElement> CREATOR_DEFINITION_IS_FORBIDDEN = DiagnosticFactory0.create(ERROR);
DiagnosticFactory0<PsiElement> PARCELABLE_TYPE_NOT_SUPPORTED = DiagnosticFactory0.create(ERROR);
@SuppressWarnings("UnusedDeclaration")
Object _initializer = new Object() {
@@ -0,0 +1,25 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package kotlinx.android.parcel
/**
* Write the corresponding property value with [android.os.Parcel.writeValue].
* Serialization may fail at runtime, depending on actual property value type.
*/
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.BINARY)
annotation class RawValue