From c23bca6afe69f9e2c902b62564d65bc6af4cc0ee Mon Sep 17 00:00:00 2001 From: Yan Zhulanow Date: Fri, 16 Jun 2017 16:52:29 +0300 Subject: [PATCH] Parcelable: Add Parcelable functionality to Android Extensions plugin --- compiler/compiler.pro | 5 +- .../extensions/SyntheticResolveExtension.kt | 1 + .../kotlin/generators/tests/GenerateTests.kt | 5 + idea/src/META-INF/android.xml | 4 + ...eClinitClassBuilderInterceptorExtension.kt | 144 +++++ .../parcel/ParcelableCodegenExtension.kt | 252 ++++++++ .../parcel/ParcelableResolveExtension.kt | 105 ++++ .../parcel/serializers/ParcelSerializer.kt | 184 ++++++ .../parcel/serializers/ParcelSerializers.kt | 579 ++++++++++++++++++ .../synthetic/AndroidComponentRegistrar.kt | 15 + .../testData/codegen/android/FakeParcel.kt | 14 + .../testData/parcel/box/arraySimple.kt | 38 ++ .../testData/parcel/box/arrays.kt | 76 +++ .../testData/parcel/box/boxedTypes.kt | 41 ++ .../testData/parcel/box/bundle.kt | 34 + .../testData/parcel/box/listKinds.kt | 48 ++ .../testData/parcel/box/listSimple.kt | 24 + .../testData/parcel/box/lists.kt | 41 ++ .../testData/parcel/box/mapKinds.kt | 40 ++ .../testData/parcel/box/mapSimple.kt | 24 + .../testData/parcel/box/maps.kt | 40 ++ .../testData/parcel/box/nullableTypes.kt | 33 + .../parcel/box/nullableTypesSimple.kt | 28 + .../testData/parcel/box/primitiveTypes.kt | 38 ++ .../testData/parcel/box/simple.kt | 22 + .../testData/parcel/box/sparseArrays.kt | 70 +++ .../testData/parcel/box/sparseBooleanArray.kt | 36 ++ .../testData/parcel/boxLib.kt | 18 + .../testData/parcel/codegen/listInsideList.kt | 6 + .../parcel/codegen/listInsideList.txt | 82 +++ .../parcel/codegen/nullableNotNullSize.kt | 10 + .../parcel/codegen/nullableNotNullSize.txt | 80 +++ .../testData/parcel/codegen/serializable.kt | 10 + .../testData/parcel/codegen/serializable.txt | 74 +++ .../testData/parcel/codegen/simple.kt | 7 + .../testData/parcel/codegen/simple.txt | 82 +++ .../testData/parcel/codegen/simpleList.kt | 6 + .../testData/parcel/codegen/simpleList.txt | 50 ++ .../testData/parcel/codegen/size.kt | 12 + .../testData/parcel/codegen/size.txt | 179 ++++++ .../src/kotlinx/android/parcel/MagicParcel.kt | 28 + plugins/plugins-tests/plugins-tests.iml | 2 + .../AbstractAsmLikeInstructionListingTest.kt | 173 ++++++ .../android/parcel/AbstractParcelBoxTest.kt | 71 +++ .../AbstractParcelBytecodeListingTest.kt | 32 + .../kotlin/android/parcel/ParcelBoxTest.kt | 45 ++ .../ParcelBytecodeListingTestGenerated.java | 74 +++ 47 files changed, 2979 insertions(+), 3 deletions(-) create mode 100644 plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableClinitClassBuilderInterceptorExtension.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableCodegenExtension.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableResolveExtension.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/serializers/ParcelSerializer.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/serializers/ParcelSerializers.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/codegen/android/FakeParcel.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/box/arraySimple.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/box/arrays.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/box/boxedTypes.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/box/bundle.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/box/listKinds.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/box/listSimple.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/box/lists.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/box/mapKinds.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/box/mapSimple.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/box/maps.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/box/nullableTypes.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/box/nullableTypesSimple.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/box/primitiveTypes.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/box/simple.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/box/sparseArrays.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/box/sparseBooleanArray.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/boxLib.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/listInsideList.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/listInsideList.txt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/nullableNotNullSize.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/nullableNotNullSize.txt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/serializable.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/serializable.txt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simple.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simple.txt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simpleList.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simpleList.txt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/size.kt create mode 100644 plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/size.txt create mode 100644 plugins/android-extensions/android-extensions-runtime/src/kotlinx/android/parcel/MagicParcel.kt create mode 100644 plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/AbstractAsmLikeInstructionListingTest.kt create mode 100644 plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/AbstractParcelBoxTest.kt create mode 100644 plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/AbstractParcelBytecodeListingTest.kt create mode 100644 plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/ParcelBoxTest.kt create mode 100644 plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/ParcelBytecodeListingTestGenerated.java diff --git a/compiler/compiler.pro b/compiler/compiler.pro index ee9cda84991..9ac53d32f8d 100644 --- a/compiler/compiler.pro +++ b/compiler/compiler.pro @@ -186,9 +186,8 @@ messages/**) -keep class org.jetbrains.org.objectweb.asm.signature.SignatureReader { *; } -keep class org.jetbrains.org.objectweb.asm.signature.SignatureVisitor { *; } --keepclassmembers class org.jetbrains.org.objectweb.asm.Type { - *** ARRAY; - *** OBJECT; +-keep class org.jetbrains.org.objectweb.asm.Type { + public protected *; } -keepclassmembers class org.jetbrains.org.objectweb.asm.ClassReader { diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/extensions/SyntheticResolveExtension.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/extensions/SyntheticResolveExtension.kt index de838994a35..fd4ec1e3e95 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/extensions/SyntheticResolveExtension.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/extensions/SyntheticResolveExtension.kt @@ -43,6 +43,7 @@ interface SyntheticResolveExtension { override fun addSyntheticSupertypes(thisDescriptor: ClassDescriptor, supertypes: MutableList) = instances.forEach { it.addSyntheticSupertypes(thisDescriptor, supertypes) } + // todo revert override fun generateSyntheticMethods(thisDescriptor: ClassDescriptor, name: Name, fromSupertypes: List, result: MutableCollection) = diff --git a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt index 3e40e539279..ac136183852 100755 --- a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt +++ b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt @@ -28,6 +28,7 @@ import org.jetbrains.kotlin.android.folding.AbstractAndroidResourceFoldingTest import org.jetbrains.kotlin.android.intention.AbstractAndroidIntentionTest import org.jetbrains.kotlin.android.intention.AbstractAndroidResourceIntentionTest import org.jetbrains.kotlin.android.lint.AbstractKotlinLintTest +import org.jetbrains.kotlin.android.parcel.AbstractParcelBytecodeListingTest import org.jetbrains.kotlin.android.quickfix.AbstractAndroidLintQuickfixTest import org.jetbrains.kotlin.android.quickfix.AbstractAndroidQuickFixMultiFileTest import org.jetbrains.kotlin.asJava.AbstractCompilerLightClassTest @@ -1249,6 +1250,10 @@ fun main(args: Array) { testClass { model("codegen/bytecodeShape", recursive = false, extension = null) } + + testClass { + model("parcel/codegen") + } } testGroup("plugins/plugins-tests/tests", "plugins/annotation-collector/testData") { diff --git a/idea/src/META-INF/android.xml b/idea/src/META-INF/android.xml index 1b5e34fe10a..a3adc8b67a5 100644 --- a/idea/src/META-INF/android.xml +++ b/idea/src/META-INF/android.xml @@ -85,6 +85,10 @@ + + + + diff --git a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableClinitClassBuilderInterceptorExtension.kt b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableClinitClassBuilderInterceptorExtension.kt new file mode 100644 index 00000000000..9b9aca494fc --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableClinitClassBuilderInterceptorExtension.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2010-2015 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 org.jetbrains.kotlin.android.synthetic.codegen + +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.android.parcel.isMagicParcelable +import org.jetbrains.kotlin.codegen.ClassBuilder +import org.jetbrains.kotlin.codegen.ClassBuilderFactory +import org.jetbrains.kotlin.codegen.DelegatingClassBuilder +import org.jetbrains.kotlin.codegen.extensions.ClassBuilderInterceptorExtension +import org.jetbrains.kotlin.diagnostics.DiagnosticSink +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin +import org.jetbrains.org.objectweb.asm.* +import org.jetbrains.org.objectweb.asm.Opcodes.* +import org.jetbrains.org.objectweb.asm.commons.InstructionAdapter + +class ParcelableClinitClassBuilderInterceptorExtension : ClassBuilderInterceptorExtension { + override fun interceptClassBuilderFactory( + interceptedFactory: ClassBuilderFactory, + bindingContext: BindingContext, + diagnostics: DiagnosticSink + ): ClassBuilderFactory { + return ParcelableClinitClassBuilderFactory(interceptedFactory, bindingContext) + } + + private inner class ParcelableClinitClassBuilderFactory( + private val delegateFactory: ClassBuilderFactory, + val bindingContext: BindingContext + ) : ClassBuilderFactory { + + override fun newClassBuilder(origin: JvmDeclarationOrigin): ClassBuilder { + return AndroidOnDestroyCollectorClassBuilder(delegateFactory.newClassBuilder(origin), bindingContext) + } + + override fun getClassBuilderMode() = delegateFactory.classBuilderMode + + override fun asText(builder: ClassBuilder?): String? { + return delegateFactory.asText((builder as AndroidOnDestroyCollectorClassBuilder).delegateClassBuilder) + } + + override fun asBytes(builder: ClassBuilder?): ByteArray? { + return delegateFactory.asBytes((builder as AndroidOnDestroyCollectorClassBuilder).delegateClassBuilder) + } + + override fun close() { + delegateFactory.close() + } + } + + private inner class AndroidOnDestroyCollectorClassBuilder( + internal val delegateClassBuilder: ClassBuilder, + val bindingContext: BindingContext + ) : DelegatingClassBuilder() { + private var currentClass: KtClass? = null + private var currentClassName: String? = null + private var isClinitGenerated = false + + override fun getDelegate() = delegateClassBuilder + + override fun defineClass( + origin: PsiElement?, + version: Int, + access: Int, + name: String, + signature: String?, + superName: String, + interfaces: Array + ) { + if (origin is KtClass) { + currentClass = origin + } + + currentClassName = name + isClinitGenerated = false + + super.defineClass(origin, version, access, name, signature, superName, interfaces) + } + + override fun done() { + if (!isClinitGenerated && currentClass != null && currentClassName != null) { + val descriptor = bindingContext[BindingContext.CLASS, currentClass] + if (descriptor != null && descriptor.isMagicParcelable) { + val baseVisitor = super.newMethod(JvmDeclarationOrigin.NO_ORIGIN, ACC_STATIC, "", "()V", null, null) + val visitor = ClinitAwareMethodVisitor(currentClassName!!, baseVisitor) + + visitor.visitCode() + visitor.visitInsn(Opcodes.RETURN) + visitor.visitEnd() + } + } + + super.done() + } + + override fun newMethod( + origin: JvmDeclarationOrigin, + access: Int, + name: String, + desc: String, + signature: String?, + exceptions: Array? + ): MethodVisitor { + if (name == "" && currentClass != null && currentClassName != null) { + isClinitGenerated = true + return ClinitAwareMethodVisitor(currentClassName!!, super.newMethod(origin, access, name, desc, signature, exceptions)) + } + + return super.newMethod(origin, access, name, desc, signature, exceptions) + } + } + + private class ClinitAwareMethodVisitor(val parcelableName: String, mv: MethodVisitor) : MethodVisitor(Opcodes.ASM5, mv) { + override fun visitInsn(opcode: Int) { + if (opcode == Opcodes.RETURN) { + val iv = InstructionAdapter(this) + val creatorName = "$parcelableName\$CREATOR" + val creatorType = Type.getObjectType(creatorName) + + iv.anew(creatorType) + iv.dup() + iv.invokespecial(creatorName, "", "()V", false) + iv.putstatic(parcelableName, "CREATOR", creatorType.descriptor) + } + + super.visitInsn(opcode) + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000..4c9a9dc5326 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableCodegenExtension.kt @@ -0,0 +1,252 @@ +/* + * 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 org.jetbrains.kotlin.android.parcel + +import org.jetbrains.kotlin.android.parcel.ParcelableSyntheticComponent.ComponentKind.* +import org.jetbrains.kotlin.android.parcel.ParcelableResolveExtension.Companion.createMethod +import org.jetbrains.kotlin.android.parcel.serializers.ParcelSerializer +import org.jetbrains.kotlin.codegen.ExpressionCodegen +import org.jetbrains.kotlin.codegen.ImplementationBodyCodegen +import org.jetbrains.kotlin.codegen.extensions.ExpressionCodegenExtension +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.descriptorUtil.builtIns +import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin +import org.jetbrains.kotlin.resolve.jvm.jvmSignature.JvmMethodSignature +import org.jetbrains.kotlin.codegen.FunctionGenerationStrategy.CodegenBased +import org.jetbrains.kotlin.codegen.OwnerKind +import org.jetbrains.kotlin.codegen.context.ClassContext +import org.jetbrains.kotlin.codegen.writeSyntheticClassMetadata +import org.jetbrains.kotlin.descriptors.impl.ClassDescriptorImpl +import org.jetbrains.kotlin.incremental.components.NoLookupLocation.* +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.DescriptorFactory +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.resolve.descriptorUtil.module +import org.jetbrains.kotlin.resolve.scopes.MemberScope +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.Variance +import org.jetbrains.org.objectweb.asm.Opcodes.* +import org.jetbrains.org.objectweb.asm.Type +import java.io.FileDescriptor + +class ParcelableCodegenExtension : ExpressionCodegenExtension { + private companion object { + private val FILE_DESCRIPTOR_FQNAME = FqName(FileDescriptor::class.java.canonicalName) + } + + override fun generateClassSyntheticParts(codegen: ImplementationBodyCodegen) { + val parcelableClass = codegen.descriptor + if (!parcelableClass.isMagicParcelable) return + assert(parcelableClass.kind == ClassKind.CLASS || parcelableClass.kind == ClassKind.OBJECT) + + val propertiesToSerialize = getPropertiesToSerialize(codegen, parcelableClass) + + val parcelClassType = ParcelableResolveExtension.resolveParcelClassType(parcelableClass.module) + val parcelAsmType = codegen.typeMapper.mapType(parcelClassType) + + with (parcelableClass) { + writeDescribeContentsFunction(codegen, propertiesToSerialize) + writeWriteToParcel(codegen, propertiesToSerialize, parcelAsmType) + } + + writeCreatorAccessField(codegen, parcelableClass) + writeCreatorClass(codegen, parcelableClass, parcelClassType, parcelAsmType, propertiesToSerialize) + } + + private fun ClassDescriptor.writeWriteToParcel( + codegen: ImplementationBodyCodegen, + properties: List>, + parcelAsmType: Type + ): Unit? { + val containerAsmType = codegen.typeMapper.mapType(this.defaultType) + + return findFunction(WRITE_TO_PARCEL)?.write(codegen) { + for ((fieldName, type) in properties) { + val asmType = codegen.typeMapper.mapType(type) + + v.load(1, parcelAsmType) + v.load(0, containerAsmType) + v.getfield(containerAsmType.internalName, fieldName, asmType.descriptor) + + val serializer = ParcelSerializer.get(type, asmType, codegen.typeMapper) + serializer.writeValue(v) + } + + v.areturn(Type.VOID_TYPE) + } + } + + private fun ClassDescriptor.writeDescribeContentsFunction( + codegen: ImplementationBodyCodegen, + propertiesToSerialize: List> + ): Unit? { + val hasFileDescriptorAnywhere = propertiesToSerialize.any { it.second.containsFileDescriptor() } + + return findFunction(DESCRIBE_CONTENTS)?.write(codegen) { + v.aconst(if (hasFileDescriptorAnywhere) 1 /* CONTENTS_FILE_DESCRIPTOR */ else 0) + v.areturn(Type.INT_TYPE) + } + } + + private fun KotlinType.containsFileDescriptor(): Boolean { + val declarationDescriptor = this.constructor.declarationDescriptor + if (declarationDescriptor != null) { + if (declarationDescriptor.fqNameSafe == FILE_DESCRIPTOR_FQNAME) { + return true + } + } + + return this.arguments.any { it.type.containsFileDescriptor() } + } + + private fun getPropertiesToSerialize( + codegen: ImplementationBodyCodegen, + parcelableClass: ClassDescriptor + ): List> { + val constructor = parcelableClass.constructors.first { it.isPrimary } + + val propertiesToSerialize = constructor.valueParameters.map { param -> + codegen.bindingContext[BindingContext.VALUE_PARAMETER_AS_PROPERTY, param] + ?: error("Value parameter should have 'val' or 'var' keyword") + } + + return propertiesToSerialize.map { it.name.asString() /* TODO */ to it.type } + } + + private fun writeCreateFromParcel( + codegen: ImplementationBodyCodegen, + parcelableClass: ClassDescriptor, + creatorClass: ClassDescriptorImpl, + parcelClassType: KotlinType, + parcelAsmType: Type, + properties: List> + ) { + val containerAsmType = codegen.typeMapper.mapType(parcelableClass) + + createMethod(creatorClass, CREATE_FROM_PARCEL, parcelableClass.defaultType, "in" to parcelClassType).write(codegen) { + v.anew(containerAsmType) + v.dup() + + val asmConstructorParameters = StringBuilder() + + for ((_, type) in properties) { + val asmType = codegen.typeMapper.mapType(type) + asmConstructorParameters.append(asmType.descriptor) + + val serializer = ParcelSerializer.get(type, asmType, codegen.typeMapper) + v.load(1, parcelAsmType) + serializer.readValue(v) + } + + v.invokespecial(containerAsmType.internalName, "", "($asmConstructorParameters)V", false) + v.areturn(containerAsmType) + } + } + + private fun writeCreatorAccessField(codegen: ImplementationBodyCodegen, parcelableClass: ClassDescriptor) { + val parcelableAsmType = codegen.typeMapper.mapType(parcelableClass.defaultType) + val creatorAsmType = Type.getObjectType(parcelableAsmType.internalName + "\$CREATOR") + codegen.v.newField(JvmDeclarationOrigin.NO_ORIGIN, ACC_STATIC or ACC_PUBLIC or ACC_FINAL, "CREATOR", + creatorAsmType.descriptor, null, null) + } + + private fun writeCreatorClass( + codegen: ImplementationBodyCodegen, + parcelableClass: ClassDescriptor, + parcelClassType: KotlinType, + parcelAsmType: Type, + properties: List> + ) { + val containerAsmType = codegen.typeMapper.mapType(parcelableClass.defaultType) + val creatorAsmType = Type.getObjectType(containerAsmType.internalName + "\$CREATOR") + + val creatorClass = ClassDescriptorImpl( + parcelableClass.containingDeclaration, Name.identifier("Creator"), Modality.FINAL, ClassKind.CLASS, emptyList(), + parcelableClass.source, false) + + creatorClass.initialize( + MemberScope.Empty, emptySet(), + DescriptorFactory.createPrimaryConstructorForObject(creatorClass, creatorClass.source)) + + val classBuilderForCreator = codegen.state.factory.newVisitor( + JvmDeclarationOrigin.NO_ORIGIN, + Type.getObjectType(creatorAsmType.internalName), + codegen.myClass.containingKtFile) + + val classContextForCreator = ClassContext( + codegen.typeMapper, creatorClass, OwnerKind.IMPLEMENTATION, codegen.context.parentContext, null) + val codegenForCreator = ImplementationBodyCodegen( + codegen.myClass, classContextForCreator, classBuilderForCreator, codegen.state, codegen.parentCodegen, false) + + classBuilderForCreator.defineClass(null, V1_6, ACC_PUBLIC or ACC_STATIC, + creatorAsmType.internalName, null, "java/lang/Object", + arrayOf("android/os/Parcelable\$Creator")) + + writeSyntheticClassMetadata(classBuilderForCreator, codegen.state) + + writeCreatorConstructor(codegenForCreator, creatorClass, creatorAsmType) + writeNewArrayMethod(codegenForCreator, parcelableClass, creatorClass) + writeCreateFromParcel(codegenForCreator, parcelableClass, creatorClass, parcelClassType, parcelAsmType, properties) + + classBuilderForCreator.done() + } + + private fun writeCreatorConstructor(codegen: ImplementationBodyCodegen, creatorClass: ClassDescriptor, creatorAsmType: Type) { + DescriptorFactory.createPrimaryConstructorForObject(creatorClass, creatorClass.source) + .apply { + returnType = creatorClass.defaultType + }.write(codegen) { + v.load(0, creatorAsmType) + v.invokespecial("java/lang/Object", "", "()V", false) + v.areturn(Type.VOID_TYPE) + } + } + + private fun writeNewArrayMethod( + codegen: ImplementationBodyCodegen, + parcelableClass: ClassDescriptor, + creatorClass: ClassDescriptorImpl + ) { + val builtIns = parcelableClass.builtIns + val parcelableAsmType = codegen.typeMapper.mapType(parcelableClass) + + createMethod(creatorClass, NEW_ARRAY, + builtIns.getArrayType(Variance.INVARIANT, parcelableClass.defaultType), + "size" to builtIns.intType + ).write(codegen) { + v.load(1, Type.INT_TYPE) + v.newarray(parcelableAsmType) + v.areturn(Type.getType("[L$parcelableAsmType;")) + } + } + + private fun FunctionDescriptor.write(codegen: ImplementationBodyCodegen, code: ExpressionCodegen.() -> Unit) { + codegen.functionCodegen.generateMethod(JvmDeclarationOrigin.NO_ORIGIN, this, object : CodegenBased(codegen.state) { + override fun doGenerateBody(e: ExpressionCodegen, signature: JvmMethodSignature) { + e.code() + } + }) + } + + private fun ClassDescriptor.findFunction(componentKind: ParcelableSyntheticComponent.ComponentKind): SimpleFunctionDescriptor? { + return unsubstitutedMemberScope + .getContributedFunctions(Name.identifier(componentKind.methodName), WHEN_GET_ALL_DESCRIPTORS) + .firstOrNull { (it as? ParcelableSyntheticComponent)?.componentKind == componentKind } + } +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableResolveExtension.kt b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableResolveExtension.kt new file mode 100644 index 00000000000..4bdef4aa42e --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/ParcelableResolveExtension.kt @@ -0,0 +1,105 @@ +/* + * 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 org.jetbrains.kotlin.android.parcel + +import kotlinx.android.parcel.MagicParcel +import org.jetbrains.kotlin.android.parcel.ParcelableSyntheticComponent.ComponentKind.* +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.annotations.Annotations +import org.jetbrains.kotlin.descriptors.impl.SimpleFunctionDescriptorImpl +import org.jetbrains.kotlin.descriptors.impl.ValueParameterDescriptorImpl +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.resolve.descriptorUtil.builtIns +import org.jetbrains.kotlin.resolve.descriptorUtil.module +import org.jetbrains.kotlin.resolve.extensions.SyntheticResolveExtension +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.SimpleType + +class ParcelableResolveExtension : SyntheticResolveExtension { + companion object { + fun resolveParcelClassType(module: ModuleDescriptor): SimpleType { + return module.findClassAcrossModuleDependencies( + ClassId.topLevel(FqName("android.os.Parcel")))?.defaultType ?: error("Can't resolve 'android.os.Parcel' class") + } + + fun createMethod( + classDescriptor: ClassDescriptor, + componentKind: ParcelableSyntheticComponent.ComponentKind, + returnType: KotlinType, + vararg parameters: Pair + ): SimpleFunctionDescriptor { + val functionDescriptor = object : ParcelableSyntheticComponent, SimpleFunctionDescriptorImpl( + classDescriptor, + null, + Annotations.EMPTY, + Name.identifier(componentKind.methodName), + CallableMemberDescriptor.Kind.SYNTHESIZED, + classDescriptor.source + ) { + override val componentKind = componentKind + } + + val valueParameters = parameters.mapIndexed { index, (name, type) -> functionDescriptor.makeValueParameter(name, type, index) } + + functionDescriptor.initialize( + null, classDescriptor.thisAsReceiverParameter, emptyList(), valueParameters, + returnType, Modality.FINAL, Visibilities.PUBLIC) + + return functionDescriptor + } + + private fun FunctionDescriptor.makeValueParameter(name: String, type: KotlinType, index: Int): ValueParameterDescriptor { + return ValueParameterDescriptorImpl( + this, null, index, Annotations.EMPTY, Name.identifier(name), type, false, false, false, null, this.source) + } + } + + override fun getSyntheticCompanionObjectNameIfNeeded(thisDescriptor: ClassDescriptor) = null + + override fun generateSyntheticMethods( + clazz: ClassDescriptor, + name: Name, + fromSupertypes: List, + result: MutableCollection + ) { + if (name.asString() == DESCRIBE_CONTENTS.methodName && clazz.isMagicParcelable) { + result += createMethod(clazz, DESCRIBE_CONTENTS, clazz.builtIns.intType) + } else if (name.asString() == WRITE_TO_PARCEL.methodName && clazz.isMagicParcelable) { + val builtIns = clazz.builtIns + val parcelClassType = resolveParcelClassType(clazz.module) + result += createMethod(clazz, WRITE_TO_PARCEL, builtIns.unitType, "parcel" to parcelClassType, "flags" to builtIns.intType) + } + } +} + +interface ParcelableSyntheticComponent { + val componentKind: ComponentKind + + enum class ComponentKind(val methodName: String) { + WRITE_TO_PARCEL("writeToParcel"), + DESCRIBE_CONTENTS("describeContents"), + NEW_ARRAY("newArray"), + CREATE_FROM_PARCEL("createFromParcel") + } +} + +private val MAGIC_PARCEL_CLASS_FQNAME = FqName(MagicParcel::class.java.canonicalName) + +internal val ClassDescriptor.isMagicParcelable: Boolean + get() = this.annotations.hasAnnotation(MAGIC_PARCEL_CLASS_FQNAME) \ No newline at end of file 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 new file mode 100644 index 00000000000..bf063732fc4 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/serializers/ParcelSerializer.kt @@ -0,0 +1,184 @@ +/* + * 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 org.jetbrains.kotlin.android.parcel.serializers + +import org.jetbrains.kotlin.codegen.state.KotlinTypeMapper +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.types.KotlinType +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 + +interface ParcelSerializer { + val asmType: Type + + fun writeValue(v: InstructionAdapter) + fun readValue(v: InstructionAdapter) + + companion object { + fun get(type: KotlinType, asmType: Type, typeMapper: KotlinTypeMapper, forceBoxed: Boolean = false): ParcelSerializer { + val className = asmType.className + + return when { + asmType.sort == Type.ARRAY -> { + val elementType = type.builtIns.getArrayElementType(type) + + wrapToNullAwareIfNeeded( + type, + ArrayParcelSerializer(asmType, get(elementType, typeMapper.mapType(elementType), typeMapper))) + } + + asmType.isPrimitive() -> { + if (forceBoxed || type.isMarkedNullable) + wrapToNullAwareIfNeeded(type, BoxedPrimitiveTypeParcelSerializer.forUnboxedType(asmType)) + else + PrimitiveTypeParcelSerializer.getInstance(asmType) + } + + asmType.isString() -> NullCompliantObjectParcelSerializer(asmType, + Method("writeString"), + Method("readString")) + + className == List::class.java.canonicalName + || className == ArrayList::class.java.canonicalName + || className == LinkedList::class.java.canonicalName + || className == Set::class.java.canonicalName + || className == HashSet::class.java.canonicalName + || className == LinkedHashSet::class.java.canonicalName + || className == TreeSet::class.java.canonicalName + -> { + val elementType = type.arguments.single().type + val elementSerializer = get(elementType, typeMapper.mapType(elementType), typeMapper, forceBoxed = true) + wrapToNullAwareIfNeeded(type, ListSetParcelSerializer(asmType, elementSerializer)) + } + + className == Map::class.java.canonicalName + || className == HashMap::class.java.canonicalName + || className == LinkedHashMap::class.java.canonicalName + || className == TreeMap::class.java.canonicalName + || 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) + wrapToNullAwareIfNeeded(type, MapParcelSerializer(asmType, keySerializer, valueSerializer)) + } + + asmType.isBoxedPrimitive() -> wrapToNullAwareIfNeeded(type, BoxedPrimitiveTypeParcelSerializer.forBoxedType(asmType)) + + asmType.isBlob() -> NullCompliantObjectParcelSerializer(asmType, + Method("writeBlob"), + Method("readBlob")) + + asmType.isSize() -> wrapToNullAwareIfNeeded(type, NullCompliantObjectParcelSerializer(asmType, + Method("writeSize"), + Method("readSize"))) + + asmType.isSizeF() -> wrapToNullAwareIfNeeded(type, NullCompliantObjectParcelSerializer(asmType, + Method("writeSizeF"), + Method("readSizeF"))) + + asmType.isBundle() -> NullCompliantObjectParcelSerializer(asmType, + Method("writeBundle"), + Method("readBundle")) + + asmType.isPersistableBundle() -> NullCompliantObjectParcelSerializer(asmType, + Method("writeBundle"), + Method("readBundle")) + + asmType.isSparseBooleanArray() -> NullCompliantObjectParcelSerializer(asmType, + Method("writeSparseBooleanArray"), + Method("readSparseBooleanArray")) + + asmType.isSparseIntArray() -> wrapToNullAwareIfNeeded(type, SparseArrayParcelSerializer( + asmType, PrimitiveTypeParcelSerializer.getInstance(Type.INT_TYPE))) + + asmType.isSparseLongArray() -> wrapToNullAwareIfNeeded(type, SparseArrayParcelSerializer( + asmType, PrimitiveTypeParcelSerializer.getInstance(Type.LONG_TYPE))) + + asmType.isSparseArray() -> { + val elementType = type.arguments.single().type + val elementSerializer = get(elementType, typeMapper.mapType(elementType), typeMapper, forceBoxed = true) + wrapToNullAwareIfNeeded(type, SparseArrayParcelSerializer(asmType, elementSerializer)) + } + + type.isException() -> wrapToNullAwareIfNeeded(type, NullCompliantObjectParcelSerializer(asmType, + Method("writeException"), + Method("readException"))) + + asmType.isFileDescriptor() -> wrapToNullAwareIfNeeded(type, NullCompliantObjectParcelSerializer(asmType, + Method("writeRawFileDescriptor"), + Method("readRawFileDescriptor"))) + + type.isSerializable() -> NullCompliantObjectParcelSerializer(asmType, + Method("writeSerializable"), + Method("readSerializable")) + + else -> GenericParcelSerializer + } + } + private fun wrapToNullAwareIfNeeded(type: KotlinType, serializer: ParcelSerializer) = when { + type.isMarkedNullable -> NullAwareParcelSerializerWrapper(serializer) + else -> serializer + } + + private fun Type.isBlob() = this.sort == Type.ARRAY && this.elementType == Type.BYTE_TYPE + private fun Type.isString() = this.descriptor == "Ljava/lang/String;" + private fun Type.isSize() = this.descriptor == "Landroid/util/Size;" + private fun Type.isSizeF() = this.descriptor == "Landroid/util/SizeF;" + private fun Type.isFileDescriptor() = this.descriptor == "Ljava/io/FileDescriptor;" + private fun Type.isBundle() = this.descriptor == "Landroid/os/Bundle;" + private fun Type.isPersistableBundle() = this.descriptor == "Landroid/os/PersistableBundle;" + private fun Type.isSparseBooleanArray() = this.descriptor == "Landroid/util/SparseBooleanArray;" + private fun Type.isSparseIntArray() = this.descriptor == "Landroid/util/SparseIntArray;" + private fun Type.isSparseLongArray() = this.descriptor == "Landroid/util/SparseLongArray;" + private fun Type.isSparseArray() = this.descriptor == "Landroid/util/SparseArray;" + private fun KotlinType.isSerializable() = matchesFqNameWithSupertypes("java.io.Serializable") + private fun KotlinType.isException() = matchesFqNameWithSupertypes("java.lang.Exception") + + private fun Type.isPrimitive(): Boolean = when (this.sort) { + Type.BOOLEAN, Type.CHAR, Type.BYTE, Type.SHORT, Type.INT, Type.FLOAT, Type.LONG, Type.DOUBLE -> true + else -> false + } + + private fun Type.isBoxedPrimitive(): Boolean = when(this.descriptor) { + "Ljava/lang/Boolean;", + "Ljava/lang/Character;", + "Ljava/lang/Byte;", + "Ljava/lang/Short;", + "Ljava/lang/Integer;", + "Ljava/lang/Float;", + "Ljava/lang/Long;", + "Ljava/lang/Double;" -> true + else -> false + } + + private fun KotlinType.matchesFqNameWithSupertypes(fqName: String): Boolean { + if (this.matchesFqName(fqName)) { + return true + } + + return this.constructor.supertypes.any { it.matchesFqName(fqName) } + } + + private fun KotlinType.matchesFqName(fqName: String): Boolean { + return this.constructor.declarationDescriptor?.fqNameSafe?.asString() == fqName + } + } +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/serializers/ParcelSerializers.kt b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/serializers/ParcelSerializers.kt new file mode 100644 index 00000000000..582d692ea42 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/parcel/serializers/ParcelSerializers.kt @@ -0,0 +1,579 @@ +/* + * 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 org.jetbrains.kotlin.android.parcel.serializers + +import org.jetbrains.org.objectweb.asm.Label +import org.jetbrains.org.objectweb.asm.Type +import org.jetbrains.org.objectweb.asm.commons.InstructionAdapter + +private val PARCEL_TYPE = Type.getObjectType("android/os/Parcel") + +internal object GenericParcelSerializer : ParcelSerializer { + override val asmType: Type = Type.getObjectType("java/lang/Object") + + override fun writeValue(v: InstructionAdapter) { + v.invokevirtual(PARCEL_TYPE.internalName, "writeValue", "(Ljava/lang/Object;)V", false) + } + + override fun readValue(v: InstructionAdapter) { + v.aconst(asmType) // -> parcel, type + v.invokevirtual("java/lang/Class", "getClassLoader", "()Ljava/lang/ClassLoader;", false) // -> parcel, classloader + v.invokevirtual(PARCEL_TYPE.internalName, "readValue", "(Ljava/lang/ClassLoader;)Ljava/lang/Object;", false) + } +} + +internal class ArrayParcelSerializer(override val asmType: Type, private val elementSerializer: ParcelSerializer) : ParcelSerializer { + override fun writeValue(v: InstructionAdapter) { + v.dupX1() // -> arr, parcel, arr + v.arraylength() // -> arr, parcel, length + v.dupX2() // -> length, arr, parcel, length + + // Write array size + v.invokevirtual(PARCEL_TYPE.internalName, "writeInt", "(I)V", false) // -> length, arr + v.swap() // -> arr, length + v.aconst(0) // -> arr, length, + + val nextLoopIteration = Label() + val loopIsOver = Label() + + v.visitLabel(nextLoopIteration) + + // Loop + v.dup2() // -> arr, length, index, length, index + v.ificmple(loopIsOver) // -> arr, length, index + + v.swap() // -> arr, index, length + v.dupX2() // -> length, arr, index, length + v.pop() // -> length, arr, index + v.dup2() // -> length, arr, index, arr, index + v.aload(elementSerializer.asmType) // -> length, arr, index, obj + v.castIfNeeded(elementSerializer.asmType) + + v.load(1, PARCEL_TYPE) // -> length, arr, index, obj, parcel + v.swap() // -> length, arr, index, parcel, obj + elementSerializer.writeValue(v) // -> length, arr, index + + v.aconst(1) // -> length, arr, index, (1) + v.add(Type.INT_TYPE) // -> length, arr, (index + 1) + v.swap() // -> length, (index + 1), arr + v.dupX2() // -> arr, length, (index + 1), arr + v.pop() // -> arr, length, (index + 1) + v.goTo(nextLoopIteration) + + v.visitLabel(loopIsOver) + v.pop2() // -> arr + v.pop() + } + + override fun readValue(v: InstructionAdapter) { + v.invokevirtual(PARCEL_TYPE.internalName, "readInt", "()I", false) // -> length + v.dup() // -> length, length + v.newarray(elementSerializer.asmType) // -> length, arr + v.swap() // -> arr, length + v.aconst(0) // -> arr, length, index + + val nextLoopIteration = Label() + val loopIsOver = Label() + + v.visitLabel(nextLoopIteration) + v.dup2() // -> arr, length, index, length, index + v.ificmple(loopIsOver) // -> arr, length, index + + v.swap() // -> arr, index, length + v.dupX2() // -> length, arr, index, length + v.pop() // -> length, arr, index + v.dup2() // -> length, arr, index, arr, index + + v.load(1, PARCEL_TYPE) // -> length, arr, index, arr, index, parcel + elementSerializer.readValue(v) // -> length, arr, index, arr, index, obj + v.castIfNeeded(elementSerializer.asmType) + v.astore(elementSerializer.asmType) // -> length, arr, index + v.aconst(1) // -> length, arr, index, (1) + v.add(Type.INT_TYPE) // -> length, arr, (index + 1) + v.swap() // -> length, (index + 1), arr + v.dupX2() // -> arr, length, (index + 1), arr + v.pop() // -> arr, length, (index + 1) + v.goTo(nextLoopIteration) + + v.visitLabel(loopIsOver) + v.pop2() // -> arr + } +} + +internal fun InstructionAdapter.castIfNeeded(targetType: Type) { + if (targetType.sort != Type.OBJECT && targetType.sort != Type.ARRAY) return + if (targetType.descriptor == "Ljava/lang/Object;") return + checkcast(targetType) +} + +internal class ListSetParcelSerializer( + asmType: Type, + elementSerializer: ParcelSerializer +) : AbstractCollectionParcelSerializer(asmType, elementSerializer) { + override fun getSize(v: InstructionAdapter) { + v.invokeinterface("java/util/Collection", "size", "()I") + } + + override fun getIterator(v: InstructionAdapter) { + v.invokeinterface("java/util/Collection", "iterator", "()Ljava/util/Iterator;") + } + + override fun doWriteValue(v: InstructionAdapter) { + // -> parcel, obj + v.castIfNeeded(elementSerializer.asmType) + elementSerializer.writeValue(v) + } + + override fun doReadValue(v: InstructionAdapter) { + // -> collection, parcel + + elementSerializer.readValue(v) // -> collection, element + v.castIfNeeded(elementSerializer.asmType) + + v.invokevirtual(collectionType.internalName, "add", "(Ljava/lang/Object;)Z", false) // -> bool + v.pop() + } +} + +internal class MapParcelSerializer( + asmType: Type, + private val keySerializer: ParcelSerializer, + elementSerializer: ParcelSerializer +) : AbstractCollectionParcelSerializer(asmType, elementSerializer) { + override fun getSize(v: InstructionAdapter) { + v.invokeinterface("java/util/Map", "size", "()I") + } + + override fun getIterator(v: InstructionAdapter) { + v.invokeinterface("java/util/Map", "entrySet", "()Ljava/util/Set;") + v.invokeinterface("java/util/Set", "iterator", "()Ljava/util/Iterator;") + } + + override fun doWriteValue(v: InstructionAdapter) { + // -> parcel, obj + + v.dup2() // -> parcel, obj, parcel, obj + + v.invokeinterface("java/util/Map\$Entry", "getKey", "()Ljava/lang/Object;") // -> parcel, obj, parcel, key + v.castIfNeeded(keySerializer.asmType) + keySerializer.writeValue(v) // -> parcel, obj + + v.invokeinterface("java/util/Map\$Entry", "getValue", "()Ljava/lang/Object;") // -> parcel, value + v.castIfNeeded(elementSerializer.asmType) + elementSerializer.writeValue(v) + } + + override fun doReadValue(v: InstructionAdapter) { + // -> map, parcel + v.dup() // -> map, parcel, parcel + + keySerializer.readValue(v) // -> map, parcel, key + v.castIfNeeded(keySerializer.asmType) + + v.swap() // -> map, key, parcel + + elementSerializer.readValue(v) // -> map, key, value + v.castIfNeeded(elementSerializer.asmType) + + v.invokeinterface("java/util/Map", "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;") // -> obj + v.pop() + } +} + +abstract internal class AbstractCollectionParcelSerializer( + final override val asmType: Type, + protected val elementSerializer: ParcelSerializer +) : ParcelSerializer { + protected val collectionType: Type = Type.getObjectType(when (asmType.internalName) { + "java/util/List" -> "java/util/ArrayList" + "java/util/Set" -> "java/util/LinkedHashSet" + "java/util/Map" -> "java/util/LinkedHashMap" + else -> asmType.internalName + }) + + private var hasConstructorWithCapacity = when (collectionType.internalName) { + "java/util/LinkedList", "java/util/TreeSet", "java/util/TreeMap" -> false + else -> true + } + + /** + * Stack before: collection + * Stack after: size + */ + protected abstract fun getSize(v: InstructionAdapter) + + /** + * Stack before: collection + * Stack after: iterator + */ + protected abstract fun getIterator(v: InstructionAdapter) + + /** + * Stack before: parcel, obj + * Stack after: + */ + protected abstract fun doWriteValue(v: InstructionAdapter) + + /** + * Stack before: collection, parcel + * Stack after: + */ + protected abstract fun doReadValue(v: InstructionAdapter) + + override fun writeValue(v: InstructionAdapter) { + val labelIteratorLoop = Label() + val labelReturn = Label() + + v.dupX1() // -> collection, parcel, collection + getSize(v) // -> collection, parcel, size + v.invokevirtual(PARCEL_TYPE.internalName, "writeInt", "(I)V", false) // collection + getIterator(v) // -> iterator + + v.visitLabel(labelIteratorLoop) + v.dup() // -> iterator, iterator + v.invokeinterface("java/util/Iterator", "hasNext", "()Z") // -> iterator, hasNext + v.ifeq(labelReturn) // -> iterator + + v.dup() // -> iterator, iterator + v.invokeinterface("java/util/Iterator", "next", "()Ljava/lang/Object;") // -> iterator, obj + + v.load(1, PARCEL_TYPE) // -> iterator, obj, parcel + v.swap() // -> iterator, parcel, obj + doWriteValue(v) // -> iterator + + v.goTo(labelIteratorLoop) + + v.visitLabel(labelReturn) + v.pop() + } + + override fun readValue(v: InstructionAdapter) { + val nextLoopIteration = Label() + val loopIsOver = Label() + + // Read list size + v.invokevirtual(PARCEL_TYPE.internalName, "readInt", "()I", false) // -> size + v.dup() // -> size, size + + v.anew(collectionType) // -> size, size, list + v.dupX1() // -> size, list, size, list + v.swap() // -> size, list, list, size + + if (hasConstructorWithCapacity) { + v.invokespecial(collectionType.internalName, "", "(I)V", false) // -> size, list + } + else { + v.pop() // -> size, list, list + v.invokespecial(collectionType.internalName, "", "()V", false) // -> size, list + } + + v.visitLabel(nextLoopIteration) + v.swap() // -> list, size + v.dupX1() // -> size, list, size + v.ifeq(loopIsOver) // -> size, list + v.dup() // -> size, list, list + v.load(1, PARCEL_TYPE) // -> size, list, list, parcel + doReadValue(v) // -> size, list + + v.swap() // -> list, size + v.aconst(-1) // -> list, size, (-1) + v.add(Type.INT_TYPE) // -> list, (size - 1) + + v.swap() // -> size, list + v.goTo(nextLoopIteration) + + v.visitLabel(loopIsOver) + v.swap() + v.pop() + } +} + +internal class SparseArrayParcelSerializer(override val asmType: Type, private val valueSerializer: ParcelSerializer) : ParcelSerializer { + private val valueType = (valueSerializer as? PrimitiveTypeParcelSerializer)?.asmType ?: Type.getObjectType("java/lang/Object") + + override fun writeValue(v: InstructionAdapter) { + v.dup() // -> parcel, arr, arr + v.invokevirtual(asmType.internalName, "size", "()I", false) // -> parcel, arr, size + v.dup2X1() // -> arr, size, parcel, arr, size + v.swap() // -> arr, size, parcel, size, arr + v.pop() // -> arr, size, parcel, size + v.invokevirtual(PARCEL_TYPE.internalName, "writeInt", "(I)V", false) // -> arr, size + + v.aconst(0) // -> arr, size, + + val nextLoopIteration = Label() + val loopIsOver = Label() + + v.visitLabel(nextLoopIteration) + v.dup2() // -> arr, size, index, size, index + v.ificmple(loopIsOver) // -> arr, size, index + + v.swap() // -> arr, index, size + v.dupX2() // -> size, arr, index, size + v.pop() // -> size, arr, index + v.dup2() // -> size, arr, index, arr, index + v.invokevirtual(asmType.internalName, "keyAt", "(I)I", false) // -> size, arr, index, key + v.load(1, PARCEL_TYPE) // size, arr, index, key, parcel + v.dupX2() // -> size, arr, parcel, index, key, parcel + v.swap() // -> size, arr, parcel, index, parcel, key + v.invokevirtual(PARCEL_TYPE.internalName, "writeInt", "(I)V", false) // -> size, arr, parcel, index + + v.swap() // -> size, arr, index, parcel + v.dupX2() // -> size, parcel, arr, index, parcel + v.pop() // -> size, parcel, arr, index + v.dup2X1() // -> size, arr, index, parcel, arr, index + v.invokevirtual(asmType.internalName, "valueAt", "(I)${valueType.descriptor}", false) // -> size, arr, index, parcel, value + valueSerializer.writeValue(v) // -> size, arr, index + + v.aconst(1) // -> size, arr, index, (1) + v.add(Type.INT_TYPE) // -> size, arr, (index + 1) + v.dup2X1() // -> arr, (index + 1), size, arr, (index + 1) + v.pop2() // -> arr, (index + 1), size + v.swap() // -> arr, size, (index + 1) + v.goTo(nextLoopIteration) + + v.visitLabel(loopIsOver) + v.pop2() + v.pop() + } + + override fun readValue(v: InstructionAdapter) { + v.invokevirtual(PARCEL_TYPE.internalName, "readInt", "()I", false) // -> size + v.dup() // -> size, size + v.anew(asmType) // -> size, size, arr + v.dupX2() // -> arr, size, size, arr + v.swap() // -> arr, size, arr, size + v.invokespecial(asmType.internalName, "", "(I)V", false) // -> arr, size + + val nextLoopIteration = Label() + val loopIsOver = Label() + + v.visitLabel(nextLoopIteration) + v.dup() // -> arr, size, size + v.ifle(loopIsOver) // -> arr, size + v.swap() // -> size, arr + v.dupX1() // -> arr, size, arr + + v.load(1, PARCEL_TYPE) // -> arr, size, arr, parcel + v.dup() // -> arr, size, arr, parcel, parcel + v.invokevirtual(PARCEL_TYPE.internalName, "readInt", "()I", false) // -> arr, size, arr, parcel, key + v.swap() // -> arr, size, arr, key, parcel + valueSerializer.readValue(v) // -> arr, size, arr, key, value + v.invokevirtual(asmType.internalName, "put", "(I${valueType.descriptor})V", false) // -> arr, size + v.aconst(-1) // -> arr, size, (-1) + v.add(Type.INT_TYPE) // -> arr, (size - 1) + v.goTo(nextLoopIteration) + + v.visitLabel(loopIsOver) + v.pop() // -> arr + } +} + +internal class NullAwareParcelSerializerWrapper(val delegate: ParcelSerializer) : ParcelSerializer { + override val asmType: Type + get() = delegate.asmType + + override fun writeValue(v: InstructionAdapter) = writeValueNullAware(v) { + delegate.writeValue(v) + } + + override fun readValue(v: InstructionAdapter) = readValueNullAware(v) { + delegate.readValue(v) + } +} + +/** write...() and get...() methods in Android should support passing `null` values. */ +internal class NullCompliantObjectParcelSerializer( + override val asmType: Type, + writeMethod: Method, + readMethod: Method +) : ParcelSerializer { + private val writeMethod = Method(writeMethod.name, writeMethod.signature ?: "(${asmType.descriptor})V") + private val readMethod = Method(readMethod.name, readMethod.signature ?: "()${asmType.descriptor}") + + override fun writeValue(v: InstructionAdapter) { + v.invokevirtual(PARCEL_TYPE.internalName, writeMethod.name, writeMethod.signature, false) + } + + override fun readValue(v: InstructionAdapter) { + v.invokevirtual(PARCEL_TYPE.internalName, readMethod.name, readMethod.signature, false) + v.castIfNeeded(asmType) + } +} + +internal class BoxedPrimitiveTypeParcelSerializer private constructor(val unboxedType: Type) : ParcelSerializer { + companion object { + private val BOXED_TO_UNBOXED_TYPE_MAPPINGS = mapOf( + "java/lang/Boolean" to Type.BOOLEAN_TYPE, + "java/lang/Character" to Type.CHAR_TYPE, + "java/lang/Byte" to Type.BYTE_TYPE, + "java/lang/Short" to Type.SHORT_TYPE, + "java/lang/Integer" to Type.INT_TYPE, + "java/lang/Float" to Type.FLOAT_TYPE, + "java/lang/Long" to Type.LONG_TYPE, + "java/lang/Double" to Type.DOUBLE_TYPE) + + private val UNBOXED_TO_BOXED_TYPE_MAPPINGS = BOXED_TO_UNBOXED_TYPE_MAPPINGS.map { it.value to it.key }.toMap() + + private val BOXED_VALUE_METHOD_NAMES = mapOf( + "java/lang/Boolean" to "booleanValue", + "java/lang/Character" to "charValue", + "java/lang/Byte" to "byteValue", + "java/lang/Short" to "shortValue", + "java/lang/Integer" to "intValue", + "java/lang/Float" to "floatValue", + "java/lang/Long" to "longValue", + "java/lang/Double" to "doubleValue") + + private val INSTANCES = BOXED_TO_UNBOXED_TYPE_MAPPINGS.values.map { type -> + type to BoxedPrimitiveTypeParcelSerializer(type) + }.toMap() + + fun forUnboxedType(type: Type) = INSTANCES[type] ?: error("Unsupported type $type") + fun forBoxedType(type: Type) = INSTANCES[BOXED_TO_UNBOXED_TYPE_MAPPINGS[type.internalName]] ?: error("Unsupported type $type") + } + + override val asmType: Type = Type.getObjectType(UNBOXED_TO_BOXED_TYPE_MAPPINGS[unboxedType] ?: error("Unsupported type $unboxedType")) + val unboxedSerializer = PrimitiveTypeParcelSerializer.getInstance(unboxedType) + val typeValueMethodName = BOXED_VALUE_METHOD_NAMES[asmType.internalName]!! + + override fun writeValue(v: InstructionAdapter) { + v.invokevirtual(asmType.internalName, typeValueMethodName, "()${unboxedType.descriptor}", false) + unboxedSerializer.writeValue(v) + } + + override fun readValue(v: InstructionAdapter) { + unboxedSerializer.readValue(v) + v.invokestatic(asmType.internalName, "valueOf", "(${unboxedType.descriptor})${asmType.descriptor}", false) + } +} + +internal open class PrimitiveTypeParcelSerializer private constructor(final override val asmType: Type) : ParcelSerializer { + companion object { + private val WRITE_METHOD_NAMES = mapOf( + Type.BOOLEAN_TYPE to Method("writeInt", "(I)V"), + Type.CHAR_TYPE to Method("writeInt", "(I)V"), + Type.BYTE_TYPE to Method("writeByte", "(B)V"), + Type.SHORT_TYPE to Method("writeInt", "(I)V"), + Type.INT_TYPE to Method("writeInt", "(I)V"), + Type.FLOAT_TYPE to Method("writeFloat", "(F)V"), + Type.LONG_TYPE to Method("writeLong", "(J)V"), + Type.DOUBLE_TYPE to Method("writeDouble", "(D)V")) + + private val READ_METHOD_NAMES = mapOf( + Type.BOOLEAN_TYPE to Method("readInt", "()I"), + Type.CHAR_TYPE to Method("readInt", "()I"), + Type.BYTE_TYPE to Method("readByte", "()B"), + Type.SHORT_TYPE to Method("readInt", "()I"), + Type.INT_TYPE to Method("readInt", "()I"), + Type.FLOAT_TYPE to Method("readFloat", "()F"), + Type.LONG_TYPE to Method("readLong", "()J"), + Type.DOUBLE_TYPE to Method("readDouble", "()D")) + + private val INSTANCES = READ_METHOD_NAMES.keys.map { + it to when (it) { + Type.CHAR_TYPE -> CharParcelSerializer + Type.SHORT_TYPE -> ShortParcelSerializer + else -> PrimitiveTypeParcelSerializer(it) + } + }.toMap() + + fun getInstance(type: Type) = INSTANCES[type] ?: error("Unsupported type ${type.descriptor}") + } + + object CharParcelSerializer : PrimitiveTypeParcelSerializer(Type.CHAR_TYPE) { + override fun writeValue(v: InstructionAdapter) { + v.cast(Type.CHAR_TYPE, Type.INT_TYPE) + super.writeValue(v) + } + + override fun readValue(v: InstructionAdapter) { + super.readValue(v) + v.cast(Type.INT_TYPE, Type.CHAR_TYPE) + } + } + + object ShortParcelSerializer : PrimitiveTypeParcelSerializer(Type.SHORT_TYPE) { + override fun writeValue(v: InstructionAdapter) { + v.cast(Type.SHORT_TYPE, Type.INT_TYPE) + super.writeValue(v) + } + + override fun readValue(v: InstructionAdapter) { + super.readValue(v) + v.cast(Type.INT_TYPE, Type.SHORT_TYPE) + } + } + + private val writeMethod = WRITE_METHOD_NAMES[asmType]!! + private val readMethod = READ_METHOD_NAMES[asmType]!! + + override fun writeValue(v: InstructionAdapter) { + v.invokevirtual(PARCEL_TYPE.internalName, writeMethod.name, writeMethod.signature, false) + } + + override fun readValue(v: InstructionAdapter) { + v.invokevirtual(PARCEL_TYPE.internalName, readMethod.name, readMethod.signature, false) + } +} + +private fun readValueNullAware(v: InstructionAdapter, block: () -> Unit) { + val labelNull = Label() + val labelReturn = Label() + + v.invokevirtual(PARCEL_TYPE.internalName, "readInt", "()I", false) + v.ifeq(labelNull) + + v.load(1, PARCEL_TYPE) + block() + v.goTo(labelReturn) + + // Just push null on stack if the value is null + v.visitLabel(labelNull) + v.aconst(null) + + v.visitLabel(labelReturn) +} + +private fun writeValueNullAware(v: InstructionAdapter, block: () -> Unit) { + val labelReturn = Label() + val labelNull = Label() + v.dup() + v.ifnull(labelNull) + + // Write 1 if non-null, 0 if null + + v.load(1, PARCEL_TYPE) + v.aconst(1) + v.invokevirtual(PARCEL_TYPE.internalName, "writeInt", "(I)V", false) + block() + + v.goTo(labelReturn) + + labelNull@ v.visitLabel(labelNull) + v.pop() + v.aconst(0) + v.invokevirtual(PARCEL_TYPE.internalName, "writeInt", "(I)V", false) + + labelReturn@ v.visitLabel(labelReturn) +} + +internal class Method(val name: String, val signature: T) { + companion object { + operator fun invoke(name: String) = Method(name, null) + } +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/AndroidComponentRegistrar.kt b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/AndroidComponentRegistrar.kt index 655f4bf1b17..c797a570f75 100644 --- a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/AndroidComponentRegistrar.kt +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/AndroidComponentRegistrar.kt @@ -18,6 +18,9 @@ package org.jetbrains.kotlin.android.synthetic import com.intellij.mock.MockProject import com.intellij.openapi.extensions.Extensions +import com.intellij.openapi.project.Project +import org.jetbrains.kotlin.android.parcel.ParcelableCodegenExtension +import org.jetbrains.kotlin.android.parcel.ParcelableResolveExtension import org.jetbrains.kotlin.android.synthetic.codegen.AndroidOnDestroyClassBuilderInterceptorExtension import org.jetbrains.kotlin.android.synthetic.codegen.CliAndroidExtensionsExpressionCodegenExtension import org.jetbrains.kotlin.android.synthetic.diagnostic.AndroidExtensionPropertiesCallChecker @@ -41,6 +44,8 @@ import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor import org.jetbrains.kotlin.resolve.TargetPlatform import org.jetbrains.kotlin.resolve.jvm.extensions.PackageFragmentProviderExtension import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform +import org.jetbrains.kotlin.android.synthetic.codegen.ParcelableClinitClassBuilderInterceptorExtension +import org.jetbrains.kotlin.resolve.extensions.SyntheticResolveExtension object AndroidConfigurationKeys { val VARIANT: CompilerConfigurationKey> = CompilerConfigurationKey.create>("Android build variant") @@ -75,7 +80,17 @@ class AndroidCommandLineProcessor : CommandLineProcessor { } class AndroidComponentRegistrar : ComponentRegistrar { + companion object { + fun registerParcelExtensions(project: Project) { + ExpressionCodegenExtension.registerExtension(project, ParcelableCodegenExtension()) + SyntheticResolveExtension.registerExtension(project, ParcelableResolveExtension()) + ClassBuilderInterceptorExtension.registerExtension(project, ParcelableClinitClassBuilderInterceptorExtension()) + } + } + override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { + registerParcelExtensions(project) + val applicationPackage = configuration.get(AndroidConfigurationKeys.PACKAGE) val variants = configuration.get(AndroidConfigurationKeys.VARIANT)?.mapNotNull { parseVariant(it) } ?: emptyList() val isExperimental = configuration.get(AndroidConfigurationKeys.EXPERIMENTAL) == "true" diff --git a/plugins/android-extensions/android-extensions-compiler/testData/codegen/android/FakeParcel.kt b/plugins/android-extensions/android-extensions-compiler/testData/codegen/android/FakeParcel.kt new file mode 100644 index 00000000000..aa4ddeb351e --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/codegen/android/FakeParcel.kt @@ -0,0 +1,14 @@ +package android.os + +class Parcel + +interface Parcelable { + fun describeContents(): Int + + fun writeToParcel(parcel: Parcel, flags: Int) + + interface Creator { + fun createFromParcel(parcel: Parcel): T + fun newArray(size: Int): Array + } +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/arraySimple.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/arraySimple.kt new file mode 100644 index 00000000000..6ec7e16f3a0 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/arraySimple.kt @@ -0,0 +1,38 @@ +// WITH_RUNTIME + +@file:JvmName("TestKt") +package test + +import kotlinx.android.parcel.* +import android.os.Parcel +import android.os.Parcelable +import java.util.Arrays + +@MagicParcel +data class Test(val a: Array) : Parcelable { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other?.javaClass != javaClass) return false + + other as Test + + if (!Arrays.equals(a, other.a)) return false + + return true + } + + override fun hashCode() = Arrays.hashCode(a) +} + +fun box() = parcelTest { parcel -> + val first = Test(arrayOf("A", "B")) + + first.writeToParcel(parcel, 0) + + val bytes = parcel.marshall() + parcel.unmarshall(bytes, 0, bytes.size) + + val first2 = readFromParcel(parcel) + + assert(first == first2) +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/arrays.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/arrays.kt new file mode 100644 index 00000000000..8a9b2434607 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/arrays.kt @@ -0,0 +1,76 @@ +// WITH_RUNTIME + +@file:JvmName("TestKt") +package test + +import kotlinx.android.parcel.* +import android.os.Parcel +import android.os.Parcelable +import java.util.Arrays + +@MagicParcel +data class Test( + val a: Array, + val b: Array, + val c: IntArray, + val d: CharArray?, + val e: Array, + val f: Array>, + val g: List>, + val h: Array? +) : Parcelable { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other?.javaClass != javaClass) return false + + other as Test + + if (!Arrays.equals(a, other.a)) return false + if (!Arrays.equals(b, other.b)) return false + if (!Arrays.equals(c, other.c)) return false + if (!Arrays.equals(d, other.d)) return false + if (!Arrays.deepEquals(e, other.e)) return false + if (!Arrays.equals(f, other.f)) return false + + if (g.size != other.g.size) return false + if (!g.zip(other.g).all { (f, s) -> Arrays.equals(f, s) }) return false + + if (!Arrays.equals(h, other.h)) return false + + return true + } + + override fun hashCode(): Int { + var result = Arrays.hashCode(a) + result = 31 * result + Arrays.hashCode(b) + result = 31 * result + Arrays.hashCode(c) + result = 31 * result + (d?.let { Arrays.hashCode(it) } ?: 0) + result = 31 * result + Arrays.hashCode(e) + result = 31 * result + Arrays.hashCode(f) + result = 31 * result + g.hashCode() + result = 31 * result + (h?.let { Arrays.hashCode(it) } ?: 0) + return result + } +} + +fun box() = parcelTest { parcel -> + val first = Test( + a = arrayOf("A", "B", "C"), + b = arrayOf("A", null, "B"), + c = intArrayOf(1, 2, 3), + d = null, + e = arrayOf(intArrayOf(2, 4, 1), intArrayOf(10, 20)), + f = arrayOf(listOf("A"), listOf("B", "C")), + g = listOf(arrayOf("Z", "X"), arrayOf()), + h = arrayOf("") + ) + + first.writeToParcel(parcel, 0) + + val bytes = parcel.marshall() + parcel.unmarshall(bytes, 0, bytes.size) + + val first2 = readFromParcel(parcel) + + assert(first == first2) +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/boxedTypes.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/boxedTypes.kt new file mode 100644 index 00000000000..1eb3fa91a5b --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/boxedTypes.kt @@ -0,0 +1,41 @@ +// WITH_RUNTIME + +@file:JvmName("TestKt") +package test + +import kotlinx.android.parcel.* +import android.os.Parcel +import android.os.Parcelable + +@MagicParcel +data class BoxedTypes( + val boo: java.lang.Boolean, + val c: java.lang.Character, + val byt: java.lang.Byte, + val s: java.lang.Short, + val i: java.lang.Integer, + val f: java.lang.Float, + val l: java.lang.Long, + val d: java.lang.Double +) : Parcelable + +fun box() = parcelTest { parcel -> + val first = BoxedTypes( + true as java.lang.Boolean, + '#' as java.lang.Character, + 3.toByte() as java.lang.Byte, + 10.toShort() as java.lang.Short, + -300 as java.lang.Integer, + -5.0f as java.lang.Float, + Long.MAX_VALUE as java.lang.Long, + 3.14 as java.lang.Double) + + first.writeToParcel(parcel, 0) + + val bytes = parcel.marshall() + parcel.unmarshall(bytes, 0, bytes.size) + + val first2 = readFromParcel(parcel) + + assert(first == first2) +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/bundle.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/bundle.kt new file mode 100644 index 00000000000..0739489d075 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/bundle.kt @@ -0,0 +1,34 @@ +// WITH_RUNTIME + +@file:JvmName("TestKt") +package test + +import kotlinx.android.parcel.* +import android.os.Parcel +import android.os.Parcelable +import android.os.Bundle + +@MagicParcel +data class User(val a: Bundle) : Parcelable + +fun box() = parcelTest { parcel -> + val test = User(Bundle().apply { putChar("A", 'c'); putByte("B", 40.toByte()); putString("C", "ABC") }) + test.writeToParcel(parcel, 0) + + val bytes = parcel.marshall() + parcel.unmarshall(bytes, 0, bytes.size) + + val test2 = readFromParcel(parcel) + + assert(compareBundles(test.a, test2.a)) +} + +private fun compareBundles(first: Bundle, second: Bundle): Boolean { + if (first.size() != second.size()) return false + + for (key in first.keySet()) { + if (first.get(key) != second.get(key)) return false + } + + return true +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/listKinds.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/listKinds.kt new file mode 100644 index 00000000000..747359aed1b --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/listKinds.kt @@ -0,0 +1,48 @@ +// WITH_RUNTIME +// FULL_JDK + +@file:JvmName("TestKt") +package test + +import kotlinx.android.parcel.* +import android.os.Parcel +import android.os.Parcelable +import java.util.* + +@MagicParcel +data class Test( + val a: List, + val b: MutableList, + val c: ArrayList, + val d: LinkedList, + val e: Set, + val f: MutableSet, + val g: TreeSet, + val h: HashSet, + val i: LinkedHashSet +) : Parcelable + +fun box() = parcelTest { parcel -> + val first = Test( + a = listOf("A"), + b = mutableListOf("B"), + c = ArrayList().apply { this += "C" }, + d = LinkedList().apply { this += "D" }, + e = setOf("E"), + f = mutableSetOf("F"), + g = TreeSet().apply { this += "G" }, + h = HashSet().apply { this += "H" }, + i = LinkedHashSet().apply { this += "I" } + ) + + first.writeToParcel(parcel, 0) + + val bytes = parcel.marshall() + parcel.unmarshall(bytes, 0, bytes.size) + + val first2 = readFromParcel(parcel) + + assert(first == first2) + assert((first.d as LinkedList<*>).size == 1) + assert((first2.h as HashSet<*>).size == 1) +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/listSimple.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/listSimple.kt new file mode 100644 index 00000000000..db4c01d2d3e --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/listSimple.kt @@ -0,0 +1,24 @@ +// WITH_RUNTIME + +@file:JvmName("TestKt") +package test + +import kotlinx.android.parcel.* +import android.os.Parcel +import android.os.Parcelable + +@MagicParcel +data class Test(val a: List) : Parcelable + +fun box() = parcelTest { parcel -> + val first = Test(listOf("A", "B")) + + first.writeToParcel(parcel, 0) + + val bytes = parcel.marshall() + parcel.unmarshall(bytes, 0, bytes.size) + + val first2 = readFromParcel(parcel) + + assert(first == first2) +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/lists.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/lists.kt new file mode 100644 index 00000000000..71750bc8b88 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/lists.kt @@ -0,0 +1,41 @@ +// WITH_RUNTIME + +@file:JvmName("TestKt") +package test + +import kotlinx.android.parcel.* +import android.os.Parcel +import android.os.Parcelable + +@MagicParcel +data class Test( + val a: List, + val b: List, + val c: List, + val d: List, + val e: List?>, + val f: List>> +) : Parcelable + +fun box() = parcelTest { parcel -> + val first = Test( + a = listOf("A", "B"), + b = listOf("A", null, "C"), + c = listOf(1, 2, 3), + d = listOf(1, null, 5), + e = listOf(listOf("A", "B"), listOf(), null), + f = listOf(listOf(listOf(1, 2), listOf(3)), listOf(listOf(5, 3))) + ) + + first.writeToParcel(parcel, 0) + + val bytes = parcel.marshall() + parcel.unmarshall(bytes, 0, bytes.size) + + val first2 = readFromParcel(parcel) + + assert(first == first2) + + assert(first2.a == listOf("A", "B")) + assert(first2.b.size == 3) +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/mapKinds.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/mapKinds.kt new file mode 100644 index 00000000000..9e1b5adc7c5 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/mapKinds.kt @@ -0,0 +1,40 @@ +// WITH_RUNTIME +// FULL_JDK + +@file:JvmName("TestKt") +package test + +import kotlinx.android.parcel.* +import android.os.Parcel +import android.os.Parcelable +import java.util.* + +@MagicParcel +data class Test( + val a: Map, + val b: MutableMap, + val c: HashMap, + val d: LinkedHashMap, + val e: TreeMap +) : Parcelable + +fun box() = parcelTest { parcel -> + val first = Test( + a = mapOf("A" to "B"), + b = mutableMapOf("A" to "B"), + c = HashMap().apply { put("A", "B") }, + d = LinkedHashMap().apply { put("A", "B") }, + e = TreeMap().apply { put("A", "B") } + ) + + first.writeToParcel(parcel, 0) + + val bytes = parcel.marshall() + parcel.unmarshall(bytes, 0, bytes.size) + + val first2 = readFromParcel(parcel) + + assert(first == first2) + assert((first.c as HashMap<*, *>).size == 1) + assert((first2.e as TreeMap<*, *>).size == 1) +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/mapSimple.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/mapSimple.kt new file mode 100644 index 00000000000..a55a9c4bf79 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/mapSimple.kt @@ -0,0 +1,24 @@ +// WITH_RUNTIME + +@file:JvmName("TestKt") +package test + +import kotlinx.android.parcel.* +import android.os.Parcel +import android.os.Parcelable + +@MagicParcel +data class Test(val a: Map) : Parcelable + +fun box() = parcelTest { parcel -> + val first = Test(mapOf("A" to "B", "C" to "D")) + + first.writeToParcel(parcel, 0) + + val bytes = parcel.marshall() + parcel.unmarshall(bytes, 0, bytes.size) + + val first2 = readFromParcel(parcel) + + assert(first == first2) +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/maps.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/maps.kt new file mode 100644 index 00000000000..6642b3bd9d1 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/maps.kt @@ -0,0 +1,40 @@ +// WITH_RUNTIME + +@file:JvmName("TestKt") +package test + +import kotlinx.android.parcel.* +import android.os.Parcel +import android.os.Parcelable + +@MagicParcel +data class Test( + val a: Map, + val b: Map, + val c: Map, + val d: Map>, + val e: Map>, + val f: Map, + val g: Map>> +) : Parcelable + +fun box() = parcelTest { parcel -> + val first = Test( + a = mapOf("A" to "B", "C" to "D"), + b = mapOf("A" to "B", null to "D", "E" to "F"), + c = mapOf("A" to null, "C" to "D"), + d = mapOf("A" to mapOf(1 to "", 2 to "x")), + e = mapOf(1 to listOf("", ""), null to listOf()), + f = mapOf(true to false, false to true), + g = mapOf("A" to mapOf("B" to mapOf("C" to "D", "E" to "F"))) + ) + + first.writeToParcel(parcel, 0) + + val bytes = parcel.marshall() + parcel.unmarshall(bytes, 0, bytes.size) + + val first2 = readFromParcel(parcel) + + assert(first == first2) +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/nullableTypes.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/nullableTypes.kt new file mode 100644 index 00000000000..cedbb5af913 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/nullableTypes.kt @@ -0,0 +1,33 @@ +// WITH_RUNTIME + +@file:JvmName("TestKt") +package test + +import kotlinx.android.parcel.* +import android.os.Parcel +import android.os.Parcelable + +@MagicParcel +data class Test( + val str1: String, + val str2: String?, + val int1: Int, + val int2: Int? +) : Parcelable + +fun box() = parcelTest { parcel -> + val first = Test("John", "Smith", 20, 30) + val second = Test("A", null, 20, null) + + first.writeToParcel(parcel, 0) + second.writeToParcel(parcel, 0) + + val bytes = parcel.marshall() + parcel.unmarshall(bytes, 0, bytes.size) + + val first2 = readFromParcel(parcel) + val second2 = readFromParcel(parcel) + + assert(first == first2) + assert(second == second2) +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/nullableTypesSimple.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/nullableTypesSimple.kt new file mode 100644 index 00000000000..fdf3ed5f62c --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/nullableTypesSimple.kt @@ -0,0 +1,28 @@ +// WITH_RUNTIME + +@file:JvmName("TestKt") +package test + +import kotlinx.android.parcel.* +import android.os.Parcel +import android.os.Parcelable + +@MagicParcel +data class Test(val a: String?) : Parcelable + +fun box() = parcelTest { parcel -> + val first = Test("John") + val second = Test(null) + + first.writeToParcel(parcel, 0) + second.writeToParcel(parcel, 0) + + val bytes = parcel.marshall() + parcel.unmarshall(bytes, 0, bytes.size) + + val first2 = readFromParcel(parcel) + val second2 = readFromParcel(parcel) + + assert(first == first2) + assert(second == second2) +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/primitiveTypes.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/primitiveTypes.kt new file mode 100644 index 00000000000..5d2b20fa544 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/primitiveTypes.kt @@ -0,0 +1,38 @@ +// WITH_RUNTIME + +@file:JvmName("TestKt") +package test + +import kotlinx.android.parcel.* +import android.os.Parcel +import android.os.Parcelable + +@MagicParcel +data class PrimitiveTypes( + val boo: Boolean, + val c: Char, + val byt: Byte, + val s: Short, + val i: Int, + val f: Float, + val l: Long, + val d: Double +) : Parcelable + +fun box() = parcelTest { parcel -> + val first = PrimitiveTypes(true, '#', 3.toByte(), 10.toShort(), -300, -5.0f, Long.MAX_VALUE, 3.14) + val second = PrimitiveTypes(false, '\n', Byte.MIN_VALUE, Short.MIN_VALUE, Int.MIN_VALUE, Float.POSITIVE_INFINITY, + Long.MAX_VALUE, Double.NEGATIVE_INFINITY) + + first.writeToParcel(parcel, 0) + second.writeToParcel(parcel, 0) + + val bytes = parcel.marshall() + parcel.unmarshall(bytes, 0, bytes.size) + + val first2 = readFromParcel(parcel) + val second2 = readFromParcel(parcel) + + assert(first == first2) + assert(second == second2) +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/simple.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/simple.kt new file mode 100644 index 00000000000..95bd55c536f --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/simple.kt @@ -0,0 +1,22 @@ +// WITH_RUNTIME + +@file:JvmName("TestKt") +package test + +import kotlinx.android.parcel.* +import android.os.Parcel +import android.os.Parcelable + +@MagicParcel +data class User(val firstName: String, val secondName: String, val age: Int) : Parcelable + +fun box() = parcelTest { parcel -> + val user = User("John", "Smith", 20) + user.writeToParcel(parcel, 0) + + val bytes = parcel.marshall() + parcel.unmarshall(bytes, 0, bytes.size) + + val user2 = readFromParcel(parcel) + assert(user == user2) +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/sparseArrays.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/sparseArrays.kt new file mode 100644 index 00000000000..65a12780ce2 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/sparseArrays.kt @@ -0,0 +1,70 @@ +// WITH_RUNTIME + +@file:JvmName("TestKt") +package test + +import kotlinx.android.parcel.* +import android.os.Parcel +import android.os.Parcelable +import android.util.* + +@MagicParcel +data class Data(val a: String, val b: String) : Parcelable + +@MagicParcel +data class User(val a: SparseIntArray, val b: SparseLongArray, val c: SparseArray) : Parcelable + +fun box() = parcelTest { parcel -> + val user = User( + a = SparseIntArray().apply { put(1, 5); put(100, -1); put(1000, 0) }, + b = SparseLongArray().apply { put(3, 2); put(2, 3); put(10, 10) }, + c = SparseArray().apply { put(1, Data("A", "B")); put(10, Data("C", "D")); put(105, Data("E", "")) } + ) + + user.writeToParcel(parcel, 0) + + val bytes = parcel.marshall() + parcel.unmarshall(bytes, 0, bytes.size) + + val user2 = readFromParcel(parcel) + + assert(compareSparseIntArrays(user.a, user2.a)) + assert(compareSparseLongArrays(user.b, user2.b)) + assert(compareSparseArrays(user.c, user2.c)) +} + +private fun compareSparseIntArrays(first: SparseIntArray, second: SparseIntArray): Boolean { + if (first === second) return true + if (first.size() != second.size()) return false + + for (i in 0 until first.size()) { + if (first.keyAt(i) != second.keyAt(i)) return false + if (first.valueAt(i) != second.valueAt(i)) return false + } + + return true +} + +private fun compareSparseLongArrays(first: SparseLongArray, second: SparseLongArray): Boolean { + if (first === second) return true + if (first.size() != second.size()) return false + + for (i in 0 until first.size()) { + if (first.keyAt(i) != second.keyAt(i)) return false + if (first.valueAt(i) != second.valueAt(i)) return false + } + + return true +} + +private fun compareSparseArrays(first: SparseArray<*>, second: SparseArray<*>): Boolean { + if (first === second) return true + if (first.size() != second.size()) return false + + for (i in 0 until first.size()) { + if (first.keyAt(i) != second.keyAt(i)) return false + if (first.valueAt(i) != second.valueAt(i)) return false + } + + return true +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/sparseBooleanArray.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/sparseBooleanArray.kt new file mode 100644 index 00000000000..0c9c8ad7e92 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/box/sparseBooleanArray.kt @@ -0,0 +1,36 @@ +// WITH_RUNTIME + +@file:JvmName("TestKt") +package test + +import kotlinx.android.parcel.* +import android.os.Parcel +import android.os.Parcelable +import android.util.SparseBooleanArray + +@MagicParcel +data class User(val a: SparseBooleanArray) : Parcelable + +fun box() = parcelTest { parcel -> + val test = User(SparseBooleanArray().apply { put(1, false); put(5, true); put(1000, false) }) + test.writeToParcel(parcel, 0) + + val bytes = parcel.marshall() + parcel.unmarshall(bytes, 0, bytes.size) + + val test2 = readFromParcel(parcel) + + assert(compareSparseBooleanArrays(test.a, test2.a)) +} + +private fun compareSparseBooleanArrays(first: SparseBooleanArray, second: SparseBooleanArray): Boolean { + if (first === second) return true + if (first.size() != second.size()) return false + + for (i in 0 until first.size()) { + if (first.keyAt(i) != second.keyAt(i)) return false + if (first.valueAt(i) != second.valueAt(i)) return false + } + + return true +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/boxLib.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/boxLib.kt new file mode 100644 index 00000000000..b24f2513a9b --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/boxLib.kt @@ -0,0 +1,18 @@ +package test + +import android.os.Parcel + +fun parcelTest(block: (Parcel) -> Unit): String { + val parcel = Parcel.obtain() + try { + block(parcel) + return "OK" + } finally { + parcel.recycle() + } +} + +inline fun readFromParcel(parcel: Parcel): T { + val creator = T::class.java.getDeclaredField("CREATOR").get(null) + return creator::class.java.getDeclaredMethod("createFromParcel", Parcel::class.java).invoke(creator, parcel) as T +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/listInsideList.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/listInsideList.kt new file mode 100644 index 00000000000..3338f43dad1 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/listInsideList.kt @@ -0,0 +1,6 @@ +// CURIOUS_ABOUT writeToParcel + +import kotlinx.android.parcel.* + +@MagicParcel +class Test(val names: List>>) \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/listInsideList.txt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/listInsideList.txt new file mode 100644 index 00000000000..a446b1e3da1 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/listInsideList.txt @@ -0,0 +1,82 @@ +public static class Test$CREATOR : java/lang/Object, android/os/Parcelable$Creator { + public void () + + public final Test createFromParcel(android.os.Parcel p0) + + public final Test[] newArray(int p0) +} + +public final class Test : java/lang/Object { + public final static Test$CREATOR CREATOR + + private final java.util.List names + + static void () + + public void (java.util.List p0) + + public final int describeContents() + + public final java.util.List getNames() + + public final void writeToParcel(android.os.Parcel p0, int p1) { + LABEL (L0) + ALOAD (1) + LDC (parcel) + INVOKESTATIC (kotlin/jvm/internal/Intrinsics, checkParameterIsNotNull, (Ljava/lang/Object;Ljava/lang/String;)V) + ALOAD (1) + ALOAD (0) + GETFIELD (names, Ljava/util/List;) + DUP_X1 + INVOKEINTERFACE (java/util/Collection, size, ()I) + INVOKEVIRTUAL (android/os/Parcel, writeInt, (I)V) + INVOKEINTERFACE (java/util/Collection, iterator, ()Ljava/util/Iterator;) + LABEL (L1) + DUP + INVOKEINTERFACE (java/util/Iterator, hasNext, ()Z) + IFEQ (L2) + DUP + INVOKEINTERFACE (java/util/Iterator, next, ()Ljava/lang/Object;) + ALOAD (1) + SWAP + CHECKCAST + DUP_X1 + INVOKEINTERFACE (java/util/Collection, size, ()I) + INVOKEVIRTUAL (android/os/Parcel, writeInt, (I)V) + INVOKEINTERFACE (java/util/Collection, iterator, ()Ljava/util/Iterator;) + LABEL (L3) + DUP + INVOKEINTERFACE (java/util/Iterator, hasNext, ()Z) + IFEQ (L4) + DUP + INVOKEINTERFACE (java/util/Iterator, next, ()Ljava/lang/Object;) + ALOAD (1) + SWAP + CHECKCAST + DUP_X1 + INVOKEINTERFACE (java/util/Collection, size, ()I) + INVOKEVIRTUAL (android/os/Parcel, writeInt, (I)V) + INVOKEINTERFACE (java/util/Collection, iterator, ()Ljava/util/Iterator;) + LABEL (L5) + DUP + INVOKEINTERFACE (java/util/Iterator, hasNext, ()Z) + IFEQ (L6) + DUP + INVOKEINTERFACE (java/util/Iterator, next, ()Ljava/lang/Object;) + ALOAD (1) + SWAP + CHECKCAST + INVOKEVIRTUAL (android/os/Parcel, writeString, (Ljava/lang/String;)V) + GOTO (L5) + LABEL (L6) + POP + GOTO (L3) + LABEL (L4) + POP + GOTO (L1) + LABEL (L2) + POP + RETURN + LABEL (L7) + } +} diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/nullableNotNullSize.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/nullableNotNullSize.kt new file mode 100644 index 00000000000..46f9ccdd050 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/nullableNotNullSize.kt @@ -0,0 +1,10 @@ +// CURIOUS_ABOUT writeToParcel + +import android.util.Size +import kotlinx.android.parcel.* + +@MagicParcel +class TestNullable(val a: Size?) + +@MagicParcel +class TestNotNull(val a: Size) \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/nullableNotNullSize.txt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/nullableNotNullSize.txt new file mode 100644 index 00000000000..e8f77cd5a57 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/nullableNotNullSize.txt @@ -0,0 +1,80 @@ +public static class TestNotNull$CREATOR : java/lang/Object, android/os/Parcelable$Creator { + public void () + + public final TestNotNull createFromParcel(android.os.Parcel p0) + + public final TestNotNull[] newArray(int p0) +} + +public final class TestNotNull : java/lang/Object { + public final static TestNotNull$CREATOR CREATOR + + private final android.util.Size a + + static void () + + public void (android.util.Size p0) + + public final int describeContents() + + public final android.util.Size getA() + + public final void writeToParcel(android.os.Parcel p0, int p1) { + LABEL (L0) + ALOAD (1) + LDC (parcel) + INVOKESTATIC (kotlin/jvm/internal/Intrinsics, checkParameterIsNotNull, (Ljava/lang/Object;Ljava/lang/String;)V) + ALOAD (1) + ALOAD (0) + GETFIELD (a, Landroid/util/Size;) + INVOKEVIRTUAL (android/os/Parcel, writeSize, (Landroid/util/Size;)V) + RETURN + LABEL (L1) + } +} + +public static class TestNullable$CREATOR : java/lang/Object, android/os/Parcelable$Creator { + public void () + + public final TestNullable createFromParcel(android.os.Parcel p0) + + public final TestNullable[] newArray(int p0) +} + +public final class TestNullable : java/lang/Object { + public final static TestNullable$CREATOR CREATOR + + private final android.util.Size a + + static void () + + public void (android.util.Size p0) + + public final int describeContents() + + public final android.util.Size getA() + + public final void writeToParcel(android.os.Parcel p0, int p1) { + LABEL (L0) + ALOAD (1) + LDC (parcel) + INVOKESTATIC (kotlin/jvm/internal/Intrinsics, checkParameterIsNotNull, (Ljava/lang/Object;Ljava/lang/String;)V) + ALOAD (1) + ALOAD (0) + GETFIELD (a, Landroid/util/Size;) + DUP + IFNULL (L1) + ALOAD (1) + LDC (1) + INVOKEVIRTUAL (android/os/Parcel, writeInt, (I)V) + INVOKEVIRTUAL (android/os/Parcel, writeSize, (Landroid/util/Size;)V) + GOTO (L2) + LABEL (L1) + POP + LDC (0) + INVOKEVIRTUAL (android/os/Parcel, writeInt, (I)V) + LABEL (L2) + RETURN + LABEL (L3) + } +} \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/serializable.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/serializable.kt new file mode 100644 index 00000000000..c274c7027d7 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/serializable.kt @@ -0,0 +1,10 @@ +// CURIOUS_ABOUT writeToParcel, createFromParcel, + +import kotlinx.android.parcel.* +import android.os.Parcelable +import java.io.Serializable + +class SerializableSimple(val a: String, val b: String) : Serializable + +@MagicParcel +class User(val notNull: SerializableSimple, val nullable: SerializableSimple) : Parcelable \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/serializable.txt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/serializable.txt new file mode 100644 index 00000000000..2050cce1d64 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/serializable.txt @@ -0,0 +1,74 @@ +public final class SerializableSimple : java/lang/Object, java/io/Serializable { + private final java.lang.String a + + private final java.lang.String b + + public void (java.lang.String p0, java.lang.String p1) + + public final java.lang.String getA() + + public final java.lang.String getB() +} + +public static class User$CREATOR : java/lang/Object, android/os/Parcelable$Creator { + public void () + + public final User createFromParcel(android.os.Parcel p0) { + LABEL (L0) + ALOAD (1) + LDC (in) + INVOKESTATIC (kotlin/jvm/internal/Intrinsics, checkParameterIsNotNull, (Ljava/lang/Object;Ljava/lang/String;)V) + NEW + DUP + ALOAD (1) + INVOKEVIRTUAL (android/os/Parcel, readSerializable, ()LSerializableSimple;) + ALOAD (1) + INVOKEVIRTUAL (android/os/Parcel, readSerializable, ()LSerializableSimple;) + INVOKESPECIAL (User, , (LSerializableSimple;LSerializableSimple;)V) + ARETURN + LABEL (L1) + } + + public final User[] newArray(int p0) +} + +public final class User : java/lang/Object, android/os/Parcelable { + public final static User$CREATOR CREATOR + + private final SerializableSimple notNull + + private final SerializableSimple nullable + + static void () { + NEW + DUP + INVOKESPECIAL (User$CREATOR, , ()V) + PUTSTATIC (CREATOR, LUser$CREATOR;) + RETURN + } + + public void (SerializableSimple p0, SerializableSimple p1) + + public final int describeContents() + + public final SerializableSimple getNotNull() + + public final SerializableSimple getNullable() + + public final void writeToParcel(android.os.Parcel p0, int p1) { + LABEL (L0) + ALOAD (1) + LDC (parcel) + INVOKESTATIC (kotlin/jvm/internal/Intrinsics, checkParameterIsNotNull, (Ljava/lang/Object;Ljava/lang/String;)V) + ALOAD (1) + ALOAD (0) + GETFIELD (notNull, LSerializableSimple;) + INVOKEVIRTUAL (android/os/Parcel, writeSerializable, (LSerializableSimple;)V) + ALOAD (1) + ALOAD (0) + GETFIELD (nullable, LSerializableSimple;) + INVOKEVIRTUAL (android/os/Parcel, writeSerializable, (LSerializableSimple;)V) + RETURN + LABEL (L1) + } +} diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simple.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simple.kt new file mode 100644 index 00000000000..d83471dcab9 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simple.kt @@ -0,0 +1,7 @@ +// CURIOUS_ABOUT writeToParcel, createFromParcel, + +import kotlinx.android.parcel.* +import android.os.Parcelable + +@MagicParcel +class User(val firstName: String, val lastName: String, val age: Int, val isProUser: Boolean) : Parcelable \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simple.txt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simple.txt new file mode 100644 index 00000000000..e448fe0e11d --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simple.txt @@ -0,0 +1,82 @@ +public static class User$CREATOR : java/lang/Object, android/os/Parcelable$Creator { + public void () + + public final User createFromParcel(android.os.Parcel p0) { + LABEL (L0) + ALOAD (1) + LDC (in) + INVOKESTATIC (kotlin/jvm/internal/Intrinsics, checkParameterIsNotNull, (Ljava/lang/Object;Ljava/lang/String;)V) + NEW + DUP + ALOAD (1) + INVOKEVIRTUAL (android/os/Parcel, readString, ()Ljava/lang/String;) + ALOAD (1) + INVOKEVIRTUAL (android/os/Parcel, readString, ()Ljava/lang/String;) + ALOAD (1) + INVOKEVIRTUAL (android/os/Parcel, readInt, ()I) + ALOAD (1) + INVOKEVIRTUAL (android/os/Parcel, readInt, ()I) + INVOKESPECIAL (User, , (Ljava/lang/String;Ljava/lang/String;IZ)V) + ARETURN + LABEL (L1) + } + + public final User[] newArray(int p0) +} + +public final class User : java/lang/Object, android/os/Parcelable { + public final static User$CREATOR CREATOR + + private final int age + + private final java.lang.String firstName + + private final boolean isProUser + + private final java.lang.String lastName + + static void () { + NEW + DUP + INVOKESPECIAL (User$CREATOR, , ()V) + PUTSTATIC (CREATOR, LUser$CREATOR;) + RETURN + } + + public void (java.lang.String p0, java.lang.String p1, int p2, boolean p3) + + public final int describeContents() + + public final int getAge() + + public final java.lang.String getFirstName() + + public final java.lang.String getLastName() + + public final boolean isProUser() + + public final void writeToParcel(android.os.Parcel p0, int p1) { + LABEL (L0) + ALOAD (1) + LDC (parcel) + INVOKESTATIC (kotlin/jvm/internal/Intrinsics, checkParameterIsNotNull, (Ljava/lang/Object;Ljava/lang/String;)V) + ALOAD (1) + ALOAD (0) + GETFIELD (firstName, Ljava/lang/String;) + INVOKEVIRTUAL (android/os/Parcel, writeString, (Ljava/lang/String;)V) + ALOAD (1) + ALOAD (0) + GETFIELD (lastName, Ljava/lang/String;) + INVOKEVIRTUAL (android/os/Parcel, writeString, (Ljava/lang/String;)V) + ALOAD (1) + ALOAD (0) + GETFIELD (age, I) + INVOKEVIRTUAL (android/os/Parcel, writeInt, (I)V) + ALOAD (1) + ALOAD (0) + GETFIELD (isProUser, Z) + INVOKEVIRTUAL (android/os/Parcel, writeInt, (I)V) + RETURN + LABEL (L1) + } +} diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simpleList.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simpleList.kt new file mode 100644 index 00000000000..049dfa6ae73 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simpleList.kt @@ -0,0 +1,6 @@ +// CURIOUS_ABOUT writeToParcel + +import kotlinx.android.parcel.* + +@MagicParcel +class Test(val names: List) \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simpleList.txt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simpleList.txt new file mode 100644 index 00000000000..9cb608fc39d --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simpleList.txt @@ -0,0 +1,50 @@ +public static class Test$CREATOR : java/lang/Object, android/os/Parcelable$Creator { + public void () + + public final Test createFromParcel(android.os.Parcel p0) + + public final Test[] newArray(int p0) +} + +public final class Test : java/lang/Object { + public final static Test$CREATOR CREATOR + + private final java.util.List names + + static void () + + public void (java.util.List p0) + + public final int describeContents() + + public final java.util.List getNames() + + public final void writeToParcel(android.os.Parcel p0, int p1) { + LABEL (L0) + ALOAD (1) + LDC (parcel) + INVOKESTATIC (kotlin/jvm/internal/Intrinsics, checkParameterIsNotNull, (Ljava/lang/Object;Ljava/lang/String;)V) + ALOAD (1) + ALOAD (0) + GETFIELD (names, Ljava/util/List;) + DUP_X1 + INVOKEINTERFACE (java/util/Collection, size, ()I) + INVOKEVIRTUAL (android/os/Parcel, writeInt, (I)V) + INVOKEINTERFACE (java/util/Collection, iterator, ()Ljava/util/Iterator;) + LABEL (L1) + DUP + INVOKEINTERFACE (java/util/Iterator, hasNext, ()Z) + IFEQ (L2) + DUP + INVOKEINTERFACE (java/util/Iterator, next, ()Ljava/lang/Object;) + ALOAD (1) + SWAP + CHECKCAST + INVOKEVIRTUAL (android/os/Parcel, writeString, (Ljava/lang/String;)V) + GOTO (L1) + LABEL (L2) + POP + RETURN + LABEL (L3) + } +} diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/size.kt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/size.kt new file mode 100644 index 00000000000..3d00a97a196 --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/size.kt @@ -0,0 +1,12 @@ +// CURIOUS_ABOUT writeToParcel, createFromParcel + +import kotlinx.android.parcel.* +import android.os.Parcelable +import android.util.Size +import android.util.SizeF + +@MagicParcel +data class Test(val size: Size, val nullable: Size?) : Parcelable + +@MagicParcel +data class TestF(val size: SizeF, val nullable: SizeF?) : Parcelable \ No newline at end of file diff --git a/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/size.txt b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/size.txt new file mode 100644 index 00000000000..9db41d6f96e --- /dev/null +++ b/plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/size.txt @@ -0,0 +1,179 @@ +public static class Test$CREATOR : java/lang/Object, android/os/Parcelable$Creator { + public void () + + public final Test createFromParcel(android.os.Parcel p0) { + LABEL (L0) + ALOAD (1) + LDC (in) + INVOKESTATIC (kotlin/jvm/internal/Intrinsics, checkParameterIsNotNull, (Ljava/lang/Object;Ljava/lang/String;)V) + LABEL (L1) + NEW + DUP + ALOAD (1) + INVOKEVIRTUAL (android/os/Parcel, readSize, ()Landroid/util/Size;) + ALOAD (1) + INVOKEVIRTUAL (android/os/Parcel, readInt, ()I) + IFEQ (L2) + ALOAD (1) + INVOKEVIRTUAL (android/os/Parcel, readSize, ()Landroid/util/Size;) + GOTO (L3) + LABEL (L2) + ACONST_NULL + LABEL (L3) + INVOKESPECIAL (Test, , (Landroid/util/Size;Landroid/util/Size;)V) + ARETURN + LABEL (L4) + } + + public final Test[] newArray(int p0) +} + +public final class Test : java/lang/Object, android/os/Parcelable { + public final static Test$CREATOR CREATOR + + private final android.util.Size nullable + + private final android.util.Size size + + static void () + + public void (android.util.Size p0, android.util.Size p1) + + public final android.util.Size component1() + + public final android.util.Size component2() + + public final Test copy(android.util.Size p0, android.util.Size p1) + + public static Test copy$default(Test p0, android.util.Size p1, android.util.Size p2, int p3, java.lang.Object p4) + + public final int describeContents() + + public boolean equals(java.lang.Object p0) + + public final android.util.Size getNullable() + + public final android.util.Size getSize() + + public int hashCode() + + public java.lang.String toString() + + public final void writeToParcel(android.os.Parcel p0, int p1) { + LABEL (L0) + ALOAD (1) + LDC (parcel) + INVOKESTATIC (kotlin/jvm/internal/Intrinsics, checkParameterIsNotNull, (Ljava/lang/Object;Ljava/lang/String;)V) + ALOAD (1) + ALOAD (0) + GETFIELD (size, Landroid/util/Size;) + INVOKEVIRTUAL (android/os/Parcel, writeSize, (Landroid/util/Size;)V) + ALOAD (1) + ALOAD (0) + GETFIELD (nullable, Landroid/util/Size;) + DUP + IFNULL (L1) + ALOAD (1) + LDC (1) + INVOKEVIRTUAL (android/os/Parcel, writeInt, (I)V) + INVOKEVIRTUAL (android/os/Parcel, writeSize, (Landroid/util/Size;)V) + GOTO (L2) + LABEL (L1) + POP + LDC (0) + INVOKEVIRTUAL (android/os/Parcel, writeInt, (I)V) + LABEL (L2) + RETURN + LABEL (L3) + } +} + +public static class TestF$CREATOR : java/lang/Object, android/os/Parcelable$Creator { + public void () + + public final TestF createFromParcel(android.os.Parcel p0) { + LABEL (L0) + ALOAD (1) + LDC (in) + INVOKESTATIC (kotlin/jvm/internal/Intrinsics, checkParameterIsNotNull, (Ljava/lang/Object;Ljava/lang/String;)V) + LABEL (L1) + NEW + DUP + ALOAD (1) + INVOKEVIRTUAL (android/os/Parcel, readSizeF, ()Landroid/util/SizeF;) + ALOAD (1) + INVOKEVIRTUAL (android/os/Parcel, readInt, ()I) + IFEQ (L2) + ALOAD (1) + INVOKEVIRTUAL (android/os/Parcel, readSizeF, ()Landroid/util/SizeF;) + GOTO (L3) + LABEL (L2) + ACONST_NULL + LABEL (L3) + INVOKESPECIAL (TestF, , (Landroid/util/SizeF;Landroid/util/SizeF;)V) + ARETURN + LABEL (L4) + } + + public final TestF[] newArray(int p0) +} + +public final class TestF : java/lang/Object, android/os/Parcelable { + public final static TestF$CREATOR CREATOR + + private final android.util.SizeF nullable + + private final android.util.SizeF size + + static void () + + public void (android.util.SizeF p0, android.util.SizeF p1) + + public final android.util.SizeF component1() + + public final android.util.SizeF component2() + + public final TestF copy(android.util.SizeF p0, android.util.SizeF p1) + + public static TestF copy$default(TestF p0, android.util.SizeF p1, android.util.SizeF p2, int p3, java.lang.Object p4) + + public final int describeContents() + + public boolean equals(java.lang.Object p0) + + public final android.util.SizeF getNullable() + + public final android.util.SizeF getSize() + + public int hashCode() + + public java.lang.String toString() + + public final void writeToParcel(android.os.Parcel p0, int p1) { + LABEL (L0) + ALOAD (1) + LDC (parcel) + INVOKESTATIC (kotlin/jvm/internal/Intrinsics, checkParameterIsNotNull, (Ljava/lang/Object;Ljava/lang/String;)V) + ALOAD (1) + ALOAD (0) + GETFIELD (size, Landroid/util/SizeF;) + INVOKEVIRTUAL (android/os/Parcel, writeSizeF, (Landroid/util/SizeF;)V) + ALOAD (1) + ALOAD (0) + GETFIELD (nullable, Landroid/util/SizeF;) + DUP + IFNULL (L1) + ALOAD (1) + LDC (1) + INVOKEVIRTUAL (android/os/Parcel, writeInt, (I)V) + INVOKEVIRTUAL (android/os/Parcel, writeSizeF, (Landroid/util/SizeF;)V) + GOTO (L2) + LABEL (L1) + POP + LDC (0) + INVOKEVIRTUAL (android/os/Parcel, writeInt, (I)V) + LABEL (L2) + RETURN + LABEL (L3) + } +} diff --git a/plugins/android-extensions/android-extensions-runtime/src/kotlinx/android/parcel/MagicParcel.kt b/plugins/android-extensions/android-extensions-runtime/src/kotlinx/android/parcel/MagicParcel.kt new file mode 100644 index 00000000000..d3354050839 --- /dev/null +++ b/plugins/android-extensions/android-extensions-runtime/src/kotlinx/android/parcel/MagicParcel.kt @@ -0,0 +1,28 @@ +/* + * 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 + +/** + * Instructs the Kotlin compiler to generate `writeToParcel()`, `describeContents()` [android.os.Parcelable] methods, + * as well as a `CREATOR` factory class automatically. + * + * The annotation is applicable only to classes that implements [android.os.Parcelable] (directly or indirectly). + * Note that only the primary constructor properties will be serialized. + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.BINARY) +annotation class MagicParcel \ No newline at end of file diff --git a/plugins/plugins-tests/plugins-tests.iml b/plugins/plugins-tests/plugins-tests.iml index bb54d63ef5b..30adc2f1856 100755 --- a/plugins/plugins-tests/plugins-tests.iml +++ b/plugins/plugins-tests/plugins-tests.iml @@ -16,6 +16,8 @@ + + diff --git a/plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/AbstractAsmLikeInstructionListingTest.kt b/plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/AbstractAsmLikeInstructionListingTest.kt new file mode 100644 index 00000000000..c038943f14e --- /dev/null +++ b/plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/AbstractAsmLikeInstructionListingTest.kt @@ -0,0 +1,173 @@ +/* + * 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 org.jetbrains.kotlin.android.parcel + +import org.jetbrains.kotlin.codegen.CodegenTestCase +import org.jetbrains.kotlin.codegen.getClassFiles +import org.jetbrains.kotlin.test.KotlinTestUtils +import org.jetbrains.org.objectweb.asm.ClassReader +import org.jetbrains.org.objectweb.asm.Label +import org.jetbrains.org.objectweb.asm.Opcodes.* +import org.jetbrains.org.objectweb.asm.Type +import org.jetbrains.org.objectweb.asm.tree.* +import org.jetbrains.org.objectweb.asm.util.Printer +import java.io.File + +private val LINE_SEPARATOR = System.getProperty("line.separator") + +abstract class AbstractAsmLikeInstructionListingTest : CodegenTestCase() { + private companion object { + val CURIOUS_ABOUT_DIRECTIVE = "// CURIOUS_ABOUT " + } + + override fun doMultiFileTest(wholeFile: File, files: List, javaFilesDir: File?) { + val txtFile = File(wholeFile.parentFile, wholeFile.nameWithoutExtension + ".txt") + compile(files, javaFilesDir) + + val classes = classFileFactory + .getClassFiles() + .sortedBy { it.relativePath } + .map { file -> ClassNode().also { ClassReader(file.asByteArray()).accept(it, ClassReader.EXPAND_FRAMES) } } + + val printBytecodeForTheseMethods = wholeFile.readLines() + .filter { it.startsWith(CURIOUS_ABOUT_DIRECTIVE) } + .map { it.substring(CURIOUS_ABOUT_DIRECTIVE.length) } + .flatMap { it.split(',').map { it.trim() } } + + KotlinTestUtils.assertEqualsToFile(txtFile, classes.joinToString(LINE_SEPARATOR.repeat(2)) { + renderClassNode(it, printBytecodeForTheseMethods) + }) + } + + private fun renderClassNode(clazz: ClassNode, printBytecodeForTheseMethods: List): String { + val fields = (clazz.fields ?: emptyList()).sortedBy { it.name } + val methods = (clazz.methods ?: emptyList()).sortedBy { it.name } + + val superTypes = (listOf(clazz.superName) + clazz.interfaces).filterNotNull() + + return buildString { + renderVisibilityModifiers(clazz.access) + renderModalityModifiers(clazz.access) + append(if ((clazz.access and ACC_INTERFACE) != 0) "interface " else "class ") + append(clazz.name) + + if (superTypes.isNotEmpty()) { + append(" : " + superTypes.joinToString()) + } + + appendln(" {") + + fields.joinTo(this, LINE_SEPARATOR.repeat(2)) { renderField(it).withMargin() } + + if (fields.isNotEmpty()) { + appendln().appendln() + } + + methods.joinTo(this, LINE_SEPARATOR.repeat(2)) { + val printBytecode = printBytecodeForTheseMethods.contains(it.name) + renderMethod(it, printBytecode).withMargin() + } + + appendln().append("}") + } + } + + private fun renderField(field: FieldNode) = buildString { + renderVisibilityModifiers(field.access) + renderModalityModifiers(field.access) + append(Type.getType(field.desc).className).append(' ') + append(field.name) + } + + private fun renderMethod(method: MethodNode, printBytecode: Boolean) = buildString { + renderVisibilityModifiers(method.access) + renderModalityModifiers(method.access) + val (returnType, parameterTypes) = with(Type.getMethodType(method.desc)) { returnType to argumentTypes } + append(returnType.className).append(' ') + append(method.name) + parameterTypes.mapIndexed { index, type -> "${type.className} p$index" }.joinTo(this, prefix = "(", postfix = ")") + + if (printBytecode && (method.access and ACC_ABSTRACT) == 0) { + appendln(" {") + append(renderBytecodeInstructions(method.instructions).trimEnd().withMargin()) + appendln().append("}") + } + } + + private fun renderBytecodeInstructions(instructions: InsnList) = buildString { + val labelMappings = LabelMappings() + + var currentInsn = instructions.first + while (currentInsn != null) { + renderInstruction(currentInsn, labelMappings) + currentInsn = currentInsn.next + } + } + + private fun StringBuilder.renderInstruction(node: AbstractInsnNode, labelMappings: LabelMappings) { + if (node is LabelNode) { + appendln("LABEL (L" + labelMappings[node.label] + ")") + return + } + + if (node is LineNumberNode) { + appendln("LINENUMBER (" + node.line + ")") + return + } + + if (node is FrameNode) return + + append(" ").append(Printer.OPCODES[node.opcode] ?: error("Invalid opcode ${node.opcode}")) + + when (node) { + is FieldInsnNode -> append(" (" + node.name + ", " + node.desc + ")") + is JumpInsnNode -> append(" (L" + labelMappings[node.label.label] + ")") + is IntInsnNode -> append(" (" + node.operand + ")") + is MethodInsnNode -> append(" (" + node.owner + ", "+ node.name + ", " + node.desc + ")") + is VarInsnNode -> append(" (" + node.`var` + ")") + is LdcInsnNode -> append(" (" + node.cst + ")") + } + + appendln() + } + + private fun String.withMargin(margin: String = " "): String { + return lineSequence().map { margin + it }.joinToString(LINE_SEPARATOR) + } + + private fun StringBuilder.renderVisibilityModifiers(access: Int) { + if ((access and ACC_PUBLIC) != 0) append("public ") + if ((access and ACC_PRIVATE) != 0) append("private ") + if ((access and ACC_PROTECTED) != 0) append("protected ") + } + + private fun StringBuilder.renderModalityModifiers(access: Int) { + if ((access and ACC_FINAL) != 0) append("final ") + if ((access and ACC_ABSTRACT) != 0) append("abstract ") + if ((access and ACC_STATIC) != 0) append("static ") + } + + private class LabelMappings { + private var mappings = hashMapOf() + private var currentIndex = 0 + + operator fun get(label: Label): Int { + val hashCode = System.identityHashCode(label) + return mappings.getOrPut(hashCode) { currentIndex++ } + } + } +} \ No newline at end of file diff --git a/plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/AbstractParcelBoxTest.kt b/plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/AbstractParcelBoxTest.kt new file mode 100644 index 00000000000..4b18ac4945f --- /dev/null +++ b/plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/AbstractParcelBoxTest.kt @@ -0,0 +1,71 @@ +/* + * 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 org.jetbrains.kotlin.android.parcel + +import org.jetbrains.kotlin.android.synthetic.AndroidComponentRegistrar +import org.jetbrains.kotlin.android.synthetic.test.addAndroidExtensionsRuntimeLibrary +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot +import org.jetbrains.kotlin.codegen.CodegenTestCase +import org.jetbrains.kotlin.codegen.getClassFiles +import org.jetbrains.org.objectweb.asm.ClassReader +import org.jetbrains.org.objectweb.asm.tree.ClassNode +import java.io.File +import java.net.URLClassLoader +import java.nio.ByteBuffer +import java.security.ProtectionDomain + + +abstract class AbstractParcelBoxTest : CodegenTestCase() { + protected companion object { + val BASE_DIR = "plugins/android-extensions/android-extensions-compiler/testData/parcel/box" + val LIBRARY_KT = File(File(BASE_DIR).parentFile, "boxLib.kt") + } + + override fun doTest(filePath: String) { + super.doTest(File(BASE_DIR, filePath + ".kt").absolutePath) + } + + open protected fun getClassLoaderWithGeneratedFiles(): ClassLoader { + return object : URLClassLoader(arrayOf(), this::class.java.classLoader) { + init { + for (classFile in classFileFactory.getClassFiles()) { + val bytes = classFile.asByteArray() + val className = ClassNode().also { ClassReader(bytes).accept(it, ClassReader.EXPAND_FRAMES) }.name + defineClass(className.replace('/', '.'), ByteBuffer.wrap(bytes), null as ProtectionDomain?) + } + } + } + } + + override fun doMultiFileTest(wholeFile: File, files: List, javaFilesDir: File?) { + compile(files + TestFile(LIBRARY_KT.name, LIBRARY_KT.readText()), javaFilesDir) + + val testClass = Class.forName("test.TestKt", false, getClassLoaderWithGeneratedFiles()) + try { + testClass.getDeclaredMethod("box").invoke(testClass) + } catch (e: Throwable) { + throw AssertionError(classFileFactory.createText(), e) + } + } + + override fun setupEnvironment(environment: KotlinCoreEnvironment) { + AndroidComponentRegistrar.registerParcelExtensions(environment.project) + addAndroidExtensionsRuntimeLibrary(environment) + environment.updateClasspath(listOf(JvmClasspathRoot(File("ideaSDK/plugins/android/lib/layoutlib.jar")))) + } +} \ No newline at end of file diff --git a/plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/AbstractParcelBytecodeListingTest.kt b/plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/AbstractParcelBytecodeListingTest.kt new file mode 100644 index 00000000000..b48e5b9ca83 --- /dev/null +++ b/plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/AbstractParcelBytecodeListingTest.kt @@ -0,0 +1,32 @@ +/* + * 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 org.jetbrains.kotlin.android.parcel + +import org.jetbrains.kotlin.android.synthetic.AndroidComponentRegistrar +import org.jetbrains.kotlin.android.synthetic.test.addAndroidExtensionsRuntimeLibrary +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot +import org.jetbrains.kotlin.codegen.extensions.ExpressionCodegenExtension +import java.io.File + +abstract class AbstractParcelBytecodeListingTest : AbstractAsmLikeInstructionListingTest() { + override fun setupEnvironment(environment: KotlinCoreEnvironment) { + AndroidComponentRegistrar.registerParcelExtensions(environment.project) + addAndroidExtensionsRuntimeLibrary(environment) + environment.updateClasspath(listOf(JvmClasspathRoot(File("ideaSDK/plugins/android/lib/layoutlib.jar")))) + } +} \ No newline at end of file diff --git a/plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/ParcelBoxTest.kt b/plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/ParcelBoxTest.kt new file mode 100644 index 00000000000..e6407693834 --- /dev/null +++ b/plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/ParcelBoxTest.kt @@ -0,0 +1,45 @@ +/* + * 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 org.jetbrains.kotlin.android.parcel + +import android.os.Bundle +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +// This class is not generated because it uses the custom test runner +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class ParcelBoxTest : AbstractParcelBoxTest() { + @Test fun simple() = doTest("simple") + @Test fun primitiveTypes() = doTest("primitiveTypes") + @Test fun boxedTypes() = doTest("boxedTypes") + @Test fun nullableTypesSimple() = doTest("nullableTypesSimple") + @Test fun nullableTypes() = doTest("nullableTypes") + @Test fun listSimple() = doTest("listSimple") + @Test fun lists() = doTest("lists") + @Test fun listKinds() = doTest("listKinds") + @Test fun arraySimple() = doTest("arraySimple") + @Test fun arrays() = doTest("arrays") + @Test fun mapSimple() = doTest("mapSimple") + @Test fun maps() = doTest("maps") + @Test fun mapKinds() = doTest("mapKinds") + @Test fun sparseBooleanArray() = doTest("sparseBooleanArray") + @Test fun bundle() = doTest("bundle") + @Test fun sparseArrays() = doTest("sparseArrays") +} \ No newline at end of file diff --git a/plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/ParcelBytecodeListingTestGenerated.java b/plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/ParcelBytecodeListingTestGenerated.java new file mode 100644 index 00000000000..40dbcedc090 --- /dev/null +++ b/plugins/plugins-tests/tests/org/jetbrains/kotlin/android/parcel/ParcelBytecodeListingTestGenerated.java @@ -0,0 +1,74 @@ +/* + * 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 org.jetbrains.kotlin.android.parcel; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.JUnit3RunnerWithInners; +import org.jetbrains.kotlin.test.KotlinTestUtils; +import org.jetbrains.kotlin.test.TargetBackend; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen") +@TestDataPath("$PROJECT_ROOT") +@RunWith(JUnit3RunnerWithInners.class) +public class ParcelBytecodeListingTestGenerated extends AbstractParcelBytecodeListingTest { + public void testAllFilesPresentInCodegen() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.ANY, true); + } + + @TestMetadata("listInsideList.kt") + public void testListInsideList() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/listInsideList.kt"); + doTest(fileName); + } + + @TestMetadata("nullableNotNullSize.kt") + public void testNullableNotNullSize() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/nullableNotNullSize.kt"); + doTest(fileName); + } + + @TestMetadata("serializable.kt") + public void testSerializable() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/serializable.kt"); + doTest(fileName); + } + + @TestMetadata("simple.kt") + public void testSimple() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simple.kt"); + doTest(fileName); + } + + @TestMetadata("simpleList.kt") + public void testSimpleList() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/simpleList.kt"); + doTest(fileName); + } + + @TestMetadata("size.kt") + public void testSize() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("plugins/android-extensions/android-extensions-compiler/testData/parcel/codegen/size.kt"); + doTest(fileName); + } +}