Finish extraction of PropertyGenerator to no-elements/no-kotlin-metadata

This commit is contained in:
Zac Sweers 2019-02-24 22:59:34 -05:00
parent be6c7c190c
commit 98c7d7e71c
7 changed files with 230 additions and 191 deletions

View file

@ -30,6 +30,7 @@ import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.TypeElement
import javax.lang.model.element.VariableElement
import javax.tools.Diagnostic.Kind.ERROR
/**
@ -111,7 +112,7 @@ class JsonClassCodegenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
for ((name, parameter) in type.constructor.parameters) {
if (type.properties[parameter.name] == null && !parameter.hasDefault) {
messager.printMessage(
ERROR, "No property for required constructor parameter $name" /*, parameter.element // TODO how do we pass this? */)
ERROR, "No property for required constructor parameter $name", parameter.tag<VariableElement>())
return null
}
}

View file

@ -1,158 +0,0 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import com.google.auto.common.AnnotationMirrors
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.TypeName
import com.squareup.moshi.Json
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.kotlin.codegen.api.DelegateKey
import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator
import com.squareup.moshi.kotlin.codegen.api.TargetParameter
import javax.annotation.processing.Messager
import javax.lang.model.element.AnnotationMirror
import javax.lang.model.element.Element
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier
import javax.lang.model.element.VariableElement
import javax.tools.Diagnostic
/** A property in user code that maps to JSON. */
internal data class TargetProperty constructor(
val name: String,
val type: TypeName,
private val parameter: TargetParameter?,
private val annotationHolder: ExecutableElement?,
private val field: VariableElement?,
private val setter: ExecutableElement?,
private val getter: ExecutableElement?,
private val visibility: KModifier
) {
val parameterIndex get() = parameter?.index ?: -1
val hasDefault get() = parameter?.hasDefault ?: true
private val isTransient get() = field != null && Modifier.TRANSIENT in field.modifiers
private val element get() = field ?: setter ?: getter!!
private val isSettable get() = setter != null || parameter != null
private val isVisible: Boolean
get() {
return visibility == KModifier.INTERNAL
|| visibility == KModifier.PROTECTED
|| visibility == KModifier.PUBLIC
}
/**
* Returns a generator for this property, or null if either there is an error and this property
* cannot be used with code gen, or if no codegen is necessary for this property.
*/
fun generator(messager: Messager): PropertyGenerator? {
if (isTransient) {
if (!hasDefault) {
messager.printMessage(
Diagnostic.Kind.ERROR, "No default value for transient property ${this}", element)
return null
}
return null // This property is transient and has a default value. Ignore it.
}
if (!isVisible) {
messager.printMessage(Diagnostic.Kind.ERROR, "property ${this} is not visible", element)
return null
}
if (!isSettable) {
return null // This property is not settable. Ignore it.
}
val jsonQualifierMirrors = jsonQualifiers()
// TODO Can we check this after? Should we move this check to jsonQualifiers?
// for (jsonQualifier in jsonQualifierMirrors) {
// // Check Java types since that covers both Java and Kotlin annotations.
// annotationElement.getAnnotation(Retention::class.java)?.let {
// if (it.value != RetentionPolicy.RUNTIME) {
// messager.printMessage(Diagnostic.Kind.ERROR,
// "JsonQualifier @${jsonQualifier.simpleName} must have RUNTIME retention")
// }
// }
// annotationElement.getAnnotation(Target::class.java)?.let {
// if (ElementType.FIELD !in it.value) {
// messager.printMessage(Diagnostic.Kind.ERROR,
// "JsonQualifier @${jsonQualifier.simpleName} must support FIELD target")
// }
// }
// }
val jsonQualifierSpecs = jsonQualifierMirrors.map {
it.toBuilder()
.useSiteTarget(AnnotationSpec.UseSiteTarget.FIELD)
.build()
}
return PropertyGenerator(this,
DelegateKey(type, jsonQualifierSpecs))
}
/** Returns the JsonQualifiers on the field and parameter of this property. */
private fun jsonQualifiers(): Set<AnnotationSpec> {
val elementQualifiers = element.qualifiers
val annotationHolderQualifiers = annotationHolder.qualifiers
val parameterQualifiers = parameter?.qualifiers.orEmpty()
// TODO(jwilson): union the qualifiers somehow?
return when {
elementQualifiers.isNotEmpty() -> elementQualifiers.mapTo(mutableSetOf()) { AnnotationSpec.get(it) }
annotationHolderQualifiers.isNotEmpty() -> annotationHolderQualifiers.mapTo(mutableSetOf()) { AnnotationSpec.get(it) }
parameterQualifiers.isNotEmpty() -> parameterQualifiers
else -> setOf()
}
}
private val Element?.qualifiers: Set<AnnotationMirror>
get() {
if (this == null) return setOf()
return AnnotationMirrors.getAnnotatedAnnotations(this, JsonQualifier::class.java)
}
/** Returns the @Json name of this property, or this property's name if none is provided. */
fun jsonName(): String {
val fieldJsonName = element.jsonName
val annotationHolderJsonName = annotationHolder.jsonName
val parameterJsonName = parameter?.jsonName
return when {
fieldJsonName != null -> fieldJsonName
annotationHolderJsonName != null -> annotationHolderJsonName
parameterJsonName != null -> parameterJsonName
else -> name
}
}
// private val AnnotationMirror.simpleName: Name
// get() = MoreTypes.asTypeElement(annotationType).simpleName!!
override fun toString() = name
}
internal val Element?.jsonName: String?
get() {
if (this == null) return null
return getAnnotation(Json::class.java)?.name
}

View file

@ -18,7 +18,6 @@ package com.squareup.moshi.kotlin.codegen.api
import com.squareup.kotlinpoet.BOOLEAN
import com.squareup.kotlinpoet.NameAllocator
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.moshi.kotlin.codegen.TargetProperty
/** Generates functions to encode and decode a property as JSON. */
internal class PropertyGenerator(
@ -26,7 +25,7 @@ internal class PropertyGenerator(
val delegateKey: DelegateKey
) {
val name = target.name
val jsonName = target.jsonName()
val jsonName = target.jsonName
val hasDefault = target.hasDefault
lateinit var localName: String

View file

@ -16,6 +16,7 @@
package com.squareup.moshi.kotlin.codegen.api
import com.squareup.kotlinpoet.AnnotationSpec
import kotlin.reflect.KClass
/** A parameter in user code that should be populated by generated code. */
internal data class TargetParameter(
@ -23,5 +24,15 @@ internal data class TargetParameter(
val index: Int,
val hasDefault: Boolean,
val jsonName: String = name,
val qualifiers: Set<AnnotationSpec>? = null
)
val qualifiers: Set<AnnotationSpec>? = null,
private val tags: Map<KClass<*>, Any>
) {
/** Returns the tag attached with [type] as a key, or null if no tag is attached with that key. */
fun <T : Any> tag(type: KClass<out T>): T? {
@Suppress("UNCHECKED_CAST")
return tags[type] as T?
}
/** Returns the tag attached with [T] as a key, or null if no tag is attached with that key. */
inline fun <reified T : Any> tag(): T? = tag(T::class)
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen.api
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeName
/** A property in user code that maps to JSON. */
internal data class TargetProperty(
val name: String,
val type: TypeName,
val parameter: TargetParameter?,
val annotationHolder: FunSpec?,
val field: PropertySpec?,
val setter: FunSpec?,
val getter: FunSpec?,
val visibility: KModifier,
val jsonName: String
) {
val parameterIndex get() = parameter?.index ?: -1
val hasDefault get() = parameter?.hasDefault ?: true
override fun toString() = name
}

View file

@ -18,7 +18,6 @@ package com.squareup.moshi.kotlin.codegen.api
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.moshi.kotlin.codegen.TargetProperty
/** A user type that should be decoded and encoded by generated code. */
internal data class TargetType(

View file

@ -15,6 +15,8 @@
*/
package com.squareup.moshi.kotlin.codegen
import com.google.auto.common.AnnotationMirrors
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.KModifier.PUBLIC
@ -25,10 +27,18 @@ import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName
import com.squareup.moshi.Json
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.kotlin.codegen.api.DelegateKey
import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator
import com.squareup.moshi.kotlin.codegen.api.TargetConstructor
import com.squareup.moshi.kotlin.codegen.api.TargetParameter
import com.squareup.moshi.kotlin.codegen.api.TargetProperty
import com.squareup.moshi.kotlin.codegen.api.TargetType
import com.squareup.moshi.kotlin.codegen.api.TypeResolver
import com.squareup.moshi.kotlin.codegen.api.asAnnotationSpec
import com.squareup.moshi.kotlin.codegen.api.asFunSpec
import com.squareup.moshi.kotlin.codegen.api.asPropertySpec
import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
import me.eugeniomarletti.kotlin.metadata.KotlinMetadata
import me.eugeniomarletti.kotlin.metadata.classKind
@ -54,7 +64,12 @@ import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PR
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.deserialization.NameResolver
import me.eugeniomarletti.kotlin.metadata.shadow.util.capitalizeDecapitalize.decapitalizeAsciiOnly
import me.eugeniomarletti.kotlin.metadata.visibility
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
import javax.annotation.processing.Messager
import javax.lang.model.element.AnnotationMirror
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.ExecutableElement
@ -62,9 +77,10 @@ import javax.lang.model.element.TypeElement
import javax.lang.model.element.VariableElement
import javax.lang.model.util.Elements
import javax.lang.model.util.Types
import javax.tools.Diagnostic
import javax.tools.Diagnostic.Kind.ERROR
internal fun TypeParameter.asTypeName(
private fun TypeParameter.asTypeName(
nameResolver: NameResolver,
getTypeParameter: (index: Int) -> TypeParameter,
resolveAliases: Boolean = false
@ -84,7 +100,7 @@ internal fun TypeParameter.asTypeName(
}
}
internal fun TypeParameter.Variance.asKModifier(): KModifier? {
private fun TypeParameter.Variance.asKModifier(): KModifier? {
return when (this) {
Variance.IN -> KModifier.IN
Variance.OUT -> KModifier.OUT
@ -92,7 +108,7 @@ internal fun TypeParameter.Variance.asKModifier(): KModifier? {
}
}
internal fun Visibility?.asKModifier(): KModifier {
private fun Visibility?.asKModifier(): KModifier {
return when (this) {
INTERNAL -> KModifier.INTERNAL
PRIVATE -> KModifier.PRIVATE
@ -112,7 +128,7 @@ internal fun Visibility?.asKModifier(): KModifier {
* @param [getTypeParameter] a function that returns the type parameter for the given index. **Only
* called if [ProtoBuf.Type.hasTypeParameter] is true!**
*/
internal fun Type.asTypeName(
private fun Type.asTypeName(
nameResolver: NameResolver,
getTypeParameter: (index: Int) -> TypeParameter,
useAbbreviatedType: Boolean = true
@ -173,7 +189,8 @@ internal fun Type.asTypeName(
return typeName.copy(nullable = nullable)
}
internal fun primaryConstructor(metadata: KotlinClassMetadata, elements: Elements): TargetConstructor {
internal fun primaryConstructor(metadata: KotlinClassMetadata,
elements: Elements): TargetConstructor {
val (nameResolver, classProto) = metadata.data
// TODO allow custom constructor
@ -202,8 +219,9 @@ internal fun primaryConstructor(metadata: KotlinClassMetadata, elements: Element
name = name,
index = index,
hasDefault = parameter.declaresDefaultValue,
jsonName = paramElement.jsonName ?: name
qualifiers = paramElement.qualifiers.mapTo(mutableSetOf(), AnnotationMirror::asAnnotationSpec),
jsonName = paramElement.jsonName ?: name,
tags = mapOf(VariableElement::class to paramElement)
)
}
@ -214,7 +232,10 @@ internal fun primaryConstructor(metadata: KotlinClassMetadata, elements: Element
private val OBJECT_CLASS = ClassName("java.lang", "Object")
/** Returns a target type for `element`, or null if it cannot be used with code gen. */
internal fun targetType(messager: Messager, elements: Elements, types: Types, element: Element): TargetType? {
internal fun targetType(messager: Messager,
elements: Elements,
types: Types,
element: Element): TargetType? {
val typeMetadata: KotlinMetadata? = element.kotlinMetadata
if (element !is TypeElement || typeMetadata !is KotlinClassMetadata) {
messager.printMessage(
@ -226,7 +247,9 @@ internal fun targetType(messager: Messager, elements: Elements, types: Types, el
when {
proto.classKind == Class.Kind.ENUM_CLASS -> {
messager.printMessage(
ERROR, "@JsonClass with 'generateAdapter = \"true\"' can't be applied to $element: code gen for enums is not supported or necessary", element)
ERROR,
"@JsonClass with 'generateAdapter = \"true\"' can't be applied to $element: code gen for enums is not supported or necessary",
element)
return null
}
proto.classKind != Class.Kind.CLASS -> {
@ -337,37 +360,66 @@ private fun declaredProperties(
val type = typeResolver.resolve(property.returnType.asTypeName(
nameResolver, classProto::getTypeParameter, false))
val parameter = constructor.parameters[name]
val fieldElement = fields[name]
val annotationHolder = annotationHolders[name]
// Used for setter/getter/is lookups. Guaranteed to be safe because kotlin doesn't allow you to
// have both "AAA" and "aAA".
val decapitalizedName = name.decapitalizeAsciiOnly()
result[name] = TargetProperty(name = name,
type = type,
parameter = parameter,
annotationHolder = annotationHolders[name],
field = fields[name],
setter = setters[name],
getter = getters[name],
visibility = property.visibility.asKModifier()
annotationHolder = annotationHolder?.asFunSpec(),
field = fieldElement?.asPropertySpec(),
setter = setters[decapitalizedName]?.asFunSpec(),
getter = getters[decapitalizedName]?.asFunSpec(),
visibility = property.visibility.asKModifier(),
jsonName = jsonName(
fieldElement = fieldElement,
parameter = parameter,
annotationHolder = annotationHolder,
name = name
)
)
}
return result
}
/** Returns the @Json name of this property, or this property's name if none is provided. */
private fun jsonName(
fieldElement: Element?,
parameter: TargetParameter?,
annotationHolder: ExecutableElement?,
name: String): String {
val fieldJsonName = fieldElement.jsonName
val annotationHolderJsonName = annotationHolder.jsonName
val parameterJsonName = parameter?.jsonName
return when {
fieldJsonName != null -> fieldJsonName
annotationHolderJsonName != null -> annotationHolderJsonName
parameterJsonName != null -> parameterJsonName
else -> name
}
}
private val Element.name get() = simpleName.toString()
private fun genericTypeNames(proto: Class, nameResolver: NameResolver): List<TypeVariableName> {
return proto.typeParameterList.map {
val possibleBounds = it.upperBoundList
return proto.typeParameterList.map { typeParameter ->
val possibleBounds = typeParameter.upperBoundList
.map { it.asTypeName(nameResolver, proto::getTypeParameter, false) }
val typeVar = if (possibleBounds.isEmpty()) {
TypeVariableName(
name = nameResolver.getString(it.name),
variance = it.varianceModifier)
name = nameResolver.getString(typeParameter.name),
variance = typeParameter.varianceModifier)
} else {
TypeVariableName(
name = nameResolver.getString(it.name),
name = nameResolver.getString(typeParameter.name),
bounds = *possibleBounds.toTypedArray(),
variance = it.varianceModifier)
variance = typeParameter.varianceModifier)
}
return@map typeVar.copy(reified = it.reified)
return@map typeVar.copy(reified = typeParameter.reified)
}
}
@ -382,3 +434,99 @@ private val TypeParameter.varianceModifier: KModifier?
}
}
}
private val TargetProperty.isTransient get() = field != null && field.annotations.any { it.className == Transient::class.asClassName() }
private val TargetProperty.isSettable get() = setter != null || parameter != null
private val TargetProperty.isVisible: Boolean
get() {
return visibility == KModifier.INTERNAL
|| visibility == KModifier.PROTECTED
|| visibility == KModifier.PUBLIC
}
/**
* Returns a generator for this property, or null if either there is an error and this property
* cannot be used with code gen, or if no codegen is necessary for this property.
*/
internal fun TargetProperty.generator(messager: Messager): PropertyGenerator? {
val element = field?.tag<VariableElement>() ?: setter?.tag<ExecutableElement>()
?: getter!!.tag<ExecutableElement>()
if (isTransient) {
if (!hasDefault) {
element?.let {
messager.printMessage(
Diagnostic.Kind.ERROR, "No default value for transient property ${this}",
it)
}
return null
}
return null // This property is transient and has a default value. Ignore it.
}
if (!isVisible) {
element?.let {
messager.printMessage(Diagnostic.Kind.ERROR, "property ${this} is not visible",
it)
}
return null
}
if (!isSettable) {
return null // This property is not settable. Ignore it.
}
val jsonQualifierMirrors = jsonQualifiers(element)
for (jsonQualifier in jsonQualifierMirrors) {
// Check Java types since that covers both Java and Kotlin annotations.
val annotationElement = jsonQualifier.tag<TypeElement>() ?: continue
annotationElement.getAnnotation(Retention::class.java)?.let {
if (it.value != RetentionPolicy.RUNTIME) {
messager.printMessage(Diagnostic.Kind.ERROR,
"JsonQualifier @${jsonQualifier.className.simpleName} must have RUNTIME retention")
}
}
annotationElement.getAnnotation(Target::class.java)?.let {
if (ElementType.FIELD !in it.value) {
messager.printMessage(Diagnostic.Kind.ERROR,
"JsonQualifier @${jsonQualifier.className.simpleName} must support FIELD target")
}
}
}
val jsonQualifierSpecs = jsonQualifierMirrors.map {
it.toBuilder()
.useSiteTarget(AnnotationSpec.UseSiteTarget.FIELD)
.build()
}
return PropertyGenerator(this,
DelegateKey(type, jsonQualifierSpecs))
}
/** Returns the JsonQualifiers on the field and parameter of this property. */
private fun TargetProperty.jsonQualifiers(element: Element?): Set<AnnotationSpec> {
val elementQualifiers = element.qualifiers
val annotationHolderQualifiers = annotationHolder?.tag<ExecutableElement>().qualifiers
val parameterQualifiers = parameter?.qualifiers.orEmpty()
// TODO(jwilson): union the qualifiers somehow?
return when {
elementQualifiers.isNotEmpty() -> elementQualifiers.mapTo(mutableSetOf(), AnnotationMirror::asAnnotationSpec)
annotationHolderQualifiers.isNotEmpty() -> annotationHolderQualifiers.mapTo(
mutableSetOf(), AnnotationMirror::asAnnotationSpec)
parameterQualifiers.isNotEmpty() -> parameterQualifiers
else -> setOf()
}
}
private val Element?.qualifiers: Set<AnnotationMirror>
get() {
if (this == null) return setOf()
return AnnotationMirrors.getAnnotatedAnnotations(this, JsonQualifier::class.java)
}
private val Element?.jsonName: String?
get() {
if (this == null) return null
return getAnnotation(Json::class.java)?.name
}