mirror of
https://github.com/fankes/moshi.git
synced 2025-10-20 16:39:22 +08:00
Support properties that don't have a backing field.
Currently our main loop to gather PropertyGenerators goes over the backing fields. This needs to change to iterate over the properties themselves. That leads to a lot of churn. The net result is slightly more compatibility with the reflective adapter.
This commit is contained in:
@@ -28,6 +28,7 @@ import me.eugeniomarletti.kotlin.metadata.KotlinMetadataUtils
|
||||
import me.eugeniomarletti.kotlin.metadata.classKind
|
||||
import me.eugeniomarletti.kotlin.metadata.declaresDefaultValue
|
||||
import me.eugeniomarletti.kotlin.metadata.getPropertyOrNull
|
||||
import me.eugeniomarletti.kotlin.metadata.hasSetter
|
||||
import me.eugeniomarletti.kotlin.metadata.isDataClass
|
||||
import me.eugeniomarletti.kotlin.metadata.isInnerClass
|
||||
import me.eugeniomarletti.kotlin.metadata.isPrimary
|
||||
@@ -41,6 +42,7 @@ import org.jetbrains.kotlin.serialization.ProtoBuf.Modality
|
||||
import org.jetbrains.kotlin.serialization.ProtoBuf.Property
|
||||
import org.jetbrains.kotlin.serialization.ProtoBuf.ValueParameter
|
||||
import org.jetbrains.kotlin.serialization.ProtoBuf.Visibility
|
||||
import org.jetbrains.kotlin.util.capitalizeDecapitalize.decapitalizeAsciiOnly
|
||||
import java.io.File
|
||||
import javax.annotation.processing.ProcessingEnvironment
|
||||
import javax.annotation.processing.Processor
|
||||
@@ -118,12 +120,12 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
|
||||
return true
|
||||
}
|
||||
|
||||
private fun processElement(element: Element): AdapterGenerator? {
|
||||
val metadata = element.kotlinMetadata
|
||||
private fun processElement(model: Element): AdapterGenerator? {
|
||||
val metadata = model.kotlinMetadata
|
||||
|
||||
if (metadata !is KotlinClassMetadata) {
|
||||
messager.printMessage(
|
||||
ERROR, "@JsonClass can't be applied to $element: must be a Kotlin class", element)
|
||||
ERROR, "@JsonClass can't be applied to $model: must be a Kotlin class", model)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -133,27 +135,28 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
|
||||
when {
|
||||
classProto.classKind != Class.Kind.CLASS -> {
|
||||
messager.printMessage(
|
||||
ERROR, "@JsonClass can't be applied to $element: must be a Kotlin class", element)
|
||||
ERROR, "@JsonClass can't be applied to $model: must be a Kotlin class", model)
|
||||
return null
|
||||
}
|
||||
classProto.isInnerClass -> {
|
||||
messager.printMessage(
|
||||
ERROR, "@JsonClass can't be applied to $element: must not be an inner class", element)
|
||||
ERROR, "@JsonClass can't be applied to $model: must not be an inner class",
|
||||
model)
|
||||
return null
|
||||
}
|
||||
classProto.modality == Modality.ABSTRACT -> {
|
||||
messager.printMessage(
|
||||
ERROR, "@JsonClass can't be applied to $element: must not be abstract", element)
|
||||
ERROR, "@JsonClass can't be applied to $model: must not be abstract", model)
|
||||
return null
|
||||
}
|
||||
classProto.visibility == Visibility.LOCAL -> {
|
||||
messager.printMessage(
|
||||
ERROR, "@JsonClass can't be applied to $element: must not be local", element)
|
||||
ERROR, "@JsonClass can't be applied to $model: must not be local", model)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
val typeName = element.asType().asTypeName()
|
||||
val typeName = model.asType().asTypeName()
|
||||
val className = when (typeName) {
|
||||
is ClassName -> typeName
|
||||
is ParameterizedTypeName -> typeName.rawType
|
||||
@@ -185,62 +188,106 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
|
||||
nameResolver.getString(it.name)
|
||||
}
|
||||
|
||||
// The compiler might emit methods just so it has a place to put annotations. Find these.
|
||||
val annotatedElements = mutableMapOf<Property, ExecutableElement>()
|
||||
for (enclosedElement in element.enclosedElements) {
|
||||
if (enclosedElement !is ExecutableElement) continue
|
||||
val property = classData.getPropertyOrNull(enclosedElement) ?: continue
|
||||
annotatedElements[property] = enclosedElement
|
||||
val annotationHolders = mutableMapOf<Property, ExecutableElement>()
|
||||
val fields = mutableMapOf<String, VariableElement>()
|
||||
val setters = mutableMapOf<String, ExecutableElement>()
|
||||
val getters = mutableMapOf<String, ExecutableElement>()
|
||||
for (element in model.enclosedElements) {
|
||||
if (element is VariableElement) {
|
||||
fields[element.name] = element
|
||||
} else if (element is ExecutableElement) {
|
||||
when {
|
||||
element.name.startsWith("get") -> {
|
||||
getters[element.name.substring("get".length).decapitalizeAsciiOnly()] = element
|
||||
}
|
||||
element.name.startsWith("is") -> {
|
||||
getters[element.name.substring("is".length).decapitalizeAsciiOnly()] = element
|
||||
}
|
||||
element.name.startsWith("set") -> {
|
||||
setters[element.name.substring("set".length).decapitalizeAsciiOnly()] = element
|
||||
}
|
||||
}
|
||||
|
||||
val property = classData.getPropertyOrNull(element)
|
||||
if (property != null) {
|
||||
annotationHolders[property] = element
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val propertyGenerators = mutableListOf<PropertyGenerator>()
|
||||
for (enclosedElement in element.enclosedElements) {
|
||||
if (enclosedElement !is VariableElement) continue
|
||||
val propertiesByName = mutableMapOf<String, PropertyGenerator>()
|
||||
for (property in properties.values) {
|
||||
val name = nameResolver.getString(property.name)
|
||||
|
||||
val fieldElement = fields[name]
|
||||
val setterElement = setters[name]
|
||||
val getterElement = getters[name]
|
||||
val element = fieldElement ?: setterElement ?: getterElement!!
|
||||
|
||||
val name = enclosedElement.simpleName.toString()
|
||||
val property = properties[name] ?: continue
|
||||
val parameter = parameters[name]
|
||||
|
||||
val parameterElement = if (parameter != null) {
|
||||
val parameterIndex = protoConstructor.valueParameterList.indexOf(parameter)
|
||||
constructor.parameters[parameterIndex]
|
||||
} else {
|
||||
null
|
||||
var parameterIndex: Int = -1
|
||||
var parameterElement: VariableElement? = null
|
||||
if (parameter != null) {
|
||||
parameterIndex = protoConstructor.valueParameterList.indexOf(parameter)
|
||||
parameterElement = constructor.parameters[parameterIndex]
|
||||
}
|
||||
|
||||
val annotatedElement = annotatedElements[property]
|
||||
val annotationHolder = annotationHolders[property]
|
||||
|
||||
if (property.visibility != Visibility.INTERNAL
|
||||
&& property.visibility != Visibility.PROTECTED
|
||||
&& property.visibility != Visibility.PUBLIC) {
|
||||
messager.printMessage(ERROR, "property $name is not visible", enclosedElement)
|
||||
messager.printMessage(ERROR, "property $name is not visible", element)
|
||||
return null
|
||||
}
|
||||
|
||||
val hasDefault = parameter?.declaresDefaultValue ?: true
|
||||
|
||||
if (Modifier.TRANSIENT in enclosedElement.modifiers) {
|
||||
if (Modifier.TRANSIENT in element.modifiers) {
|
||||
if (!hasDefault) {
|
||||
throw IllegalArgumentException("No default value for transient property $name")
|
||||
messager.printMessage(
|
||||
ERROR, "No default value for transient property $name", element)
|
||||
return null
|
||||
}
|
||||
continue
|
||||
continue // This property is transient and has a default value. Ignore it.
|
||||
}
|
||||
|
||||
if (!property.hasSetter && parameter == null) {
|
||||
continue // This property is not settable. Ignore it.
|
||||
}
|
||||
|
||||
val delegateKey = DelegateKey(
|
||||
property.returnType.asTypeName(nameResolver, classProto::getTypeParameter, true),
|
||||
jsonQualifiers(enclosedElement, annotatedElement, parameterElement))
|
||||
jsonQualifiers(element, annotationHolder, parameterElement))
|
||||
|
||||
propertyGenerators += PropertyGenerator(
|
||||
propertiesByName[name] = PropertyGenerator(
|
||||
delegateKey,
|
||||
name,
|
||||
jsonName(name, enclosedElement, annotatedElement, parameterElement),
|
||||
parameter != null,
|
||||
jsonName(name, element, annotationHolder, parameterElement),
|
||||
parameterIndex,
|
||||
hasDefault,
|
||||
property.returnType.asTypeName(nameResolver, classProto::getTypeParameter))
|
||||
}
|
||||
|
||||
for (parameterElement in constructor.parameters) {
|
||||
val name = parameterElement.name
|
||||
val valueParameter = parameters[name]!!
|
||||
if (properties[name] == null && !valueParameter.declaresDefaultValue) {
|
||||
messager.printMessage(
|
||||
ERROR, "No property for required constructor parameter $name", parameterElement)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Sort properties so that those with constructor parameters come first.
|
||||
propertyGenerators.sortBy { if (it.hasConstructorParameter) -1 else 1 }
|
||||
val propertyGenerators = propertiesByName.values.toMutableList()
|
||||
propertyGenerators.sortBy {
|
||||
if (it.hasConstructorParameter) {
|
||||
it.parameterIndex
|
||||
} else {
|
||||
Integer.MAX_VALUE
|
||||
}
|
||||
}
|
||||
|
||||
val genericTypeNames = classProto.typeParameterList
|
||||
.map {
|
||||
@@ -268,9 +315,9 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
|
||||
}
|
||||
|
||||
return AdapterGenerator(
|
||||
className,
|
||||
className = className,
|
||||
propertyList = propertyGenerators,
|
||||
originalElement = element,
|
||||
originalElement = model,
|
||||
hasCompanionObject = hasCompanionObject,
|
||||
visibility = classProto.visibility!!,
|
||||
genericTypeNames = genericTypeNames,
|
||||
@@ -280,18 +327,18 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
|
||||
|
||||
/** Returns the JsonQualifiers on the field and parameter of a property. */
|
||||
private fun jsonQualifiers(
|
||||
field: VariableElement,
|
||||
method: ExecutableElement?,
|
||||
element: Element,
|
||||
annotationHolder: ExecutableElement?,
|
||||
parameter: VariableElement?
|
||||
): Set<AnnotationMirror> {
|
||||
val fieldQualifiers = field.qualifiers
|
||||
val methodQualifiers = method.qualifiers
|
||||
val elementQualifiers = element.qualifiers
|
||||
val annotationHolderQualifiers = annotationHolder.qualifiers
|
||||
val parameterQualifiers = parameter.qualifiers
|
||||
|
||||
// TODO(jwilson): union the qualifiers somehow?
|
||||
return when {
|
||||
fieldQualifiers.isNotEmpty() -> fieldQualifiers
|
||||
methodQualifiers.isNotEmpty() -> methodQualifiers
|
||||
elementQualifiers.isNotEmpty() -> elementQualifiers
|
||||
annotationHolderQualifiers.isNotEmpty() -> annotationHolderQualifiers
|
||||
parameterQualifiers.isNotEmpty() -> parameterQualifiers
|
||||
else -> setOf()
|
||||
}
|
||||
@@ -300,28 +347,22 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
|
||||
/** Returns the @Json name of a property, or `propertyName` if none is provided. */
|
||||
private fun jsonName(
|
||||
propertyName: String,
|
||||
field: VariableElement,
|
||||
method: ExecutableElement?,
|
||||
element: Element,
|
||||
annotationHolder: ExecutableElement?,
|
||||
parameter: VariableElement?
|
||||
): String {
|
||||
val fieldJsonName = field.jsonName
|
||||
val methodJsonName = method.jsonName
|
||||
val fieldJsonName = element.jsonName
|
||||
val annotationHolderJsonName = annotationHolder.jsonName
|
||||
val parameterJsonName = parameter.jsonName
|
||||
|
||||
return when {
|
||||
fieldJsonName != null -> fieldJsonName
|
||||
methodJsonName != null -> methodJsonName
|
||||
annotationHolderJsonName != null -> annotationHolderJsonName
|
||||
parameterJsonName != null -> parameterJsonName
|
||||
else -> propertyName
|
||||
}
|
||||
}
|
||||
|
||||
private fun errorMustBeKotlinClass(element: Element) {
|
||||
messager.printMessage(ERROR,
|
||||
"@${JsonClass::class.java.simpleName} can't be applied to $element: must be a Kotlin class",
|
||||
element)
|
||||
}
|
||||
|
||||
private fun AdapterGenerator.generateAndWrite(generatedOption: TypeElement?) {
|
||||
val fileSpec = generateFile(generatedOption)
|
||||
val adapterName = fileSpec.members.filterIsInstance<TypeSpec>().first().name!!
|
||||
@@ -348,3 +389,8 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
|
||||
return getAnnotation(Json::class.java)?.name
|
||||
}
|
||||
}
|
||||
|
||||
private val Element.name: String
|
||||
get() {
|
||||
return simpleName.toString()
|
||||
}
|
@@ -25,7 +25,7 @@ internal class PropertyGenerator(
|
||||
val delegateKey: DelegateKey,
|
||||
val name: String,
|
||||
val serializedName: String,
|
||||
val hasConstructorParameter: Boolean,
|
||||
val parameterIndex: Int,
|
||||
val hasDefault: Boolean,
|
||||
val typeName: TypeName
|
||||
) {
|
||||
@@ -35,6 +35,9 @@ internal class PropertyGenerator(
|
||||
val isRequired
|
||||
get() = !delegateKey.nullable && !hasDefault
|
||||
|
||||
val hasConstructorParameter
|
||||
get() = parameterIndex != -1
|
||||
|
||||
/** We prefer to use 'null' to mean absent, but for some properties those are distinct. */
|
||||
val differentiateAbsentFromNull
|
||||
get() = delegateKey.nullable && hasDefault
|
||||
|
@@ -154,4 +154,38 @@ class CompilerTest {
|
||||
assertThat(result.systemErr).contains(
|
||||
"error: @JsonClass can't be applied to expression\$annotations(): must be a Kotlin class")
|
||||
}
|
||||
|
||||
@Test fun requiredTransientConstructorParameterFails() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodeGenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|class RequiredTransientConstructorParameter(@Transient var a: Int)
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains(
|
||||
"error: No default value for transient property a")
|
||||
}
|
||||
|
||||
@Test fun nonPropertyConstructorParameter() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodeGenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|class NonPropertyConstructorParameter(a: Int, val b: Int)
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains(
|
||||
"error: No property for required constructor parameter a")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user