mirror of
https://github.com/fankes/moshi.git
synced 2025-10-19 16:09:21 +08:00
Generate proguard rules for code gen on demand (#1067)
* Extract target constructor signature into TargetConstructor.kt We'll need this to know what the runtime types are for proguard rules * Add ProguardConfig * Wire in proguard config * Write proguard configs out with adapters * Add full tests * Now remove the rules! * Ignore on inline classes for now * Pass adapter constructor params correctly
This commit is contained in:
@@ -47,6 +47,11 @@
|
|||||||
<artifactId>kotlinpoet-classinspector-elements</artifactId>
|
<artifactId>kotlinpoet-classinspector-elements</artifactId>
|
||||||
<version>${kotlinpoet.version}</version>
|
<version>${kotlinpoet.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ow2.asm</groupId>
|
||||||
|
<artifactId>asm</artifactId>
|
||||||
|
<version>${asm.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.ltgt.gradle.incap</groupId>
|
<groupId>net.ltgt.gradle.incap</groupId>
|
||||||
<artifactId>incap</artifactId>
|
<artifactId>incap</artifactId>
|
||||||
|
@@ -108,9 +108,9 @@ class JsonClassCodegenProcessor : AbstractProcessor() {
|
|||||||
val jsonClass = type.getAnnotation(annotation)
|
val jsonClass = type.getAnnotation(annotation)
|
||||||
if (jsonClass.generateAdapter && jsonClass.generator.isEmpty()) {
|
if (jsonClass.generateAdapter && jsonClass.generator.isEmpty()) {
|
||||||
val generator = adapterGenerator(type, cachedClassInspector) ?: continue
|
val generator = adapterGenerator(type, cachedClassInspector) ?: continue
|
||||||
generator
|
val preparedAdapter = generator
|
||||||
.generateFile {
|
.prepare { spec ->
|
||||||
it.toBuilder()
|
spec.toBuilder()
|
||||||
.apply {
|
.apply {
|
||||||
generatedType?.asClassName()?.let { generatedClassName ->
|
generatedType?.asClassName()?.let { generatedClassName ->
|
||||||
addAnnotation(
|
addAnnotation(
|
||||||
@@ -125,14 +125,19 @@ class JsonClassCodegenProcessor : AbstractProcessor() {
|
|||||||
.addOriginatingElement(type)
|
.addOriginatingElement(type)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
.writeTo(filer)
|
|
||||||
|
preparedAdapter.spec.writeTo(filer)
|
||||||
|
preparedAdapter.proguardConfig?.writeTo(filer, type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun adapterGenerator(element: TypeElement, cachedClassInspector: MoshiCachedClassInspector): AdapterGenerator? {
|
private fun adapterGenerator(
|
||||||
|
element: TypeElement,
|
||||||
|
cachedClassInspector: MoshiCachedClassInspector
|
||||||
|
): AdapterGenerator? {
|
||||||
val type = targetType(messager, elements, types, element, cachedClassInspector) ?: return null
|
val type = targetType(messager, elements, types, element, cachedClassInspector) ?: return null
|
||||||
|
|
||||||
val properties = mutableMapOf<String, PropertyGenerator>()
|
val properties = mutableMapOf<String, PropertyGenerator>()
|
||||||
|
@@ -45,6 +45,7 @@ import com.squareup.moshi.kotlin.codegen.api.FromJsonComponent.ParameterProperty
|
|||||||
import com.squareup.moshi.kotlin.codegen.api.FromJsonComponent.PropertyOnly
|
import com.squareup.moshi.kotlin.codegen.api.FromJsonComponent.PropertyOnly
|
||||||
import java.lang.reflect.Constructor
|
import java.lang.reflect.Constructor
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
|
import org.objectweb.asm.Type as AsmType
|
||||||
|
|
||||||
private val MOSHI_UTIL = Util::class.asClassName()
|
private val MOSHI_UTIL = Util::class.asClassName()
|
||||||
private const val TO_STRING_PREFIX = "GeneratedJsonAdapter("
|
private const val TO_STRING_PREFIX = "GeneratedJsonAdapter("
|
||||||
@@ -52,7 +53,7 @@ private const val TO_STRING_SIZE_BASE = TO_STRING_PREFIX.length + 1 // 1 is the
|
|||||||
|
|
||||||
/** Generates a JSON adapter for a target type. */
|
/** Generates a JSON adapter for a target type. */
|
||||||
internal class AdapterGenerator(
|
internal class AdapterGenerator(
|
||||||
target: TargetType,
|
private val target: TargetType,
|
||||||
private val propertyList: List<PropertyGenerator>
|
private val propertyList: List<PropertyGenerator>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -60,6 +61,8 @@ internal class AdapterGenerator(
|
|||||||
private val INT_TYPE_BLOCK = CodeBlock.of("%T::class.javaPrimitiveType", INT)
|
private val INT_TYPE_BLOCK = CodeBlock.of("%T::class.javaPrimitiveType", INT)
|
||||||
private val DEFAULT_CONSTRUCTOR_MARKER_TYPE_BLOCK = CodeBlock.of(
|
private val DEFAULT_CONSTRUCTOR_MARKER_TYPE_BLOCK = CodeBlock.of(
|
||||||
"%T.DEFAULT_CONSTRUCTOR_MARKER", Util::class)
|
"%T.DEFAULT_CONSTRUCTOR_MARKER", Util::class)
|
||||||
|
private val CN_MOSHI = Moshi::class.asClassName()
|
||||||
|
private val CN_TYPE = Type::class.asClassName()
|
||||||
|
|
||||||
private val COMMON_SUPPRESS = arrayOf(
|
private val COMMON_SUPPRESS = arrayOf(
|
||||||
// https://github.com/square/moshi/issues/1023
|
// https://github.com/square/moshi/issues/1023
|
||||||
@@ -98,10 +101,10 @@ internal class AdapterGenerator(
|
|||||||
|
|
||||||
private val moshiParam = ParameterSpec.builder(
|
private val moshiParam = ParameterSpec.builder(
|
||||||
nameAllocator.newName("moshi"),
|
nameAllocator.newName("moshi"),
|
||||||
Moshi::class).build()
|
CN_MOSHI).build()
|
||||||
private val typesParam = ParameterSpec.builder(
|
private val typesParam = ParameterSpec.builder(
|
||||||
nameAllocator.newName("types"),
|
nameAllocator.newName("types"),
|
||||||
ARRAY.parameterizedBy(Type::class.asTypeName()))
|
ARRAY.parameterizedBy(CN_TYPE))
|
||||||
.build()
|
.build()
|
||||||
private val readerParam = ParameterSpec.builder(
|
private val readerParam = ParameterSpec.builder(
|
||||||
nameAllocator.newName("reader"),
|
nameAllocator.newName("reader"),
|
||||||
@@ -140,15 +143,59 @@ internal class AdapterGenerator(
|
|||||||
.initializer("null")
|
.initializer("null")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun generateFile(typeHook: (TypeSpec) -> TypeSpec = { it }): FileSpec {
|
fun prepare(typeHook: (TypeSpec) -> TypeSpec = { it }): PreparedAdapter {
|
||||||
for (property in nonTransientProperties) {
|
for (property in nonTransientProperties) {
|
||||||
property.allocateNames(nameAllocator)
|
property.allocateNames(nameAllocator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val generatedAdapter = generateType().let(typeHook)
|
||||||
val result = FileSpec.builder(className.packageName, adapterName)
|
val result = FileSpec.builder(className.packageName, adapterName)
|
||||||
result.addComment("Code generated by moshi-kotlin-codegen. Do not edit.")
|
result.addComment("Code generated by moshi-kotlin-codegen. Do not edit.")
|
||||||
result.addType(generateType().let(typeHook))
|
result.addType(generatedAdapter)
|
||||||
return result.build()
|
return PreparedAdapter(result.build(), generatedAdapter.createProguardRule())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TypeSpec.createProguardRule(): ProguardConfig {
|
||||||
|
val adapterProperties = propertySpecs
|
||||||
|
.asSequence()
|
||||||
|
.filter { prop ->
|
||||||
|
prop.type.rawType() == JsonAdapter::class.asClassName()
|
||||||
|
}
|
||||||
|
.filter { prop -> prop.annotations.isNotEmpty() }
|
||||||
|
.mapTo(mutableSetOf()) { prop ->
|
||||||
|
QualifierAdapterProperty(
|
||||||
|
name = prop.name,
|
||||||
|
qualifiers = prop.annotations.mapTo(mutableSetOf()) { it.className }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val adapterConstructorParams = when (requireNotNull(primaryConstructor).parameters.size) {
|
||||||
|
1 -> listOf(CN_MOSHI.canonicalName)
|
||||||
|
2 -> listOf(CN_MOSHI.canonicalName, "${CN_TYPE.canonicalName}[]")
|
||||||
|
// Should never happen
|
||||||
|
else -> error("Unexpected number of arguments on primary constructor: $primaryConstructor")
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasDefaultProperties = false
|
||||||
|
var parameterTypes = emptyList<String>()
|
||||||
|
target.constructor.signature?.let { constructorSignature ->
|
||||||
|
if (constructorSignature.startsWith("constructor-impl")) {
|
||||||
|
// Inline class, we don't support this yet.
|
||||||
|
// This is a static method with signature like 'constructor-impl(I)I'
|
||||||
|
return@let
|
||||||
|
}
|
||||||
|
hasDefaultProperties = propertyList.any { it.hasDefault }
|
||||||
|
parameterTypes = AsmType.getArgumentTypes(constructorSignature.removePrefix("<init>"))
|
||||||
|
.map { it.toCanonicalString() }
|
||||||
|
}
|
||||||
|
return ProguardConfig(
|
||||||
|
targetClass = className,
|
||||||
|
adapterName = adapterName,
|
||||||
|
adapterConstructorParams = adapterConstructorParams,
|
||||||
|
targetConstructorHasDefaults = hasDefaultProperties,
|
||||||
|
targetConstructorParams = parameterTypes,
|
||||||
|
qualifierProperties = adapterProperties
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateType(): TypeSpec {
|
private fun generateType(): TypeSpec {
|
||||||
@@ -518,6 +565,28 @@ internal class AdapterGenerator(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Represents a prepared adapter with its [spec] and optional associated [proguardConfig]. */
|
||||||
|
internal data class PreparedAdapter(val spec: FileSpec, val proguardConfig: ProguardConfig?)
|
||||||
|
|
||||||
|
private fun AsmType.toCanonicalString(): String {
|
||||||
|
return when (this) {
|
||||||
|
AsmType.VOID_TYPE -> "void"
|
||||||
|
AsmType.BOOLEAN_TYPE -> "boolean"
|
||||||
|
AsmType.CHAR_TYPE -> "char"
|
||||||
|
AsmType.BYTE_TYPE -> "byte"
|
||||||
|
AsmType.SHORT_TYPE -> "short"
|
||||||
|
AsmType.INT_TYPE -> "int"
|
||||||
|
AsmType.FLOAT_TYPE -> "float"
|
||||||
|
AsmType.LONG_TYPE -> "long"
|
||||||
|
AsmType.DOUBLE_TYPE -> "double"
|
||||||
|
else -> when (sort) {
|
||||||
|
AsmType.ARRAY -> "${elementType.toCanonicalString()}[]"
|
||||||
|
// Object type
|
||||||
|
else -> className
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private interface PropertyComponent {
|
private interface PropertyComponent {
|
||||||
val property: PropertyGenerator
|
val property: PropertyGenerator
|
||||||
val type: TypeName
|
val type: TypeName
|
||||||
|
@@ -0,0 +1,104 @@
|
|||||||
|
package com.squareup.moshi.kotlin.codegen.api
|
||||||
|
|
||||||
|
import com.squareup.kotlinpoet.ClassName
|
||||||
|
import javax.annotation.processing.Filer
|
||||||
|
import javax.lang.model.element.Element
|
||||||
|
import javax.tools.StandardLocation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a proguard configuration for a given spec. This covers three main areas:
|
||||||
|
* - Keeping the target class name to Moshi's reflective lookup of the adapter.
|
||||||
|
* - Keeping the generated adapter class name + public constructor for reflective lookup.
|
||||||
|
* - Keeping any used JsonQualifier annotations and the properties they are attached to.
|
||||||
|
* - If the target class has default parameter values, also keeping the associated synthetic
|
||||||
|
* constructor as well as the DefaultConstructorMarker type Kotlin adds to it.
|
||||||
|
*
|
||||||
|
* Each rule is intended to be as specific and targeted as possible to reduce footprint, and each is
|
||||||
|
* conditioned on usage of the original target type.
|
||||||
|
*
|
||||||
|
* To keep this processor as an ISOLATING incremental processor, we generate one file per target
|
||||||
|
* class with a deterministic name (see [outputFile]) with an appropriate originating element.
|
||||||
|
*/
|
||||||
|
internal data class ProguardConfig(
|
||||||
|
val targetClass: ClassName,
|
||||||
|
val adapterName: String,
|
||||||
|
val adapterConstructorParams: List<String>,
|
||||||
|
val targetConstructorHasDefaults: Boolean,
|
||||||
|
val targetConstructorParams: List<String>,
|
||||||
|
val qualifierProperties: Set<QualifierAdapterProperty>
|
||||||
|
) {
|
||||||
|
private val outputFile = "META-INF/proguard/moshi-${targetClass.canonicalName}.pro"
|
||||||
|
|
||||||
|
/** Writes this to `filer`. */
|
||||||
|
fun writeTo(filer: Filer, vararg originatingElements: Element) {
|
||||||
|
filer.createResource(StandardLocation.CLASS_OUTPUT, "", outputFile, *originatingElements)
|
||||||
|
.openWriter()
|
||||||
|
.use(::writeTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeTo(out: Appendable): Unit = out.run {
|
||||||
|
//
|
||||||
|
// -if class {the target class}
|
||||||
|
// -keepnames class {the target class}
|
||||||
|
// -if class {the target class}
|
||||||
|
// -keep class {the generated adapter} {
|
||||||
|
// <init>(...);
|
||||||
|
// private final {adapter fields}
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
val targetName = targetClass.canonicalName
|
||||||
|
val adapterCanonicalName = ClassName(targetClass.packageName, adapterName)
|
||||||
|
// Keep the class name for Moshi's reflective lookup based on it
|
||||||
|
appendln("-if class $targetName")
|
||||||
|
appendln("-keepnames class $targetName")
|
||||||
|
|
||||||
|
appendln("-if class $targetName")
|
||||||
|
appendln("-keep class $adapterCanonicalName {")
|
||||||
|
// Keep the constructor for Moshi's reflective lookup
|
||||||
|
val constructorArgs = adapterConstructorParams.joinToString(",")
|
||||||
|
appendln(" public <init>($constructorArgs)")
|
||||||
|
// Keep any qualifier properties
|
||||||
|
for (qualifierProperty in qualifierProperties) {
|
||||||
|
appendln(" private com.squareup.moshi.JsonAdapter ${qualifierProperty.name}")
|
||||||
|
}
|
||||||
|
appendln("}")
|
||||||
|
|
||||||
|
qualifierProperties.asSequence()
|
||||||
|
.flatMap { it.qualifiers.asSequence() }
|
||||||
|
.map(ClassName::canonicalName)
|
||||||
|
.sorted()
|
||||||
|
.forEach { qualifier ->
|
||||||
|
appendln("-if class $targetName")
|
||||||
|
appendln("-keep @interface $qualifier")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetConstructorHasDefaults) {
|
||||||
|
// If the target class has default parameter values, keep its synthetic constructor
|
||||||
|
//
|
||||||
|
// -keepnames class kotlin.jvm.internal.DefaultConstructorMarker
|
||||||
|
// -keepclassmembers @com.squareup.moshi.JsonClass @kotlin.Metadata class * {
|
||||||
|
// synthetic <init>(...);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
appendln("-if class $targetName")
|
||||||
|
appendln("-keepnames class kotlin.jvm.internal.DefaultConstructorMarker")
|
||||||
|
appendln("-if class $targetName")
|
||||||
|
appendln("-keepclassmembers class ${targetClass.canonicalName} {")
|
||||||
|
val allParams = targetConstructorParams.toMutableList()
|
||||||
|
val maskCount = (targetConstructorParams.size % 32) + 1
|
||||||
|
repeat(maskCount) {
|
||||||
|
allParams += "int"
|
||||||
|
}
|
||||||
|
allParams += "kotlin.jvm.internal.DefaultConstructorMarker"
|
||||||
|
val params = allParams.joinToString(",")
|
||||||
|
appendln(" public synthetic <init>($params)")
|
||||||
|
appendln("}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a qualified property with its [name] in the adapter fields and list of [qualifiers]
|
||||||
|
* associated with it.
|
||||||
|
*/
|
||||||
|
internal data class QualifierAdapterProperty(val name: String, val qualifiers: Set<ClassName>)
|
@@ -20,7 +20,8 @@ import com.squareup.kotlinpoet.KModifier
|
|||||||
/** A constructor in user code that should be called by generated code. */
|
/** A constructor in user code that should be called by generated code. */
|
||||||
internal data class TargetConstructor(
|
internal data class TargetConstructor(
|
||||||
val parameters: LinkedHashMap<String, TargetParameter>,
|
val parameters: LinkedHashMap<String, TargetParameter>,
|
||||||
val visibility: KModifier
|
val visibility: KModifier,
|
||||||
|
val signature: String?
|
||||||
) {
|
) {
|
||||||
init {
|
init {
|
||||||
visibility.checkIsVisibility()
|
visibility.checkIsVisibility()
|
||||||
|
@@ -22,6 +22,7 @@ import com.squareup.kotlinpoet.TypeName
|
|||||||
import com.squareup.kotlinpoet.TypeSpec
|
import com.squareup.kotlinpoet.TypeSpec
|
||||||
import com.squareup.kotlinpoet.asClassName
|
import com.squareup.kotlinpoet.asClassName
|
||||||
import com.squareup.kotlinpoet.asTypeName
|
import com.squareup.kotlinpoet.asTypeName
|
||||||
|
import com.squareup.kotlinpoet.metadata.ImmutableKmConstructor
|
||||||
import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview
|
import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview
|
||||||
import com.squareup.kotlinpoet.metadata.isAbstract
|
import com.squareup.kotlinpoet.metadata.isAbstract
|
||||||
import com.squareup.kotlinpoet.metadata.isClass
|
import com.squareup.kotlinpoet.metadata.isClass
|
||||||
@@ -54,7 +55,7 @@ import javax.lang.model.element.ElementKind
|
|||||||
import javax.lang.model.element.TypeElement
|
import javax.lang.model.element.TypeElement
|
||||||
import javax.lang.model.util.Elements
|
import javax.lang.model.util.Elements
|
||||||
import javax.lang.model.util.Types
|
import javax.lang.model.util.Types
|
||||||
import javax.tools.Diagnostic
|
import javax.tools.Diagnostic.Kind.ERROR
|
||||||
|
|
||||||
private val JSON_QUALIFIER = JsonQualifier::class.java
|
private val JSON_QUALIFIER = JsonQualifier::class.java
|
||||||
private val JSON = Json::class.asClassName()
|
private val JSON = Json::class.asClassName()
|
||||||
@@ -71,7 +72,12 @@ private fun Collection<KModifier>.visibility(): KModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@KotlinPoetMetadataPreview
|
@KotlinPoetMetadataPreview
|
||||||
internal fun primaryConstructor(kotlinApi: TypeSpec, elements: Elements): TargetConstructor? {
|
internal fun primaryConstructor(
|
||||||
|
targetElement: TypeElement,
|
||||||
|
kotlinApi: TypeSpec,
|
||||||
|
elements: Elements,
|
||||||
|
messager: Messager
|
||||||
|
): TargetConstructor? {
|
||||||
val primaryConstructor = kotlinApi.primaryConstructor ?: return null
|
val primaryConstructor = kotlinApi.primaryConstructor ?: return null
|
||||||
|
|
||||||
val parameters = LinkedHashMap<String, TargetParameter>()
|
val parameters = LinkedHashMap<String, TargetParameter>()
|
||||||
@@ -87,7 +93,14 @@ internal fun primaryConstructor(kotlinApi: TypeSpec, elements: Elements): Target
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return TargetConstructor(parameters, primaryConstructor.modifiers.visibility())
|
val kmConstructorSignature = primaryConstructor.tag<ImmutableKmConstructor>()?.signature?.toString()
|
||||||
|
?: run {
|
||||||
|
messager.printMessage(ERROR, "No KmConstructor found for primary constructor.",
|
||||||
|
targetElement)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
return TargetConstructor(parameters, primaryConstructor.modifiers.visibility(),
|
||||||
|
kmConstructorSignature)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns a target type for `element`, or null if it cannot be used with code gen. */
|
/** Returns a target type for `element`, or null if it cannot be used with code gen. */
|
||||||
@@ -101,7 +114,7 @@ internal fun targetType(messager: Messager,
|
|||||||
val typeMetadata = element.getAnnotation(Metadata::class.java)
|
val typeMetadata = element.getAnnotation(Metadata::class.java)
|
||||||
if (typeMetadata == null) {
|
if (typeMetadata == null) {
|
||||||
messager.printMessage(
|
messager.printMessage(
|
||||||
Diagnostic.Kind.ERROR, "@JsonClass can't be applied to $element: must be a Kotlin class",
|
ERROR, "@JsonClass can't be applied to $element: must be a Kotlin class",
|
||||||
element)
|
element)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -110,7 +123,7 @@ internal fun targetType(messager: Messager,
|
|||||||
cachedClassInspector.toImmutableKmClass(typeMetadata)
|
cachedClassInspector.toImmutableKmClass(typeMetadata)
|
||||||
} catch (e: UnsupportedOperationException) {
|
} catch (e: UnsupportedOperationException) {
|
||||||
messager.printMessage(
|
messager.printMessage(
|
||||||
Diagnostic.Kind.ERROR, "@JsonClass can't be applied to $element: must be a Class type",
|
ERROR, "@JsonClass can't be applied to $element: must be a Class type",
|
||||||
element)
|
element)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -118,44 +131,44 @@ internal fun targetType(messager: Messager,
|
|||||||
when {
|
when {
|
||||||
kmClass.isEnum -> {
|
kmClass.isEnum -> {
|
||||||
messager.printMessage(
|
messager.printMessage(
|
||||||
Diagnostic.Kind.ERROR,
|
ERROR,
|
||||||
"@JsonClass with 'generateAdapter = \"true\"' can't be applied to $element: code gen for enums is not supported or necessary",
|
"@JsonClass with 'generateAdapter = \"true\"' can't be applied to $element: code gen for enums is not supported or necessary",
|
||||||
element)
|
element)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
!kmClass.isClass -> {
|
!kmClass.isClass -> {
|
||||||
messager.printMessage(
|
messager.printMessage(
|
||||||
Diagnostic.Kind.ERROR, "@JsonClass can't be applied to $element: must be a Kotlin class",
|
ERROR, "@JsonClass can't be applied to $element: must be a Kotlin class",
|
||||||
element)
|
element)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
kmClass.isInner -> {
|
kmClass.isInner -> {
|
||||||
messager.printMessage(
|
messager.printMessage(
|
||||||
Diagnostic.Kind.ERROR,
|
ERROR,
|
||||||
"@JsonClass can't be applied to $element: must not be an inner class", element)
|
"@JsonClass can't be applied to $element: must not be an inner class", element)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
kmClass.isSealed -> {
|
kmClass.isSealed -> {
|
||||||
messager.printMessage(
|
messager.printMessage(
|
||||||
Diagnostic.Kind.ERROR, "@JsonClass can't be applied to $element: must not be sealed",
|
ERROR, "@JsonClass can't be applied to $element: must not be sealed",
|
||||||
element)
|
element)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
kmClass.isAbstract -> {
|
kmClass.isAbstract -> {
|
||||||
messager.printMessage(
|
messager.printMessage(
|
||||||
Diagnostic.Kind.ERROR, "@JsonClass can't be applied to $element: must not be abstract",
|
ERROR, "@JsonClass can't be applied to $element: must not be abstract",
|
||||||
element)
|
element)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
kmClass.isLocal -> {
|
kmClass.isLocal -> {
|
||||||
messager.printMessage(
|
messager.printMessage(
|
||||||
Diagnostic.Kind.ERROR, "@JsonClass can't be applied to $element: must not be local",
|
ERROR, "@JsonClass can't be applied to $element: must not be local",
|
||||||
element)
|
element)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
!kmClass.isPublic && !kmClass.isInternal -> {
|
!kmClass.isPublic && !kmClass.isInternal -> {
|
||||||
messager.printMessage(
|
messager.printMessage(
|
||||||
Diagnostic.Kind.ERROR,
|
ERROR,
|
||||||
"@JsonClass can't be applied to $element: must be internal or public",
|
"@JsonClass can't be applied to $element: must be internal or public",
|
||||||
element)
|
element)
|
||||||
return null
|
return null
|
||||||
@@ -166,14 +179,14 @@ internal fun targetType(messager: Messager,
|
|||||||
val typeVariables = kotlinApi.typeVariables
|
val typeVariables = kotlinApi.typeVariables
|
||||||
val appliedType = AppliedType.get(element)
|
val appliedType = AppliedType.get(element)
|
||||||
|
|
||||||
val constructor = primaryConstructor(kotlinApi, elements)
|
val constructor = primaryConstructor(element, kotlinApi, elements, messager)
|
||||||
if (constructor == null) {
|
if (constructor == null) {
|
||||||
messager.printMessage(Diagnostic.Kind.ERROR, "No primary constructor found on $element",
|
messager.printMessage(ERROR, "No primary constructor found on $element",
|
||||||
element)
|
element)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if (constructor.visibility != KModifier.INTERNAL && constructor.visibility != KModifier.PUBLIC) {
|
if (constructor.visibility != KModifier.INTERNAL && constructor.visibility != KModifier.PUBLIC) {
|
||||||
messager.printMessage(Diagnostic.Kind.ERROR, "@JsonClass can't be applied to $element: " +
|
messager.printMessage(ERROR, "@JsonClass can't be applied to $element: " +
|
||||||
"primary constructor is not internal or public", element)
|
"primary constructor is not internal or public", element)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -186,7 +199,7 @@ internal fun targetType(messager: Messager,
|
|||||||
}
|
}
|
||||||
.onEach { supertype ->
|
.onEach { supertype ->
|
||||||
if (supertype.element.getAnnotation(Metadata::class.java) == null) {
|
if (supertype.element.getAnnotation(Metadata::class.java) == null) {
|
||||||
messager.printMessage(Diagnostic.Kind.ERROR,
|
messager.printMessage(ERROR,
|
||||||
"@JsonClass can't be applied to $element: supertype $supertype is not a Kotlin type",
|
"@JsonClass can't be applied to $element: supertype $supertype is not a Kotlin type",
|
||||||
element)
|
element)
|
||||||
return null
|
return null
|
||||||
@@ -275,7 +288,7 @@ internal fun TargetProperty.generator(
|
|||||||
if (isTransient) {
|
if (isTransient) {
|
||||||
if (!hasDefault) {
|
if (!hasDefault) {
|
||||||
messager.printMessage(
|
messager.printMessage(
|
||||||
Diagnostic.Kind.ERROR, "No default value for transient property $name",
|
ERROR, "No default value for transient property $name",
|
||||||
sourceElement)
|
sourceElement)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -283,7 +296,7 @@ internal fun TargetProperty.generator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
messager.printMessage(Diagnostic.Kind.ERROR, "property $name is not visible",
|
messager.printMessage(ERROR, "property $name is not visible",
|
||||||
sourceElement)
|
sourceElement)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -300,13 +313,13 @@ internal fun TargetProperty.generator(
|
|||||||
?: continue
|
?: continue
|
||||||
annotationElement.getAnnotation(Retention::class.java)?.let {
|
annotationElement.getAnnotation(Retention::class.java)?.let {
|
||||||
if (it.value != RetentionPolicy.RUNTIME) {
|
if (it.value != RetentionPolicy.RUNTIME) {
|
||||||
messager.printMessage(Diagnostic.Kind.ERROR,
|
messager.printMessage(ERROR,
|
||||||
"JsonQualifier @${jsonQualifier.className.simpleName} must have RUNTIME retention")
|
"JsonQualifier @${jsonQualifier.className.simpleName} must have RUNTIME retention")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
annotationElement.getAnnotation(Target::class.java)?.let {
|
annotationElement.getAnnotation(Target::class.java)?.let {
|
||||||
if (ElementType.FIELD !in it.value) {
|
if (ElementType.FIELD !in it.value) {
|
||||||
messager.printMessage(Diagnostic.Kind.ERROR,
|
messager.printMessage(ERROR,
|
||||||
"JsonQualifier @${jsonQualifier.className.simpleName} must support FIELD target")
|
"JsonQualifier @${jsonQualifier.className.simpleName} must support FIELD target")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,7 +348,9 @@ private fun List<AnnotationSpec>?.jsonName(): String? {
|
|||||||
val mirror = requireNotNull(annotation.tag<AnnotationMirror>()) {
|
val mirror = requireNotNull(annotation.tag<AnnotationMirror>()) {
|
||||||
"Could not get the annotation mirror from the annotation spec"
|
"Could not get the annotation mirror from the annotation spec"
|
||||||
}
|
}
|
||||||
mirror.elementValues.entries.single { it.key.simpleName.contentEquals("name") }.value.value as String
|
mirror.elementValues.entries.single {
|
||||||
|
it.key.simpleName.contentEquals("name")
|
||||||
|
}.value.value as String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -398,6 +398,211 @@ class JsonClassCodegenProcessorTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Processor should generate comprehensive proguard rules`() {
|
||||||
|
val result = compile(kotlin("source.kt",
|
||||||
|
"""
|
||||||
|
package testPackage
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import com.squareup.moshi.JsonQualifier
|
||||||
|
|
||||||
|
typealias FirstName = String
|
||||||
|
typealias LastName = String
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class Aliases(val firstName: FirstName, val lastName: LastName, val hairColor: String)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class Simple(val firstName: String)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class Generic<T>(val firstName: T, val lastName: String)
|
||||||
|
|
||||||
|
@JsonQualifier
|
||||||
|
annotation class MyQualifier
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class UsingQualifiers(val firstName: String, @MyQualifier val lastName: String)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class MixedTypes(val firstName: String, val otherNames: MutableList<String>)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class DefaultParams(val firstName: String = "")
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class Complex<T>(val firstName: FirstName = "", @MyQualifier val names: MutableList<String>, val genericProp: T)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
class MultipleMasks(
|
||||||
|
val arg0: Long = 0,
|
||||||
|
val arg1: Long = 1,
|
||||||
|
val arg2: Long = 2,
|
||||||
|
val arg3: Long = 3,
|
||||||
|
val arg4: Long = 4,
|
||||||
|
val arg5: Long = 5,
|
||||||
|
val arg6: Long = 6,
|
||||||
|
val arg7: Long = 7,
|
||||||
|
val arg8: Long = 8,
|
||||||
|
val arg9: Long = 9,
|
||||||
|
val arg10: Long = 10,
|
||||||
|
val arg11: Long,
|
||||||
|
val arg12: Long = 12,
|
||||||
|
val arg13: Long = 13,
|
||||||
|
val arg14: Long = 14,
|
||||||
|
val arg15: Long = 15,
|
||||||
|
val arg16: Long = 16,
|
||||||
|
val arg17: Long = 17,
|
||||||
|
val arg18: Long = 18,
|
||||||
|
val arg19: Long = 19,
|
||||||
|
@Suppress("UNUSED_PARAMETER") arg20: Long = 20,
|
||||||
|
val arg21: Long = 21,
|
||||||
|
val arg22: Long = 22,
|
||||||
|
val arg23: Long = 23,
|
||||||
|
val arg24: Long = 24,
|
||||||
|
val arg25: Long = 25,
|
||||||
|
val arg26: Long = 26,
|
||||||
|
val arg27: Long = 27,
|
||||||
|
val arg28: Long = 28,
|
||||||
|
val arg29: Long = 29,
|
||||||
|
val arg30: Long = 30,
|
||||||
|
val arg31: Long = 31,
|
||||||
|
val arg32: Long = 32,
|
||||||
|
val arg33: Long = 33,
|
||||||
|
val arg34: Long = 34,
|
||||||
|
val arg35: Long = 35,
|
||||||
|
val arg36: Long = 36,
|
||||||
|
val arg37: Long = 37,
|
||||||
|
val arg38: Long = 38,
|
||||||
|
@Transient val arg39: Long = 39,
|
||||||
|
val arg40: Long = 40,
|
||||||
|
val arg41: Long = 41,
|
||||||
|
val arg42: Long = 42,
|
||||||
|
val arg43: Long = 43,
|
||||||
|
val arg44: Long = 44,
|
||||||
|
val arg45: Long = 45,
|
||||||
|
val arg46: Long = 46,
|
||||||
|
val arg47: Long = 47,
|
||||||
|
val arg48: Long = 48,
|
||||||
|
val arg49: Long = 49,
|
||||||
|
val arg50: Long = 50,
|
||||||
|
val arg51: Long = 51,
|
||||||
|
val arg52: Long = 52,
|
||||||
|
@Transient val arg53: Long = 53,
|
||||||
|
val arg54: Long = 54,
|
||||||
|
val arg55: Long = 55,
|
||||||
|
val arg56: Long = 56,
|
||||||
|
val arg57: Long = 57,
|
||||||
|
val arg58: Long = 58,
|
||||||
|
val arg59: Long = 59,
|
||||||
|
val arg60: Long = 60,
|
||||||
|
val arg61: Long = 61,
|
||||||
|
val arg62: Long = 62,
|
||||||
|
val arg63: Long = 63,
|
||||||
|
val arg64: Long = 64,
|
||||||
|
val arg65: Long = 65
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
))
|
||||||
|
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
|
||||||
|
|
||||||
|
assertThat(result.generatedFiles.find { it.name == "moshi-testPackage.Aliases.pro" }).hasContent("""
|
||||||
|
-if class testPackage.Aliases
|
||||||
|
-keepnames class testPackage.Aliases
|
||||||
|
-if class testPackage.Aliases
|
||||||
|
-keep class testPackage.AliasesJsonAdapter {
|
||||||
|
public <init>(com.squareup.moshi.Moshi)
|
||||||
|
}
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
assertThat(result.generatedFiles.find { it.name == "moshi-testPackage.Simple.pro" }).hasContent("""
|
||||||
|
-if class testPackage.Simple
|
||||||
|
-keepnames class testPackage.Simple
|
||||||
|
-if class testPackage.Simple
|
||||||
|
-keep class testPackage.SimpleJsonAdapter {
|
||||||
|
public <init>(com.squareup.moshi.Moshi)
|
||||||
|
}
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
assertThat(result.generatedFiles.find { it.name == "moshi-testPackage.Generic.pro" }).hasContent("""
|
||||||
|
-if class testPackage.Generic
|
||||||
|
-keepnames class testPackage.Generic
|
||||||
|
-if class testPackage.Generic
|
||||||
|
-keep class testPackage.GenericJsonAdapter {
|
||||||
|
public <init>(com.squareup.moshi.Moshi,java.lang.reflect.Type[])
|
||||||
|
}
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
assertThat(result.generatedFiles.find { it.name == "moshi-testPackage.UsingQualifiers.pro" }).hasContent("""
|
||||||
|
-if class testPackage.UsingQualifiers
|
||||||
|
-keepnames class testPackage.UsingQualifiers
|
||||||
|
-if class testPackage.UsingQualifiers
|
||||||
|
-keep class testPackage.UsingQualifiersJsonAdapter {
|
||||||
|
public <init>(com.squareup.moshi.Moshi)
|
||||||
|
private com.squareup.moshi.JsonAdapter stringAtMyQualifierAdapter
|
||||||
|
}
|
||||||
|
-if class testPackage.UsingQualifiers
|
||||||
|
-keep @interface testPackage.MyQualifier
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
assertThat(result.generatedFiles.find { it.name == "moshi-testPackage.MixedTypes.pro" }).hasContent("""
|
||||||
|
-if class testPackage.MixedTypes
|
||||||
|
-keepnames class testPackage.MixedTypes
|
||||||
|
-if class testPackage.MixedTypes
|
||||||
|
-keep class testPackage.MixedTypesJsonAdapter {
|
||||||
|
public <init>(com.squareup.moshi.Moshi)
|
||||||
|
}
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
assertThat(result.generatedFiles.find { it.name == "moshi-testPackage.DefaultParams.pro" }).hasContent("""
|
||||||
|
-if class testPackage.DefaultParams
|
||||||
|
-keepnames class testPackage.DefaultParams
|
||||||
|
-if class testPackage.DefaultParams
|
||||||
|
-keep class testPackage.DefaultParamsJsonAdapter {
|
||||||
|
public <init>(com.squareup.moshi.Moshi)
|
||||||
|
}
|
||||||
|
-if class testPackage.DefaultParams
|
||||||
|
-keepnames class kotlin.jvm.internal.DefaultConstructorMarker
|
||||||
|
-if class testPackage.DefaultParams
|
||||||
|
-keepclassmembers class testPackage.DefaultParams {
|
||||||
|
public synthetic <init>(java.lang.String,int,int,kotlin.jvm.internal.DefaultConstructorMarker)
|
||||||
|
}
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
assertThat(result.generatedFiles.find { it.name == "moshi-testPackage.Complex.pro" }).hasContent("""
|
||||||
|
-if class testPackage.Complex
|
||||||
|
-keepnames class testPackage.Complex
|
||||||
|
-if class testPackage.Complex
|
||||||
|
-keep class testPackage.ComplexJsonAdapter {
|
||||||
|
public <init>(com.squareup.moshi.Moshi,java.lang.reflect.Type[])
|
||||||
|
private com.squareup.moshi.JsonAdapter mutableListOfStringAtMyQualifierAdapter
|
||||||
|
}
|
||||||
|
-if class testPackage.Complex
|
||||||
|
-keep @interface testPackage.MyQualifier
|
||||||
|
-if class testPackage.Complex
|
||||||
|
-keepnames class kotlin.jvm.internal.DefaultConstructorMarker
|
||||||
|
-if class testPackage.Complex
|
||||||
|
-keepclassmembers class testPackage.Complex {
|
||||||
|
public synthetic <init>(java.lang.String,java.util.List,java.lang.Object,int,int,int,int,kotlin.jvm.internal.DefaultConstructorMarker)
|
||||||
|
}
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
assertThat(result.generatedFiles.find { it.name == "moshi-testPackage.MultipleMasks.pro" }).hasContent("""
|
||||||
|
-if class testPackage.MultipleMasks
|
||||||
|
-keepnames class testPackage.MultipleMasks
|
||||||
|
-if class testPackage.MultipleMasks
|
||||||
|
-keep class testPackage.MultipleMasksJsonAdapter {
|
||||||
|
public <init>(com.squareup.moshi.Moshi)
|
||||||
|
}
|
||||||
|
-if class testPackage.MultipleMasks
|
||||||
|
-keepnames class kotlin.jvm.internal.DefaultConstructorMarker
|
||||||
|
-if class testPackage.MultipleMasks
|
||||||
|
-keepclassmembers class testPackage.MultipleMasks {
|
||||||
|
public synthetic <init>(long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,int,int,int,kotlin.jvm.internal.DefaultConstructorMarker)
|
||||||
|
}
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
private fun prepareCompilation(vararg sourceFiles: SourceFile): KotlinCompilation {
|
private fun prepareCompilation(vararg sourceFiles: SourceFile): KotlinCompilation {
|
||||||
return KotlinCompilation()
|
return KotlinCompilation()
|
||||||
.apply {
|
.apply {
|
||||||
|
@@ -14,48 +14,3 @@
|
|||||||
<fields>;
|
<fields>;
|
||||||
**[] values();
|
**[] values();
|
||||||
}
|
}
|
||||||
|
|
||||||
# The name of @JsonClass types is used to look up the generated adapter.
|
|
||||||
-keepnames @com.squareup.moshi.JsonClass class *
|
|
||||||
|
|
||||||
# Retain generated target class's synthetic defaults constructor and keep DefaultConstructorMarker's
|
|
||||||
# name. We will look this up reflectively to invoke the type's constructor.
|
|
||||||
#
|
|
||||||
# We can't _just_ keep the defaults constructor because Proguard/R8's spec doesn't allow wildcard
|
|
||||||
# matching preceding parameters.
|
|
||||||
-keepnames class kotlin.jvm.internal.DefaultConstructorMarker
|
|
||||||
-keepclassmembers @com.squareup.moshi.JsonClass @kotlin.Metadata class * {
|
|
||||||
synthetic <init>(...);
|
|
||||||
}
|
|
||||||
|
|
||||||
# Retain generated JsonAdapters if annotated type is retained.
|
|
||||||
-if @com.squareup.moshi.JsonClass class *
|
|
||||||
-keep class <1>JsonAdapter {
|
|
||||||
<init>(...);
|
|
||||||
<fields>;
|
|
||||||
}
|
|
||||||
-if @com.squareup.moshi.JsonClass class **$*
|
|
||||||
-keep class <1>_<2>JsonAdapter {
|
|
||||||
<init>(...);
|
|
||||||
<fields>;
|
|
||||||
}
|
|
||||||
-if @com.squareup.moshi.JsonClass class **$*$*
|
|
||||||
-keep class <1>_<2>_<3>JsonAdapter {
|
|
||||||
<init>(...);
|
|
||||||
<fields>;
|
|
||||||
}
|
|
||||||
-if @com.squareup.moshi.JsonClass class **$*$*$*
|
|
||||||
-keep class <1>_<2>_<3>_<4>JsonAdapter {
|
|
||||||
<init>(...);
|
|
||||||
<fields>;
|
|
||||||
}
|
|
||||||
-if @com.squareup.moshi.JsonClass class **$*$*$*$*
|
|
||||||
-keep class <1>_<2>_<3>_<4>_<5>JsonAdapter {
|
|
||||||
<init>(...);
|
|
||||||
<fields>;
|
|
||||||
}
|
|
||||||
-if @com.squareup.moshi.JsonClass class **$*$*$*$*$*
|
|
||||||
-keep class <1>_<2>_<3>_<4>_<5>_<6>JsonAdapter {
|
|
||||||
<init>(...);
|
|
||||||
<fields>;
|
|
||||||
}
|
|
||||||
|
1
pom.xml
1
pom.xml
@@ -38,6 +38,7 @@
|
|||||||
<okio2.version>2.1.0</okio2.version>
|
<okio2.version>2.1.0</okio2.version>
|
||||||
<kotlin.version>1.3.60</kotlin.version>
|
<kotlin.version>1.3.60</kotlin.version>
|
||||||
<kotlinpoet.version>1.5.0</kotlinpoet.version>
|
<kotlinpoet.version>1.5.0</kotlinpoet.version>
|
||||||
|
<asm.version>7.1</asm.version>
|
||||||
<kotlinx-metadata.version>0.1.0</kotlinx-metadata.version>
|
<kotlinx-metadata.version>0.1.0</kotlinx-metadata.version>
|
||||||
<maven-assembly.version>3.1.0</maven-assembly.version>
|
<maven-assembly.version>3.1.0</maven-assembly.version>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user