From 715cadd34b7b678e12d0b76f58bd775dc0062d1b Mon Sep 17 00:00:00 2001 From: Eli Hart Date: Sun, 22 Oct 2017 18:05:47 -0700 Subject: [PATCH] Improve transformation of java types to kotlin for extensions (#331) --- .../ModelGroupWithAnnotation.java | 23 +++ .../ModelWithConstructors.java | 47 ++++++ .../airbnb/epoxy/ModelGroupIntegrationTest.kt | 18 +++ .../java/com/airbnb/epoxy/PoetExtensions.kt | 145 ++++++++++++++++-- 4 files changed, 220 insertions(+), 13 deletions(-) create mode 100644 epoxy-integrationtest/src/main/java/com/airbnb/epoxy/integrationtest/ModelGroupWithAnnotation.java create mode 100644 epoxy-integrationtest/src/test/java/com/airbnb/epoxy/ModelGroupIntegrationTest.kt diff --git a/epoxy-integrationtest/src/main/java/com/airbnb/epoxy/integrationtest/ModelGroupWithAnnotation.java b/epoxy-integrationtest/src/main/java/com/airbnb/epoxy/integrationtest/ModelGroupWithAnnotation.java new file mode 100644 index 0000000000..e2d237120c --- /dev/null +++ b/epoxy-integrationtest/src/main/java/com/airbnb/epoxy/integrationtest/ModelGroupWithAnnotation.java @@ -0,0 +1,23 @@ +package com.airbnb.epoxy.integrationtest; + +import com.airbnb.epoxy.EpoxyAttribute; +import com.airbnb.epoxy.EpoxyModel; +import com.airbnb.epoxy.EpoxyModelClass; +import com.airbnb.epoxy.EpoxyModelGroup; + +import java.util.List; + +@EpoxyModelClass +public abstract class ModelGroupWithAnnotation extends EpoxyModelGroup { + @EpoxyAttribute int backgroundColor; + + public ModelGroupWithAnnotation(List> models) { + super(R.layout.model_with_click_listener, models); + } + + @Override + public void bind(Holder holder) { + super.bind(holder); + holder.getRootView().setBackgroundColor(backgroundColor); + } +} diff --git a/epoxy-integrationtest/src/main/java/com/airbnb/epoxy/integrationtest/ModelWithConstructors.java b/epoxy-integrationtest/src/main/java/com/airbnb/epoxy/integrationtest/ModelWithConstructors.java index e48e234476..32e242a4bd 100644 --- a/epoxy-integrationtest/src/main/java/com/airbnb/epoxy/integrationtest/ModelWithConstructors.java +++ b/epoxy-integrationtest/src/main/java/com/airbnb/epoxy/integrationtest/ModelWithConstructors.java @@ -6,6 +6,11 @@ import com.airbnb.epoxy.EpoxyModel; import com.airbnb.epoxy.EpoxyModelClass; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + @EpoxyModelClass public abstract class ModelWithConstructors extends EpoxyModel { @EpoxyAttribute public int value; @@ -23,6 +28,48 @@ public ModelWithConstructors(long id) { super(id); } + // Tests that kotlin extension functions + public ModelWithConstructors( + Collection collection, + Iterable iterable, + List list, + List upperBoundList, +// List lowerBoundList,!!! Doesn't work, we would need to transform this +// type to +// a MutableList + List wildCardList, + Map map, + Set set, + Boolean boxedBoolean, + Byte boxedByte, + Short boxedShort, + Integer boxedInteger, + Long boxedLong, + Character boxedCharacter, + Float boxedFloat, + Double boxedDouble, + String[] stringArray, + byte[] byteArray, + short[] shortArray, + char[] charArray, + int[] intArray, + float[] floatArray, + double[] doubleArray, + long[] longArray, + boolean[] booleanArray, + Byte[] boxedByteArray, + Short[] boxedShortArray, + Character[] boxedCharArray, + Integer[] boxedIntArray, + Float[] boxedFloatArray, + Double[] boxedDoubleArray, + Long[] boxedLongArray, + Boolean[] boxedBooleanArray, + Object objectParam + ) { + + } + @Override protected int getDefaultLayout() { return R.layout.model_with_click_listener; diff --git a/epoxy-integrationtest/src/test/java/com/airbnb/epoxy/ModelGroupIntegrationTest.kt b/epoxy-integrationtest/src/test/java/com/airbnb/epoxy/ModelGroupIntegrationTest.kt new file mode 100644 index 0000000000..92a9d8d432 --- /dev/null +++ b/epoxy-integrationtest/src/test/java/com/airbnb/epoxy/ModelGroupIntegrationTest.kt @@ -0,0 +1,18 @@ +package com.airbnb.epoxy + +import com.airbnb.epoxy.integrationtest.* +import org.junit.* +import org.junit.runner.* +import org.robolectric.* +import org.robolectric.annotation.* + +@RunWith(RobolectricTestRunner::class) +@Config(constants = BuildConfig::class, sdk = intArrayOf(21)) +class ModelGroupIntegrationTest { + + @Test + fun modelGroupSubclassIsGenerated() { + // Just checking that the generated class exists and compiles + ModelGroupWithAnnotation_(listOf(Model_())) + } +} diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/PoetExtensions.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/PoetExtensions.kt index b8e0085437..fec3e09278 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/PoetExtensions.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/PoetExtensions.kt @@ -1,5 +1,6 @@ package com.airbnb.epoxy +import com.squareup.javapoet.TypeName import com.squareup.kotlinpoet.* import javax.lang.model.element.* @@ -25,9 +26,68 @@ typealias KotlinParameterSpec = com.squareup.kotlinpoet.ParameterSpec typealias KotlinAnnotationSpec = com.squareup.kotlinpoet.AnnotationSpec typealias KotlinTypeSpec = com.squareup.kotlinpoet.TypeSpec -fun JavaClassName.toKPoet() = KotlinClassName( - packageName(), - simpleName()) +private val javaUtilPkg = "java.util" +private val javaLangPkg = "java.lang" +private val kotlinCollectionsPkg = "kotlin.collections" +private val kotlinPkg = "kotlin" +fun JavaClassName.toKPoet(): KotlinClassName { + + val simpleNames = getSimpleNamesInKotlin() + val packageName = getPackageNameInKotlin() + + return KotlinClassName( + packageName, + simpleNames.first(), + *simpleNames.drop(1).toTypedArray()) +} + +/** Some classes, like List or Byte have the same class name but a different package for their kotlin equivalent. */ +private fun JavaClassName.getPackageNameInKotlin(): String { + if (packageName() in listOf(javaUtilPkg, javaLangPkg) && simpleNames().size == 1) { + + val transformedPkg = if (isBoxedPrimitive) { + kotlinPkg + } else { + when (simpleName()) { + "Collection", + "List", + "Map", + "Set", + "Iterable" -> kotlinCollectionsPkg + "String" -> kotlinPkg + else -> null + } + } + + if (transformedPkg != null) { + return transformedPkg + } + } + + return packageName() +} + +/** Some classes, notably Integer and Character, have a different simple name in Kotlin. */ +private fun JavaClassName.getSimpleNamesInKotlin(): List { + val originalNames = simpleNames() + + if (isBoxedPrimitive) { + val transformedName = when (originalNames.first()) { + "Integer" -> "Int" + "Character" -> "Char" + else -> null + } + + if (transformedName != null) { + return listOf(transformedName) + } + } + + return originalNames +} + +fun JavaClassName.setPackage(packageName: String) + = JavaClassName.get(packageName, simpleName(), *simpleNames().drop(1).toTypedArray())!! // Does not support transferring annotations fun JavaWildcardTypeName.toKPoet() = @@ -44,10 +104,31 @@ fun JavaParametrizedTypeName.toKPoet() *typeArguments.toKPoet().toTypedArray()) // Does not support transferring annotations -fun JavaArrayTypeName.toKPoet() - = KotlinParameterizedTypeName.get( - KotlinClassName.bestGuess("kotlin.Array"), - this.componentType.toKPoet()) +fun JavaArrayTypeName.toKPoet(): KotlinTypeName { + + // Kotlin has special classes for primitive arrays + if (componentType.isPrimitive) { + val kotlinArrayType = when (componentType) { + TypeName.BYTE -> "ByteArray" + TypeName.SHORT -> "ShortArray" + TypeName.CHAR -> "CharArray" + TypeName.INT -> "IntArray" + TypeName.FLOAT -> "FloatArray" + TypeName.DOUBLE -> "DoubleArray" + TypeName.LONG -> "LongArray" + TypeName.BOOLEAN -> "BooleanArray" + else -> null + } + + if (kotlinArrayType != null) { + return KotlinClassName(kotlinPkg, kotlinArrayType) + } + } + + return KotlinParameterizedTypeName.get( + KotlinClassName(kotlinPkg, "Array"), + this.componentType.toKPoet()) +} // Does not support transferring annotations fun JavaTypeVariableName.toKPoet() @@ -76,12 +157,18 @@ fun JavaTypeName.toKPoet(): KotlinTypeName = when (this) { fun Iterable.toKPoet() = map { it.toKPoet() } -fun JavaParameterSpec.toKPoet(): KotlinParameterSpec - = KotlinParameterSpec.builder( - name, - type.toKPoet(), - *modifiers.toKModifier().toTypedArray() -).build() +fun JavaParameterSpec.toKPoet(): KotlinParameterSpec { + + // A param name in java might be reserved in kotlin + val paramName = if (name in KOTLIN_KEYWORDS) name + "Param" else name + + return KotlinParameterSpec.builder( + paramName, + type.toKPoet(), + *modifiers.toKModifier().toTypedArray() + ).build() + +} fun Iterable.toKParams() = map { it.toKPoet() } @@ -97,3 +184,35 @@ fun Modifier.toKModifier() = when (this) { else -> null } +// https://github.com/JetBrains/kotlin/blob/master/core/descriptors/src/org/jetbrains/kotlin/renderer/KeywordStringsGenerated.java +private val KOTLIN_KEYWORDS = setOf( + "package", + "as", + "typealias", + "class", + "this", + "super", + "val", + "var", + "fun", + "for", + "null", + "true", + "false", + "is", + "in", + "throw", + "return", + "break", + "continue", + "object", + "if", + "try", + "else", + "while", + "do", + "when", + "interface", + "typeof" +) +