Decouple code gen API from apt and kotlin-metadata (#816)

* Decouple AdapterGenerator from kotlin-metadata

* Decouple AdapterGenerator from javax elements API

* Extract checkIsVisibility helper

* Remove elements and kotlinmetadata from property, constructor, type

* Extract kotlinpoet-only parts to new API package for better separation

* Add mirroring APIs

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

* Update for new tag API in KotlinPoet

* Update for new tag API in KotlinPoet

* Move rest of elements utils out of API

We should split this up better as metadata.kt has become a dumping ground

* Move type handling to JsonClassCodegenProcessor

* Rebase fix

* Opportunistic idiomatic require()
This commit is contained in:
Zac Sweers
2019-09-08 21:24:10 -04:00
committed by GitHub
parent 5a4530b7d9
commit ea300997a1
17 changed files with 713 additions and 506 deletions

View File

@@ -18,6 +18,7 @@ package com.squareup.moshi.kotlin.codegen
import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asTypeName import com.squareup.kotlinpoet.asTypeName
import com.squareup.moshi.kotlin.codegen.api.TypeResolver
import javax.lang.model.element.TypeElement import javax.lang.model.element.TypeElement
import javax.lang.model.type.DeclaredType import javax.lang.model.type.DeclaredType
import javax.lang.model.util.Types import javax.lang.model.util.Types

View File

@@ -16,9 +16,11 @@
package com.squareup.moshi.kotlin.codegen package com.squareup.moshi.kotlin.codegen
import com.google.auto.service.AutoService import com.google.auto.service.AutoService
import com.squareup.kotlinpoet.asClassName
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.squareup.moshi.kotlin.codegen.api.AdapterGenerator
import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator
import me.eugeniomarletti.kotlin.metadata.KotlinMetadataUtils import me.eugeniomarletti.kotlin.metadata.KotlinMetadataUtils
import me.eugeniomarletti.kotlin.metadata.declaresDefaultValue
import me.eugeniomarletti.kotlin.processing.KotlinAbstractProcessor import me.eugeniomarletti.kotlin.processing.KotlinAbstractProcessor
import net.ltgt.gradle.incap.IncrementalAnnotationProcessor import net.ltgt.gradle.incap.IncrementalAnnotationProcessor
import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType.ISOLATING import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType.ISOLATING
@@ -28,6 +30,7 @@ import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion import javax.lang.model.SourceVersion
import javax.lang.model.element.Element import javax.lang.model.element.Element
import javax.lang.model.element.TypeElement import javax.lang.model.element.TypeElement
import javax.lang.model.element.VariableElement
import javax.tools.Diagnostic.Kind.ERROR import javax.tools.Diagnostic.Kind.ERROR
/** /**
@@ -74,9 +77,9 @@ class JsonClassCodegenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
override fun init(processingEnv: ProcessingEnvironment) { override fun init(processingEnv: ProcessingEnvironment) {
super.init(processingEnv) super.init(processingEnv)
generatedType = processingEnv.options[OPTION_GENERATED]?.let { generatedType = processingEnv.options[OPTION_GENERATED]?.let {
if (it !in POSSIBLE_GENERATED_NAMES) { require(it in POSSIBLE_GENERATED_NAMES) {
throw IllegalArgumentException("Invalid option value for $OPTION_GENERATED. Found $it, " + "Invalid option value for $OPTION_GENERATED. Found $it, " +
"allowable values are $POSSIBLE_GENERATED_NAMES.") "allowable values are $POSSIBLE_GENERATED_NAMES."
} }
processingEnv.elementUtils.getTypeElement(it) processingEnv.elementUtils.getTypeElement(it)
} }
@@ -87,7 +90,12 @@ class JsonClassCodegenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
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) ?: continue val generator = adapterGenerator(type) ?: continue
generator.generateFile(generatedType) generator
.generateFile(generatedType?.asClassName()) {
it.toBuilder()
.addOriginatingElement(type)
.build()
}
.writeTo(filer) .writeTo(filer)
} }
} }
@@ -96,7 +104,7 @@ class JsonClassCodegenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
} }
private fun adapterGenerator(element: Element): AdapterGenerator? { private fun adapterGenerator(element: Element): AdapterGenerator? {
val type = TargetType.get(messager, elementUtils, typeUtils, element) ?: return null val type = targetType(messager, elementUtils, typeUtils, element) ?: return null
val properties = mutableMapOf<String, PropertyGenerator>() val properties = mutableMapOf<String, PropertyGenerator>()
for (property in type.properties.values) { for (property in type.properties.values) {
@@ -107,9 +115,9 @@ class JsonClassCodegenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
} }
for ((name, parameter) in type.constructor.parameters) { for ((name, parameter) in type.constructor.parameters) {
if (type.properties[parameter.name] == null && !parameter.proto.declaresDefaultValue) { if (type.properties[parameter.name] == null && !parameter.hasDefault) {
messager.printMessage( messager.printMessage(
ERROR, "No property for required constructor parameter $name", parameter.element) ERROR, "No property for required constructor parameter $name", parameter.tag<VariableElement>())
return null return null
} }
} }

View File

@@ -1,63 +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 me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
import me.eugeniomarletti.kotlin.metadata.isPrimary
import me.eugeniomarletti.kotlin.metadata.jvm.getJvmConstructorSignature
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Constructor
import javax.lang.model.element.ElementKind
import javax.lang.model.element.ExecutableElement
import javax.lang.model.util.Elements
/** A constructor in user code that should be called by generated code. */
internal data class TargetConstructor(
val element: ExecutableElement,
val proto: Constructor,
val parameters: Map<String, TargetParameter>
) {
companion object {
fun primary(metadata: KotlinClassMetadata, elements: Elements): TargetConstructor {
val (nameResolver, classProto) = metadata.data
// todo allow custom constructor
val proto = classProto.constructorList
.single { it.isPrimary }
val constructorJvmSignature = proto.getJvmConstructorSignature(
nameResolver, classProto.typeTable)
val element = classProto.fqName
.let(nameResolver::getString)
.replace('/', '.')
.let(elements::getTypeElement)
.enclosedElements
.mapNotNull {
it.takeIf { it.kind == ElementKind.CONSTRUCTOR }?.let { it as ExecutableElement }
}
.first()
// TODO Temporary until JVM method signature matching is better
// .single { it.jvmMethodSignature == constructorJvmSignature }
val parameters = mutableMapOf<String, TargetParameter>()
for (parameter in proto.valueParameterList) {
val name = nameResolver.getString(parameter.name)
val index = proto.valueParameterList.indexOf(parameter)
parameters[name] = TargetParameter(name, parameter, index, element.parameters[index])
}
return TargetConstructor(element, proto, parameters)
}
}
}

View File

@@ -1,167 +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.google.auto.common.MoreTypes
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.TypeName
import com.squareup.moshi.Json
import com.squareup.moshi.JsonQualifier
import me.eugeniomarletti.kotlin.metadata.declaresDefaultValue
import me.eugeniomarletti.kotlin.metadata.hasSetter
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Property
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.INTERNAL
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PROTECTED
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PUBLIC
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.ExecutableElement
import javax.lang.model.element.Modifier
import javax.lang.model.element.Name
import javax.lang.model.element.VariableElement
import javax.tools.Diagnostic
/** A property in user code that maps to JSON. */
internal data class TargetProperty(
val name: String,
val type: TypeName,
private val proto: Property,
private val parameter: TargetParameter?,
private val annotationHolder: ExecutableElement?,
private val field: VariableElement?,
private val setter: ExecutableElement?,
private val getter: ExecutableElement?
) {
val parameterIndex get() = parameter?.index ?: -1
val hasDefault get() = parameter?.proto?.declaresDefaultValue ?: true
private val isTransient get() = field != null && Modifier.TRANSIENT in field.modifiers
private val element get() = field ?: setter ?: getter!!
private val isSettable get() = proto.hasSetter || parameter != null
private val isVisible: Boolean
get() {
return proto.visibility == INTERNAL
|| proto.visibility == PROTECTED
|| proto.visibility == 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 PropertyGenerator(this, DelegateKey(type, emptyList()), true)
}
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()
for (jsonQualifier in jsonQualifierMirrors) {
// Check Java types since that covers both Java and Kotlin annotations.
val annotationElement = MoreTypes.asTypeElement(jsonQualifier.annotationType)
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 {
AnnotationSpec.get(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<AnnotationMirror> {
val elementQualifiers = element.qualifiers
val annotationHolderQualifiers = annotationHolder.qualifiers
val parameterQualifiers = parameter?.element.qualifiers
// TODO(jwilson): union the qualifiers somehow?
return when {
elementQualifiers.isNotEmpty() -> elementQualifiers
annotationHolderQualifiers.isNotEmpty() -> annotationHolderQualifiers
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?.element.jsonName
return when {
fieldJsonName != null -> fieldJsonName
annotationHolderJsonName != null -> annotationHolderJsonName
parameterJsonName != null -> parameterJsonName
else -> name
}
}
private val Element?.jsonName: String?
get() {
if (this == null) return null
return getAnnotation(Json::class.java)?.name
}
private val AnnotationMirror.simpleName: Name
get() = MoreTypes.asTypeElement(annotationType).simpleName!!
override fun toString() = name
}

View File

@@ -1,229 +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.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName
import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
import me.eugeniomarletti.kotlin.metadata.KotlinMetadata
import me.eugeniomarletti.kotlin.metadata.classKind
import me.eugeniomarletti.kotlin.metadata.getPropertyOrNull
import me.eugeniomarletti.kotlin.metadata.isInnerClass
import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
import me.eugeniomarletti.kotlin.metadata.modality
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Class
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Modality.ABSTRACT
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.TypeParameter
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.INTERNAL
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.LOCAL
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PUBLIC
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 javax.annotation.processing.Messager
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.ExecutableElement
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.Kind.ERROR
/** A user type that should be decoded and encoded by generated code. */
internal data class TargetType(
val proto: Class,
val element: TypeElement,
val constructor: TargetConstructor,
val properties: Map<String, TargetProperty>,
val typeVariables: List<TypeVariableName>
) {
val name = element.className
companion object {
private val OBJECT_CLASS = ClassName("java.lang", "Object")
/** Returns a target type for `element`, or null if it cannot be used with code gen. */
fun get(messager: Messager, elements: Elements, types: Types, element: Element): TargetType? {
val typeMetadata: KotlinMetadata? = element.kotlinMetadata
if (element !is TypeElement || typeMetadata !is KotlinClassMetadata) {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must be a Kotlin class", element)
return null
}
val proto = typeMetadata.data.classProto
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)
return null
}
proto.classKind != Class.Kind.CLASS -> {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must be a Kotlin class", element)
return null
}
proto.isInnerClass -> {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must not be an inner class", element)
return null
}
proto.modality == ABSTRACT -> {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must not be abstract", element)
return null
}
proto.visibility == LOCAL -> {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must not be local", element)
return null
}
}
val typeVariables = genericTypeNames(proto, typeMetadata.data.nameResolver)
val appliedType = AppliedType.get(element)
val constructor = TargetConstructor.primary(typeMetadata, elements)
if (constructor.proto.visibility != INTERNAL && constructor.proto.visibility != PUBLIC) {
messager.printMessage(ERROR, "@JsonClass can't be applied to $element: " +
"primary constructor is not internal or public", element)
return null
}
val properties = mutableMapOf<String, TargetProperty>()
for (supertype in appliedType.supertypes(types)) {
if (supertype.element.asClassName() == OBJECT_CLASS) {
continue // Don't load properties for java.lang.Object.
}
if (supertype.element.kind != ElementKind.CLASS) {
continue // Don't load properties for interface types.
}
if (supertype.element.kotlinMetadata == null) {
messager.printMessage(ERROR,
"@JsonClass can't be applied to $element: supertype $supertype is not a Kotlin type",
element)
return null
}
val supertypeProperties = declaredProperties(
supertype.element, supertype.resolver, constructor)
for ((name, property) in supertypeProperties) {
properties.putIfAbsent(name, property)
}
}
return TargetType(proto, element, constructor, properties, typeVariables)
}
/** Returns the properties declared by `typeElement`. */
private fun declaredProperties(
typeElement: TypeElement,
typeResolver: TypeResolver,
constructor: TargetConstructor
): Map<String, TargetProperty> {
val typeMetadata: KotlinClassMetadata = typeElement.kotlinMetadata as KotlinClassMetadata
val nameResolver = typeMetadata.data.nameResolver
val classProto = typeMetadata.data.classProto
val annotationHolders = mutableMapOf<String, ExecutableElement>()
val fields = mutableMapOf<String, VariableElement>()
val setters = mutableMapOf<String, ExecutableElement>()
val getters = mutableMapOf<String, ExecutableElement>()
for (element in typeElement.enclosedElements) {
if (element is VariableElement) {
fields[element.name] = element
} else if (element is ExecutableElement) {
when {
element.name.startsWith("get") -> {
val name = element.name.substring("get".length).decapitalizeAsciiOnly()
getters[name] = element
}
element.name.startsWith("is") -> {
val name = element.name.substring("is".length).decapitalizeAsciiOnly()
getters[name] = element
}
element.name.startsWith("set") -> {
val name = element.name.substring("set".length).decapitalizeAsciiOnly()
setters[name] = element
}
}
val propertyProto = typeMetadata.data.getPropertyOrNull(element)
if (propertyProto != null) {
val name = nameResolver.getString(propertyProto.name)
annotationHolders[name] = element
}
}
}
val result = mutableMapOf<String, TargetProperty>()
for (property in classProto.propertyList) {
val name = nameResolver.getString(property.name)
val type = typeResolver.resolve(property.returnType.asTypeName(
nameResolver, classProto::getTypeParameter, false))
result[name] = TargetProperty(name, type, property, constructor.parameters[name],
annotationHolders[name], fields[name], setters[name], getters[name])
}
return result
}
private val Element.className: ClassName
get() {
val typeName = asType().asTypeName()
return when (typeName) {
is ClassName -> typeName
is ParameterizedTypeName -> typeName.rawType
else -> throw IllegalStateException("unexpected TypeName: ${typeName::class}")
}
}
private val Element.name get() = simpleName.toString()
private fun genericTypeNames(proto: Class, nameResolver: NameResolver): List<TypeVariableName> {
return proto.typeParameterList.map {
val possibleBounds = it.upperBoundList
.map { it.asTypeName(nameResolver, proto::getTypeParameter, false) }
val typeVar = if (possibleBounds.isEmpty()) {
TypeVariableName(
name = nameResolver.getString(it.name),
variance = it.varianceModifier)
} else {
TypeVariableName(
name = nameResolver.getString(it.name),
bounds = *possibleBounds.toTypedArray(),
variance = it.varianceModifier)
}
return@map typeVar.copy(reified = it.reified)
}
}
private val TypeParameter.varianceModifier: KModifier?
get() {
return variance.asKModifier().let {
// We don't redeclare out variance here
if (it == KModifier.OUT) {
null
} else {
it
}
}
}
}
}

View File

@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.squareup.moshi.kotlin.codegen package com.squareup.moshi.kotlin.codegen.api
import com.squareup.kotlinpoet.ARRAY import com.squareup.kotlinpoet.ARRAY
import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.FunSpec
@@ -35,11 +36,9 @@ import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.internal.Util import com.squareup.moshi.internal.Util
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility
import me.eugeniomarletti.kotlin.metadata.visibility
import java.lang.reflect.Constructor import java.lang.reflect.Constructor
import com.squareup.moshi.kotlin.codegen.JsonClassCodegenProcessor
import java.lang.reflect.Type import java.lang.reflect.Type
import javax.lang.model.element.TypeElement
private val MOSHI_UTIL = Util::class.asClassName() private val MOSHI_UTIL = Util::class.asClassName()
@@ -49,14 +48,13 @@ internal class AdapterGenerator(
private val propertyList: List<PropertyGenerator> private val propertyList: List<PropertyGenerator>
) { ) {
private val nonTransientProperties = propertyList.filterNot { it.isTransient } private val nonTransientProperties = propertyList.filterNot { it.isTransient }
private val className = target.name private val className = target.typeName.rawType()
private val visibility = target.proto.visibility!! private val visibility = target.visibility
private val typeVariables = target.typeVariables private val typeVariables = target.typeVariables
private val nameAllocator = NameAllocator() private val nameAllocator = NameAllocator()
private val adapterName = "${className.simpleNames.joinToString(separator = "_")}JsonAdapter" private val adapterName = "${className.simpleNames.joinToString(separator = "_")}JsonAdapter"
private val originalElement = target.element private val originalTypeName = target.typeName
private val originalTypeName = target.element.asType().asTypeName()
private val moshiParam = ParameterSpec.builder( private val moshiParam = ParameterSpec.builder(
nameAllocator.newName("moshi"), nameAllocator.newName("moshi"),
@@ -98,23 +96,22 @@ internal class AdapterGenerator(
.initializer("null") .initializer("null")
.build() .build()
fun generateFile(generatedOption: TypeElement?): FileSpec { fun generateFile(generatedOption: ClassName?, typeHook: (TypeSpec) -> TypeSpec = { it }): FileSpec {
for (property in nonTransientProperties) { for (property in nonTransientProperties) {
property.allocateNames(nameAllocator) property.allocateNames(nameAllocator)
} }
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(generatedOption)) result.addType(generateType(generatedOption).let(typeHook))
return result.build() return result.build()
} }
private fun generateType(generatedOption: TypeElement?): TypeSpec { private fun generateType(generatedOption: ClassName?): TypeSpec {
val result = TypeSpec.classBuilder(adapterName) val result = TypeSpec.classBuilder(adapterName)
.addOriginatingElement(originalElement)
generatedOption?.let { generatedOption?.let {
result.addAnnotation(AnnotationSpec.builder(it.asClassName()) result.addAnnotation(AnnotationSpec.builder(it)
.addMember("value = [%S]", JsonClassCodegenProcessor::class.java.canonicalName) .addMember("value = [%S]", JsonClassCodegenProcessor::class.java.canonicalName)
.addMember("comments = %S", "https://github.com/square/moshi") .addMember("comments = %S", "https://github.com/square/moshi")
.build()) .build())
@@ -127,7 +124,7 @@ internal class AdapterGenerator(
} }
// TODO make this configurable. Right now it just matches the source model // TODO make this configurable. Right now it just matches the source model
if (visibility == Visibility.INTERNAL) { if (visibility == KModifier.INTERNAL) {
result.addModifiers(KModifier.INTERNAL) result.addModifiers(KModifier.INTERNAL)
} }

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.squareup.moshi.kotlin.codegen package com.squareup.moshi.kotlin.codegen.api
import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.ClassName

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.squareup.moshi.kotlin.codegen package com.squareup.moshi.kotlin.codegen.api
import com.squareup.kotlinpoet.BOOLEAN import com.squareup.kotlinpoet.BOOLEAN
import com.squareup.kotlinpoet.NameAllocator import com.squareup.kotlinpoet.NameAllocator
@@ -26,7 +26,7 @@ internal class PropertyGenerator(
val isTransient: Boolean = false val isTransient: Boolean = false
) { ) {
val name = target.name val name = target.name
val jsonName = target.jsonName() val jsonName = target.jsonName
val hasDefault = target.hasDefault val hasDefault = target.hasDefault
lateinit var localName: String lateinit var localName: String

View File

@@ -13,15 +13,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.squareup.moshi.kotlin.codegen package com.squareup.moshi.kotlin.codegen.api
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.ValueParameter import com.squareup.kotlinpoet.KModifier
import javax.lang.model.element.VariableElement
/** A parameter in user code that should be populated by generated code. */ /** A constructor in user code that should be called by generated code. */
internal data class TargetParameter( internal data class TargetConstructor(
val name: String, val parameters: Map<String, TargetParameter>,
val proto: ValueParameter, val visibility: KModifier
val index: Int, ) {
val element: VariableElement init {
) visibility.checkIsVisibility()
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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.AnnotationSpec
import kotlin.reflect.KClass
/** A parameter in user code that should be populated by generated code. */
internal data class TargetParameter(
val name: String,
val index: Int,
val hasDefault: Boolean,
val jsonName: String = name,
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

@@ -0,0 +1,35 @@
/*
* 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.KModifier
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
/** A user type that should be decoded and encoded by generated code. */
internal data class TargetType(
val typeName: TypeName,
val constructor: TargetConstructor,
val properties: Map<String, TargetProperty>,
val typeVariables: List<TypeVariableName>,
val isDataClass: Boolean,
val visibility: KModifier
) {
init {
visibility.checkIsVisibility()
}
}

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.squareup.moshi.kotlin.codegen package com.squareup.moshi.kotlin.codegen.api
import com.squareup.kotlinpoet.ARRAY import com.squareup.kotlinpoet.ARRAY
import com.squareup.kotlinpoet.BOOLEAN import com.squareup.kotlinpoet.BOOLEAN

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.squareup.moshi.kotlin.codegen package com.squareup.moshi.kotlin.codegen.api
import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.ParameterizedTypeName

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.squareup.moshi.kotlin.codegen package com.squareup.moshi.kotlin.codegen.api
import com.squareup.kotlinpoet.BOOLEAN import com.squareup.kotlinpoet.BOOLEAN
import com.squareup.kotlinpoet.BYTE import com.squareup.kotlinpoet.BYTE
@@ -23,6 +23,7 @@ import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.DOUBLE import com.squareup.kotlinpoet.DOUBLE
import com.squareup.kotlinpoet.FLOAT import com.squareup.kotlinpoet.FLOAT
import com.squareup.kotlinpoet.INT import com.squareup.kotlinpoet.INT
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.LONG import com.squareup.kotlinpoet.LONG
import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.SHORT import com.squareup.kotlinpoet.SHORT
@@ -51,3 +52,9 @@ internal fun TypeName.defaultPrimitiveValue(): CodeBlock =
UNIT, Void::class.asTypeName() -> throw IllegalStateException("Parameter with void or Unit type is illegal") UNIT, Void::class.asTypeName() -> throw IllegalStateException("Parameter with void or Unit type is illegal")
else -> CodeBlock.of("null") else -> CodeBlock.of("null")
} }
internal fun KModifier.checkIsVisibility() {
require(ordinal <= ordinal) {
"Visibility must be one of ${(0..ordinal).joinToString { KModifier.values()[it].name }}. Is $name"
}
}

View File

@@ -15,22 +15,90 @@
*/ */
package com.squareup.moshi.kotlin.codegen package com.squareup.moshi.kotlin.codegen
import com.google.auto.common.AnnotationMirrors
import com.google.auto.common.MoreTypes
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.KModifier.PUBLIC
import com.squareup.kotlinpoet.KModifier.VARARG
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.STAR import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName
import com.squareup.kotlinpoet.asTypeVariableName
import com.squareup.kotlinpoet.tag
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 me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
import me.eugeniomarletti.kotlin.metadata.KotlinMetadata
import me.eugeniomarletti.kotlin.metadata.classKind
import me.eugeniomarletti.kotlin.metadata.declaresDefaultValue
import me.eugeniomarletti.kotlin.metadata.getPropertyOrNull
import me.eugeniomarletti.kotlin.metadata.isDataClass
import me.eugeniomarletti.kotlin.metadata.isInnerClass
import me.eugeniomarletti.kotlin.metadata.isPrimary
import me.eugeniomarletti.kotlin.metadata.jvm.getJvmConstructorSignature
import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
import me.eugeniomarletti.kotlin.metadata.modality
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Class
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Modality.ABSTRACT
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Type import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Type
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.TypeParameter import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.TypeParameter
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.TypeParameter.Variance import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.TypeParameter.Variance
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.INTERNAL
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.LOCAL
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PRIVATE
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PRIVATE_TO_THIS
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PROTECTED
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.deserialization.NameResolver 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
import javax.lang.model.element.Modifier
import javax.lang.model.element.Modifier.DEFAULT
import javax.lang.model.element.Modifier.FINAL
import javax.lang.model.element.Modifier.NATIVE
import javax.lang.model.element.Modifier.STATIC
import javax.lang.model.element.Modifier.SYNCHRONIZED
import javax.lang.model.element.Modifier.TRANSIENT
import javax.lang.model.element.Modifier.VOLATILE
import javax.lang.model.element.TypeElement
import javax.lang.model.element.VariableElement
import javax.lang.model.type.TypeVariable
import javax.lang.model.util.Elements
import javax.lang.model.util.Types
import javax.tools.Diagnostic
import javax.tools.Diagnostic.Kind.ERROR
import kotlin.reflect.KClass
internal fun TypeParameter.asTypeName( private fun TypeParameter.asTypeName(
nameResolver: NameResolver, nameResolver: NameResolver,
getTypeParameter: (index: Int) -> TypeParameter, getTypeParameter: (index: Int) -> TypeParameter,
resolveAliases: Boolean = false resolveAliases: Boolean = false
): TypeVariableName { ): TypeVariableName {
val possibleBounds = upperBoundList.map { val possibleBounds = upperBoundList.map {
it.asTypeName(nameResolver, getTypeParameter, resolveAliases) it.asTypeName(nameResolver, getTypeParameter, resolveAliases)
@@ -47,7 +115,7 @@ internal fun TypeParameter.asTypeName(
} }
} }
internal fun TypeParameter.Variance.asKModifier(): KModifier? { private fun TypeParameter.Variance.asKModifier(): KModifier? {
return when (this) { return when (this) {
Variance.IN -> KModifier.IN Variance.IN -> KModifier.IN
Variance.OUT -> KModifier.OUT Variance.OUT -> KModifier.OUT
@@ -55,6 +123,18 @@ internal fun TypeParameter.Variance.asKModifier(): KModifier? {
} }
} }
private fun Visibility?.asKModifier(): KModifier {
return when (this) {
INTERNAL -> KModifier.INTERNAL
PRIVATE -> KModifier.PRIVATE
PROTECTED -> KModifier.PROTECTED
Visibility.PUBLIC -> KModifier.PUBLIC
PRIVATE_TO_THIS -> KModifier.PRIVATE
LOCAL -> KModifier.PRIVATE
else -> PUBLIC
}
}
/** /**
* Returns the TypeName of this type as it would be seen in the source code, including nullability * Returns the TypeName of this type as it would be seen in the source code, including nullability
* and generic type parameters. * and generic type parameters.
@@ -63,10 +143,10 @@ internal fun TypeParameter.Variance.asKModifier(): KModifier? {
* @param [getTypeParameter] a function that returns the type parameter for the given index. **Only * @param [getTypeParameter] a function that returns the type parameter for the given index. **Only
* called if [ProtoBuf.Type.hasTypeParameter] is true!** * called if [ProtoBuf.Type.hasTypeParameter] is true!**
*/ */
internal fun Type.asTypeName( private fun Type.asTypeName(
nameResolver: NameResolver, nameResolver: NameResolver,
getTypeParameter: (index: Int) -> TypeParameter, getTypeParameter: (index: Int) -> TypeParameter,
useAbbreviatedType: Boolean = true useAbbreviatedType: Boolean = true
): TypeName { ): TypeName {
val argumentList = when { val argumentList = when {
@@ -123,3 +203,462 @@ internal fun Type.asTypeName(
return typeName.copy(nullable = nullable) return typeName.copy(nullable = nullable)
} }
internal fun primaryConstructor(metadata: KotlinClassMetadata,
elements: Elements): TargetConstructor {
val (nameResolver, classProto) = metadata.data
// TODO allow custom constructor
val proto = classProto.constructorList
.single { it.isPrimary }
val constructorJvmSignature = proto.getJvmConstructorSignature(
nameResolver, classProto.typeTable)
val element = classProto.fqName
.let(nameResolver::getString)
.replace('/', '.')
.let(elements::getTypeElement)
.enclosedElements
.mapNotNull {
it.takeIf { it.kind == ElementKind.CONSTRUCTOR }?.let { it as ExecutableElement }
}
.first()
// TODO Temporary until JVM method signature matching is better
// .single { it.jvmMethodSignature == constructorJvmSignature }
val parameters = mutableMapOf<String, TargetParameter>()
for (parameter in proto.valueParameterList) {
val name = nameResolver.getString(parameter.name)
val index = proto.valueParameterList.indexOf(parameter)
val paramElement = element.parameters[index]
parameters[name] = TargetParameter(
name = name,
index = index,
hasDefault = parameter.declaresDefaultValue,
qualifiers = paramElement.qualifiers.mapTo(mutableSetOf(), AnnotationMirror::asAnnotationSpec),
jsonName = paramElement.jsonName ?: name,
tags = mapOf(VariableElement::class to paramElement)
)
}
return TargetConstructor(parameters,
proto.visibility.asKModifier())
}
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? {
val typeMetadata: KotlinMetadata? = element.kotlinMetadata
if (element !is TypeElement || typeMetadata !is KotlinClassMetadata) {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must be a Kotlin class", element)
return null
}
val proto = typeMetadata.data.classProto
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)
return null
}
proto.classKind != Class.Kind.CLASS -> {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must be a Kotlin class", element)
return null
}
proto.isInnerClass -> {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must not be an inner class", element)
return null
}
proto.modality == ABSTRACT -> {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must not be abstract", element)
return null
}
proto.visibility == LOCAL -> {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must not be local", element)
return null
}
}
val typeVariables = genericTypeNames(proto, typeMetadata.data.nameResolver)
val appliedType = AppliedType.get(element)
val constructor = primaryConstructor(typeMetadata, elements)
if (constructor.visibility != KModifier.INTERNAL && constructor.visibility != KModifier.PUBLIC) {
messager.printMessage(ERROR, "@JsonClass can't be applied to $element: " +
"primary constructor is not internal or public", element)
return null
}
val properties = mutableMapOf<String, TargetProperty>()
for (supertype in appliedType.supertypes(types)) {
if (supertype.element.asClassName() == OBJECT_CLASS) {
continue // Don't load properties for java.lang.Object.
}
if (supertype.element.kind != ElementKind.CLASS) {
continue // Don't load properties for interface types.
}
if (supertype.element.kotlinMetadata == null) {
messager.printMessage(ERROR,
"@JsonClass can't be applied to $element: supertype $supertype is not a Kotlin type",
element)
return null
}
val supertypeProperties = declaredProperties(
supertype.element, supertype.resolver, constructor)
for ((name, property) in supertypeProperties) {
properties.putIfAbsent(name, property)
}
}
return TargetType(
typeName = element.asType().asTypeName(),
constructor = constructor,
properties = properties,
typeVariables = typeVariables,
isDataClass = proto.isDataClass,
visibility = proto.visibility.asKModifier())
}
/** Returns the properties declared by `typeElement`. */
private fun declaredProperties(
typeElement: TypeElement,
typeResolver: TypeResolver,
constructor: TargetConstructor
): Map<String, TargetProperty> {
val typeMetadata: KotlinClassMetadata = typeElement.kotlinMetadata as KotlinClassMetadata
val nameResolver = typeMetadata.data.nameResolver
val classProto = typeMetadata.data.classProto
val annotationHolders = mutableMapOf<String, ExecutableElement>()
val fields = mutableMapOf<String, VariableElement>()
val setters = mutableMapOf<String, ExecutableElement>()
val getters = mutableMapOf<String, ExecutableElement>()
for (element in typeElement.enclosedElements) {
if (element is VariableElement) {
fields[element.name] = element
} else if (element is ExecutableElement) {
when {
element.name.startsWith("get") -> {
val name = element.name.substring("get".length).decapitalizeAsciiOnly()
getters[name] = element
}
element.name.startsWith("is") -> {
val name = element.name.substring("is".length).decapitalizeAsciiOnly()
getters[name] = element
}
element.name.startsWith("set") -> {
val name = element.name.substring("set".length).decapitalizeAsciiOnly()
setters[name] = element
}
}
val propertyProto = typeMetadata.data.getPropertyOrNull(element)
if (propertyProto != null) {
val name = nameResolver.getString(propertyProto.name)
annotationHolders[name] = element
}
}
}
val result = mutableMapOf<String, TargetProperty>()
for (property in classProto.propertyList) {
val name = nameResolver.getString(property.name)
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 = 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 { typeParameter ->
val possibleBounds = typeParameter.upperBoundList
.map { it.asTypeName(nameResolver, proto::getTypeParameter, false) }
val typeVar = if (possibleBounds.isEmpty()) {
TypeVariableName(
name = nameResolver.getString(typeParameter.name),
variance = typeParameter.varianceModifier)
} else {
TypeVariableName(
name = nameResolver.getString(typeParameter.name),
bounds = *possibleBounds.toTypedArray(),
variance = typeParameter.varianceModifier)
}
return@map typeVar.copy(reified = typeParameter.reified)
}
}
private val TypeParameter.varianceModifier: KModifier?
get() {
return variance.asKModifier().let {
// We don't redeclare out variance here
if (it == KModifier.OUT) {
null
} else {
it
}
}
}
/**
* Returns a new [PropertySpec] representation of [this].
*
* This will copy its name, type, visibility modifiers, constant value, and annotations. Note that
* Java modifiers that correspond to annotations in kotlin will be added as well (`volatile`,
* `transient`, etc`.
*
* The original `field` ([this]) is stored in [PropertySpec.tag].
*/
internal fun VariableElement.asPropertySpec(asJvmField: Boolean = false): PropertySpec {
require(kind == ElementKind.FIELD) {
"Must be a field!"
}
val modifiers: Set<Modifier> = modifiers
val fieldName = simpleName.toString()
val propertyBuilder = PropertySpec.builder(fieldName, asType().asTypeName())
propertyBuilder.addModifiers(*modifiers.mapNotNull { it.asKModifier() }.toTypedArray())
constantValue?.let {
if (it is String) {
propertyBuilder.initializer(CodeBlock.of("%S", it))
} else {
propertyBuilder.initializer(CodeBlock.of("%L", it))
}
}
propertyBuilder.addAnnotations(annotationMirrors.map(AnnotationMirror::asAnnotationSpec))
propertyBuilder.addAnnotations(modifiers.mapNotNull { it.asAnnotation() })
propertyBuilder.tag(this)
if (asJvmField && KModifier.PRIVATE !in propertyBuilder.modifiers) {
propertyBuilder.addAnnotation(JvmField::class)
}
return propertyBuilder.build()
}
/**
* Returns a new [AnnotationSpec] representation of [this].
*
* Identical and delegates to [AnnotationSpec.get], but the original `mirror` is also stored
* in [AnnotationSpec.tag].
*/
internal fun AnnotationMirror.asAnnotationSpec(): AnnotationSpec {
return AnnotationSpec.get(this)
.toBuilder()
.tag(MoreTypes.asTypeElement(annotationType))
.build()
}
/**
* Returns a new [FunSpec] representation of [this].
*
* This will copy its visibility modifiers, type parameters, return type, name, parameters, and
* throws declarations.
*
* The original `method` ([this]) is stored in [FunSpec.tag].
*
* Nearly identical to [FunSpec.overriding], but no override modifier is added nor are checks around
* overridability done
*/
internal fun ExecutableElement.asFunSpec(): FunSpec {
var modifiers: Set<Modifier> = modifiers
val methodName = simpleName.toString()
val funBuilder = FunSpec.builder(methodName)
modifiers = modifiers.toMutableSet()
funBuilder.jvmModifiers(modifiers)
typeParameters
.map { it.asType() as TypeVariable }
.map { it.asTypeVariableName() }
.forEach { funBuilder.addTypeVariable(it) }
funBuilder.returns(returnType.asTypeName())
funBuilder.addParameters(ParameterSpec.parametersOf(this))
if (isVarArgs) {
funBuilder.parameters[funBuilder.parameters.lastIndex] = funBuilder.parameters.last()
.toBuilder()
.addModifiers(VARARG)
.build()
}
if (thrownTypes.isNotEmpty()) {
val throwsValueString = thrownTypes.joinToString { "%T::class" }
funBuilder.addAnnotation(AnnotationSpec.builder(Throws::class)
.addMember(throwsValueString, *thrownTypes.toTypedArray())
.build())
}
funBuilder.tag(this)
return funBuilder.build()
}
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 PropertyGenerator(this, DelegateKey(type, emptyList()), true)
}
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 fun Modifier.asKModifier(): KModifier? {
return when (this) {
Modifier.PUBLIC -> KModifier.PUBLIC
Modifier.PROTECTED -> KModifier.PROTECTED
Modifier.PRIVATE -> KModifier.PRIVATE
Modifier.ABSTRACT -> KModifier.ABSTRACT
FINAL -> KModifier.FINAL
else -> null
}
}
private fun Modifier.asAnnotation(): AnnotationSpec? {
return when (this) {
DEFAULT -> JvmDefault::class.asAnnotationSpec()
STATIC -> JvmStatic::class.asAnnotationSpec()
TRANSIENT -> Transient::class.asAnnotationSpec()
VOLATILE -> Volatile::class.asAnnotationSpec()
SYNCHRONIZED -> Synchronized::class.asAnnotationSpec()
NATIVE -> JvmDefault::class.asAnnotationSpec()
else -> null
}
}
private fun <T : Annotation> KClass<T>.asAnnotationSpec(): AnnotationSpec {
return AnnotationSpec.builder(this).build()
}
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
}

View File

@@ -19,6 +19,7 @@ import com.google.common.truth.Truth.assertThat
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter
import com.squareup.kotlinpoet.WildcardTypeName import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.asClassName
import com.squareup.moshi.kotlin.codegen.api.TypeResolver
import org.junit.Test import org.junit.Test
class TypeResolverTest { class TypeResolverTest {