From ea300997a12d15c286dbfff06693d0650d97620f Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Sun, 8 Sep 2019 21:24:10 -0400 Subject: [PATCH] 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() --- .../moshi/kotlin/codegen/AppliedType.kt | 1 + .../codegen/JsonClassCodegenProcessor.kt | 24 +- .../moshi/kotlin/codegen/TargetConstructor.kt | 63 -- .../moshi/kotlin/codegen/TargetProperty.kt | 167 ------ .../moshi/kotlin/codegen/TargetType.kt | 229 ------- .../codegen/{ => api}/AdapterGenerator.kt | 25 +- .../kotlin/codegen/{ => api}/DelegateKey.kt | 2 +- .../codegen/{ => api}/PropertyGenerator.kt | 4 +- .../TargetConstructor.kt} | 21 +- .../kotlin/codegen/api/TargetParameter.kt | 38 ++ .../kotlin/codegen/api/TargetProperty.kt | 39 ++ .../moshi/kotlin/codegen/api/TargetType.kt | 35 ++ .../kotlin/codegen/{ => api}/TypeRenderer.kt | 2 +- .../kotlin/codegen/{ => api}/TypeResolver.kt | 2 +- .../kotlin/codegen/{ => api}/kotlintypes.kt | 9 +- .../squareup/moshi/kotlin/codegen/metadata.kt | 557 +++++++++++++++++- .../moshi/kotlin/codegen/TypeResolverTest.kt | 1 + 17 files changed, 713 insertions(+), 506 deletions(-) delete mode 100644 kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TargetConstructor.kt delete mode 100644 kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TargetProperty.kt delete mode 100644 kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TargetType.kt rename kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/{ => api}/AdapterGenerator.kt (94%) rename kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/{ => api}/DelegateKey.kt (98%) rename kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/{ => api}/PropertyGenerator.kt (96%) rename kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/{TargetParameter.kt => api/TargetConstructor.kt} (61%) create mode 100644 kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetParameter.kt create mode 100644 kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetProperty.kt create mode 100644 kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetType.kt rename kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/{ => api}/TypeRenderer.kt (98%) rename kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/{ => api}/TypeResolver.kt (97%) rename kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/{ => api}/kotlintypes.kt (87%) diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/AppliedType.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/AppliedType.kt index 06baf79..a3e27e9 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/AppliedType.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/AppliedType.kt @@ -18,6 +18,7 @@ package com.squareup.moshi.kotlin.codegen import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.asTypeName +import com.squareup.moshi.kotlin.codegen.api.TypeResolver import javax.lang.model.element.TypeElement import javax.lang.model.type.DeclaredType import javax.lang.model.util.Types diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/JsonClassCodegenProcessor.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/JsonClassCodegenProcessor.kt index e203ced..7517696 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/JsonClassCodegenProcessor.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/JsonClassCodegenProcessor.kt @@ -16,9 +16,11 @@ package com.squareup.moshi.kotlin.codegen import com.google.auto.service.AutoService +import com.squareup.kotlinpoet.asClassName 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.declaresDefaultValue import me.eugeniomarletti.kotlin.processing.KotlinAbstractProcessor import net.ltgt.gradle.incap.IncrementalAnnotationProcessor 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.element.Element import javax.lang.model.element.TypeElement +import javax.lang.model.element.VariableElement import javax.tools.Diagnostic.Kind.ERROR /** @@ -74,9 +77,9 @@ class JsonClassCodegenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils override fun init(processingEnv: ProcessingEnvironment) { super.init(processingEnv) generatedType = processingEnv.options[OPTION_GENERATED]?.let { - if (it !in POSSIBLE_GENERATED_NAMES) { - throw IllegalArgumentException("Invalid option value for $OPTION_GENERATED. Found $it, " + - "allowable values are $POSSIBLE_GENERATED_NAMES.") + require(it in POSSIBLE_GENERATED_NAMES) { + "Invalid option value for $OPTION_GENERATED. Found $it, " + + "allowable values are $POSSIBLE_GENERATED_NAMES." } processingEnv.elementUtils.getTypeElement(it) } @@ -87,7 +90,12 @@ class JsonClassCodegenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils val jsonClass = type.getAnnotation(annotation) if (jsonClass.generateAdapter && jsonClass.generator.isEmpty()) { val generator = adapterGenerator(type) ?: continue - generator.generateFile(generatedType) + generator + .generateFile(generatedType?.asClassName()) { + it.toBuilder() + .addOriginatingElement(type) + .build() + } .writeTo(filer) } } @@ -96,7 +104,7 @@ class JsonClassCodegenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils } 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() for (property in type.properties.values) { @@ -107,9 +115,9 @@ class JsonClassCodegenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils } 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( - ERROR, "No property for required constructor parameter $name", parameter.element) + ERROR, "No property for required constructor parameter $name", parameter.tag()) return null } } diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TargetConstructor.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TargetConstructor.kt deleted file mode 100644 index df8a22c..0000000 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TargetConstructor.kt +++ /dev/null @@ -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 -) { - 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() - 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) - } - } -} diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TargetProperty.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TargetProperty.kt deleted file mode 100644 index 2d6dadd..0000000 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TargetProperty.kt +++ /dev/null @@ -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 { - 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 - 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 -} diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TargetType.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TargetType.kt deleted file mode 100644 index 30a2e62..0000000 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TargetType.kt +++ /dev/null @@ -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, - val typeVariables: List -) { - 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() - 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 { - val typeMetadata: KotlinClassMetadata = typeElement.kotlinMetadata as KotlinClassMetadata - val nameResolver = typeMetadata.data.nameResolver - val classProto = typeMetadata.data.classProto - - val annotationHolders = mutableMapOf() - val fields = mutableMapOf() - val setters = mutableMapOf() - val getters = mutableMapOf() - 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() - 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 { - 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 - } - } - } - } -} diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/AdapterGenerator.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/AdapterGenerator.kt similarity index 94% rename from kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/AdapterGenerator.kt rename to kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/AdapterGenerator.kt index cb6fac4..ef52e4b 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/AdapterGenerator.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/AdapterGenerator.kt @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * 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.AnnotationSpec +import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec @@ -35,11 +36,9 @@ import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter import com.squareup.moshi.Moshi 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 com.squareup.moshi.kotlin.codegen.JsonClassCodegenProcessor import java.lang.reflect.Type -import javax.lang.model.element.TypeElement private val MOSHI_UTIL = Util::class.asClassName() @@ -49,14 +48,13 @@ internal class AdapterGenerator( private val propertyList: List ) { private val nonTransientProperties = propertyList.filterNot { it.isTransient } - private val className = target.name - private val visibility = target.proto.visibility!! + private val className = target.typeName.rawType() + private val visibility = target.visibility private val typeVariables = target.typeVariables private val nameAllocator = NameAllocator() private val adapterName = "${className.simpleNames.joinToString(separator = "_")}JsonAdapter" - private val originalElement = target.element - private val originalTypeName = target.element.asType().asTypeName() + private val originalTypeName = target.typeName private val moshiParam = ParameterSpec.builder( nameAllocator.newName("moshi"), @@ -98,23 +96,22 @@ internal class AdapterGenerator( .initializer("null") .build() - fun generateFile(generatedOption: TypeElement?): FileSpec { + fun generateFile(generatedOption: ClassName?, typeHook: (TypeSpec) -> TypeSpec = { it }): FileSpec { for (property in nonTransientProperties) { property.allocateNames(nameAllocator) } val result = FileSpec.builder(className.packageName, adapterName) result.addComment("Code generated by moshi-kotlin-codegen. Do not edit.") - result.addType(generateType(generatedOption)) + result.addType(generateType(generatedOption).let(typeHook)) return result.build() } - private fun generateType(generatedOption: TypeElement?): TypeSpec { + private fun generateType(generatedOption: ClassName?): TypeSpec { val result = TypeSpec.classBuilder(adapterName) - .addOriginatingElement(originalElement) generatedOption?.let { - result.addAnnotation(AnnotationSpec.builder(it.asClassName()) + result.addAnnotation(AnnotationSpec.builder(it) .addMember("value = [%S]", JsonClassCodegenProcessor::class.java.canonicalName) .addMember("comments = %S", "https://github.com/square/moshi") .build()) @@ -127,7 +124,7 @@ internal class AdapterGenerator( } // TODO make this configurable. Right now it just matches the source model - if (visibility == Visibility.INTERNAL) { + if (visibility == KModifier.INTERNAL) { result.addModifiers(KModifier.INTERNAL) } diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/DelegateKey.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/DelegateKey.kt similarity index 98% rename from kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/DelegateKey.kt rename to kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/DelegateKey.kt index d9b9061..410bc65 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/DelegateKey.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/DelegateKey.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * 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.ClassName diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/PropertyGenerator.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/PropertyGenerator.kt similarity index 96% rename from kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/PropertyGenerator.kt rename to kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/PropertyGenerator.kt index 75e3068..46d6f20 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/PropertyGenerator.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/PropertyGenerator.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * 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.NameAllocator @@ -26,7 +26,7 @@ internal class PropertyGenerator( val isTransient: Boolean = false ) { val name = target.name - val jsonName = target.jsonName() + val jsonName = target.jsonName val hasDefault = target.hasDefault lateinit var localName: String diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TargetParameter.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetConstructor.kt similarity index 61% rename from kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TargetParameter.kt rename to kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetConstructor.kt index 2294d87..1606807 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TargetParameter.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetConstructor.kt @@ -13,15 +13,16 @@ * See the License for the specific language governing permissions and * 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 javax.lang.model.element.VariableElement +import com.squareup.kotlinpoet.KModifier -/** A parameter in user code that should be populated by generated code. */ -internal data class TargetParameter( - val name: String, - val proto: ValueParameter, - val index: Int, - val element: VariableElement -) +/** A constructor in user code that should be called by generated code. */ +internal data class TargetConstructor( + val parameters: Map, + val visibility: KModifier +) { + init { + visibility.checkIsVisibility() + } +} diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetParameter.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetParameter.kt new file mode 100644 index 0000000..9c07707 --- /dev/null +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetParameter.kt @@ -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? = null, + private val tags: Map, Any> +) { + /** Returns the tag attached with [type] as a key, or null if no tag is attached with that key. */ + fun tag(type: KClass): 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 tag(): T? = tag(T::class) +} diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetProperty.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetProperty.kt new file mode 100644 index 0000000..20c5958 --- /dev/null +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetProperty.kt @@ -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 +} diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetType.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetType.kt new file mode 100644 index 0000000..ac2a7c6 --- /dev/null +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetType.kt @@ -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, + val typeVariables: List, + val isDataClass: Boolean, + val visibility: KModifier +) { + + init { + visibility.checkIsVisibility() + } +} diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TypeRenderer.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TypeRenderer.kt similarity index 98% rename from kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TypeRenderer.kt rename to kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TypeRenderer.kt index 75347fc..fdc7a93 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TypeRenderer.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TypeRenderer.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * 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.BOOLEAN diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TypeResolver.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TypeResolver.kt similarity index 97% rename from kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TypeResolver.kt rename to kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TypeResolver.kt index acd9fbc..c956702 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/TypeResolver.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TypeResolver.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * 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.ParameterizedTypeName diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/kotlintypes.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/kotlintypes.kt similarity index 87% rename from kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/kotlintypes.kt rename to kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/kotlintypes.kt index a9537be..ab01df5 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/kotlintypes.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/kotlintypes.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * 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.BYTE @@ -23,6 +23,7 @@ import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.DOUBLE import com.squareup.kotlinpoet.FLOAT import com.squareup.kotlinpoet.INT +import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.LONG import com.squareup.kotlinpoet.ParameterizedTypeName 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") 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" + } +} diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/metadata.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/metadata.kt index bb1aeae..4ccefbd 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/metadata.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/metadata.kt @@ -15,22 +15,90 @@ */ 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.CodeBlock +import com.squareup.kotlinpoet.FunSpec 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.PropertySpec import com.squareup.kotlinpoet.STAR import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeVariableName 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.TypeParameter 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.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( - nameResolver: NameResolver, - getTypeParameter: (index: Int) -> TypeParameter, - resolveAliases: Boolean = false +private fun TypeParameter.asTypeName( + nameResolver: NameResolver, + getTypeParameter: (index: Int) -> TypeParameter, + resolveAliases: Boolean = false ): TypeVariableName { val possibleBounds = upperBoundList.map { 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) { Variance.IN -> KModifier.IN 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 * 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 * called if [ProtoBuf.Type.hasTypeParameter] is true!** */ -internal fun Type.asTypeName( - nameResolver: NameResolver, - getTypeParameter: (index: Int) -> TypeParameter, - useAbbreviatedType: Boolean = true +private fun Type.asTypeName( + nameResolver: NameResolver, + getTypeParameter: (index: Int) -> TypeParameter, + useAbbreviatedType: Boolean = true ): TypeName { val argumentList = when { @@ -123,3 +203,462 @@ internal fun Type.asTypeName( 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() + 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() + 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 { + val typeMetadata: KotlinClassMetadata = typeElement.kotlinMetadata as KotlinClassMetadata + val nameResolver = typeMetadata.data.nameResolver + val classProto = typeMetadata.data.classProto + + val annotationHolders = mutableMapOf() + val fields = mutableMapOf() + val setters = mutableMapOf() + val getters = mutableMapOf() + 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() + 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 { + 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 = 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 = 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() ?: setter?.tag() + ?: getter!!.tag() + 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() ?: 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 { + val elementQualifiers = element.qualifiers + val annotationHolderQualifiers = annotationHolder?.tag().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 KClass.asAnnotationSpec(): AnnotationSpec { + return AnnotationSpec.builder(this).build() +} + +private val Element?.qualifiers: Set + 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 + } diff --git a/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/TypeResolverTest.kt b/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/TypeResolverTest.kt index e7ab974..8d1c339 100644 --- a/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/TypeResolverTest.kt +++ b/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/TypeResolverTest.kt @@ -19,6 +19,7 @@ import com.google.common.truth.Truth.assertThat import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter import com.squareup.kotlinpoet.WildcardTypeName import com.squareup.kotlinpoet.asClassName +import com.squareup.moshi.kotlin.codegen.api.TypeResolver import org.junit.Test class TypeResolverTest {