From cf607a0f14f676bcc6f154e00a61cee3964dd598 Mon Sep 17 00:00:00 2001 From: Yan Zhulanow Date: Mon, 3 Jul 2017 23:44:13 +0300 Subject: [PATCH] Parcelable: Report error on unsupported parameter types, add @RawValue annotation support --- .../parcel/ParcelableCodegenExtension.kt | 2 +- .../parcel/ParcelableDeclarationChecker.kt | 51 +++++++++++++++++-- .../parcel/serializers/ParcelSerializer.kt | 43 ++++++++++++---- .../diagnostic/DefaultErrorMessagesAndroid.kt | 4 ++ .../synthetic/diagnostic/ErrorsAndroid.java | 1 + .../src/kotlinx/android/parcel/RawValue.kt | 25 +++++++++ 6 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 plugins/android-extensions/android-extensions-runtime/src/kotlinx/android/parcel/RawValue.kt diff --git a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableCodegenExtension.kt b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableCodegenExtension.kt index 213cc74b9aa..f1d0f909f53 100644 --- a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableCodegenExtension.kt +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableCodegenExtension.kt @@ -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) } diff --git a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableDeclarationChecker.kt b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableDeclarationChecker.kt index e77bf43fbfe..5a5d56cd6ff 100644 --- a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableDeclarationChecker.kt +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableDeclarationChecker.kt @@ -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)) } } } diff --git a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/serializers/ParcelSerializer.kt b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/serializers/ParcelSerializer.kt index 66c721b1675..04a078deb34 100644 --- a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/serializers/ParcelSerializer.kt +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/serializers/ParcelSerializer.kt @@ -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 { diff --git a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/DefaultErrorMessagesAndroid.kt b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/DefaultErrorMessagesAndroid.kt index 62b2c244199..57ab5f1639c 100644 --- a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/DefaultErrorMessagesAndroid.kt +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/DefaultErrorMessagesAndroid.kt @@ -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()'") } } diff --git a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/ErrorsAndroid.java b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/ErrorsAndroid.java index a0298204da8..ab8b34c2362 100644 --- a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/ErrorsAndroid.java +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/ErrorsAndroid.java @@ -41,6 +41,7 @@ public interface ErrorsAndroid { DiagnosticFactory0 PROPERTY_WONT_BE_SERIALIZED = DiagnosticFactory0.create(WARNING); DiagnosticFactory0 OVERRIDING_WRITE_TO_PARCEL_IS_FORBIDDEN = DiagnosticFactory0.create(ERROR); DiagnosticFactory0 CREATOR_DEFINITION_IS_FORBIDDEN = DiagnosticFactory0.create(ERROR); + DiagnosticFactory0 PARCELABLE_TYPE_NOT_SUPPORTED = DiagnosticFactory0.create(ERROR); @SuppressWarnings("UnusedDeclaration") Object _initializer = new Object() { diff --git a/plugins/android-extensions/android-extensions-runtime/src/kotlinx/android/parcel/RawValue.kt b/plugins/android-extensions/android-extensions-runtime/src/kotlinx/android/parcel/RawValue.kt new file mode 100644 index 00000000000..80f955c1e78 --- /dev/null +++ b/plugins/android-extensions/android-extensions-runtime/src/kotlinx/android/parcel/RawValue.kt @@ -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 \ No newline at end of file