diff --git a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/AdapterGenerator.kt b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/AdapterGenerator.kt index 707bc11..5d71cbf 100644 --- a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/AdapterGenerator.kt +++ b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/AdapterGenerator.kt @@ -29,26 +29,28 @@ import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.asTypeName -import org.jetbrains.kotlin.serialization.ProtoBuf +import me.eugeniomarletti.kotlin.metadata.isDataClass +import me.eugeniomarletti.kotlin.metadata.visibility +import org.jetbrains.kotlin.serialization.ProtoBuf.Visibility import java.lang.reflect.Type -import javax.lang.model.element.Element import javax.lang.model.element.TypeElement import javax.lang.model.util.Elements /** Generates a JSON adapter for a target type. */ internal class AdapterGenerator( - val className: ClassName, - val propertyList: List, - val originalElement: Element, - val isDataClass: Boolean, - val hasCompanionObject: Boolean, - val visibility: ProtoBuf.Visibility, - val elements: Elements, - val genericTypeNames: List? + target: TargetType, + private val propertyList: List, + val elements: Elements ) { - val nameAllocator = NameAllocator() - val adapterName = "${className.simpleNames().joinToString(separator = "_")}JsonAdapter" - val originalTypeName = originalElement.asType().asTypeName() + private val className = target.name + private val isDataClass = target.proto.isDataClass + private val hasCompanionObject = target.hasCompanionObject + private val visibility = target.proto.visibility!! + val genericTypeNames = target.genericTypeNames + + private val nameAllocator = NameAllocator() + private val adapterName = "${className.simpleNames().joinToString(separator = "_")}JsonAdapter" + private val originalTypeName = target.element.asType().asTypeName() val moshiParam = ParameterSpec.builder( nameAllocator.newName("moshi"), @@ -58,30 +60,30 @@ internal class AdapterGenerator( ParameterizedTypeName.get(ARRAY, Type::class.asTypeName())) .build() - val readerParam = ParameterSpec.builder( + private val readerParam = ParameterSpec.builder( nameAllocator.newName("reader"), JsonReader::class) .build() - val writerParam = ParameterSpec.builder( + private val writerParam = ParameterSpec.builder( nameAllocator.newName("writer"), JsonWriter::class) .build() - val valueParam = ParameterSpec.builder( + private val valueParam = ParameterSpec.builder( nameAllocator.newName("value"), originalTypeName.asNullable()) .build() - val jsonAdapterTypeName = ParameterizedTypeName.get( + private val jsonAdapterTypeName = ParameterizedTypeName.get( JsonAdapter::class.asClassName(), originalTypeName) // selectName() API setup - val optionsProperty = PropertySpec.builder( + private val optionsProperty = PropertySpec.builder( nameAllocator.newName("options"), JsonReader.Options::class.asTypeName(), KModifier.PRIVATE) - .initializer("%T.of(${propertyList.map { it.serializedName } + .initializer("%T.of(${propertyList.map { it.jsonName } .joinToString(", ") { "\"$it\"" }})", JsonReader.Options::class.asTypeName()) .build() - val delegateAdapters = propertyList.distinctBy { it.delegateKey } + private val delegateAdapters = propertyList.distinctBy { it.delegateKey } fun generateFile(generatedOption: TypeElement?): FileSpec { for (property in delegateAdapters) { @@ -112,12 +114,12 @@ internal class AdapterGenerator( result.superclass(jsonAdapterTypeName) - genericTypeNames?.let { + if (genericTypeNames.isNotEmpty()) { result.addTypeVariables(genericTypeNames) } // TODO make this configurable. Right now it just matches the source model - if (visibility == ProtoBuf.Visibility.INTERNAL) { + if (visibility == Visibility.INTERNAL) { result.addModifiers(KModifier.INTERNAL) } @@ -139,7 +141,7 @@ internal class AdapterGenerator( val result = FunSpec.constructorBuilder() result.addParameter(moshiParam) - genericTypeNames?.let { + if (genericTypeNames.isNotEmpty()) { result.addParameter(typesParam) } @@ -279,7 +281,7 @@ internal class AdapterGenerator( result.addStatement("%N.beginObject()", writerParam) propertyList.forEach { property -> - result.addStatement("%N.name(%S)", writerParam, property.serializedName) + result.addStatement("%N.name(%S)", writerParam, property.jsonName) result.addStatement("%N.toJson(%N, %N.%L)", nameAllocator.get(property.delegateKey), writerParam, valueParam, property.name) } @@ -301,16 +303,13 @@ internal class AdapterGenerator( .addParameter(moshiParam) // TODO make this configurable. Right now it just matches the source model - if (visibility == ProtoBuf.Visibility.INTERNAL) { + if (visibility == Visibility.INTERNAL) { result.addModifiers(KModifier.INTERNAL) } - genericTypeNames?.let { + if (genericTypeNames.isNotEmpty()) { result.addParameter(typesParam) - result.addTypeVariables(it) - } - - if (genericTypeNames != null) { + result.addTypeVariables(genericTypeNames) result.addStatement("return %N(%N, %N)", adapterName, moshiParam, typesParam) } else { result.addStatement("return %N(%N)", adapterName, moshiParam) diff --git a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/DelegateKey.kt b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/DelegateKey.kt index c5ec302..d02b2a9 100644 --- a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/DelegateKey.kt +++ b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/DelegateKey.kt @@ -29,11 +29,10 @@ import javax.lang.model.element.AnnotationMirror /** A JsonAdapter that can be used to encode and decode a particular field. */ internal data class DelegateKey( - val type: TypeName, - val jsonQualifiers: Set + private val type: TypeName, + private val jsonQualifiers: Set ) { - val nullable - get() = type.nullable || type is TypeVariableName + val nullable get() = type.nullable || type is TypeVariableName fun reserveName(nameAllocator: NameAllocator) { val qualifierNames = jsonQualifiers.joinToString("") { @@ -53,8 +52,7 @@ internal data class DelegateKey( } else { CodeBlock.of("<%T>", type) }, - type.makeType( - enclosing.elements, enclosing.typesParam, enclosing.genericTypeNames ?: emptyList())) + type.makeType(enclosing.elements, enclosing.typesParam, enclosing.genericTypeNames)) val standardArgsSize = standardArgs.size + 1 val (initializerString, args) = when { qualifiers.isEmpty() -> "" to emptyArray() diff --git a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/JsonClassCodeGenProcessor.kt b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/JsonClassCodeGenProcessor.kt index 0d39ab1..6a36f47 100644 --- a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/JsonClassCodeGenProcessor.kt +++ b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/JsonClassCodeGenProcessor.kt @@ -15,46 +15,18 @@ */ package com.squareup.moshi -import com.google.auto.common.AnnotationMirrors import com.google.auto.service.AutoService -import com.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.KModifier.OUT -import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.TypeSpec -import com.squareup.kotlinpoet.TypeVariableName -import com.squareup.kotlinpoet.asTypeName -import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata import me.eugeniomarletti.kotlin.metadata.KotlinMetadataUtils -import me.eugeniomarletti.kotlin.metadata.classKind import me.eugeniomarletti.kotlin.metadata.declaresDefaultValue -import me.eugeniomarletti.kotlin.metadata.getPropertyOrNull -import me.eugeniomarletti.kotlin.metadata.hasSetter -import me.eugeniomarletti.kotlin.metadata.isDataClass -import me.eugeniomarletti.kotlin.metadata.isInnerClass -import me.eugeniomarletti.kotlin.metadata.isPrimary -import me.eugeniomarletti.kotlin.metadata.jvm.getJvmConstructorSignature -import me.eugeniomarletti.kotlin.metadata.kotlinMetadata -import me.eugeniomarletti.kotlin.metadata.modality -import me.eugeniomarletti.kotlin.metadata.visibility import me.eugeniomarletti.kotlin.processing.KotlinAbstractProcessor -import org.jetbrains.kotlin.serialization.ProtoBuf.Class -import org.jetbrains.kotlin.serialization.ProtoBuf.Modality -import org.jetbrains.kotlin.serialization.ProtoBuf.Property -import org.jetbrains.kotlin.serialization.ProtoBuf.ValueParameter -import org.jetbrains.kotlin.serialization.ProtoBuf.Visibility -import org.jetbrains.kotlin.util.capitalizeDecapitalize.decapitalizeAsciiOnly import java.io.File import javax.annotation.processing.ProcessingEnvironment import javax.annotation.processing.Processor import javax.annotation.processing.RoundEnvironment import javax.lang.model.SourceVersion -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.TypeElement -import javax.lang.model.element.VariableElement import javax.tools.Diagnostic.Kind.ERROR /** @@ -112,255 +84,44 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils for (type in roundEnv.getElementsAnnotatedWith(annotation)) { val jsonClass = type.getAnnotation(annotation) if (jsonClass.generateAdapter) { - val adapterGenerator = processElement(type) ?: continue - adapterGenerator.generateAndWrite(generatedType) + val generator = adapterGenerator(type) ?: continue + generator.generateAndWrite(generatedType) } } return true } - private fun processElement(model: Element): AdapterGenerator? { - val metadata = model.kotlinMetadata + private fun adapterGenerator(element: Element): AdapterGenerator? { + val type = TargetType.get(messager, elementUtils, element) ?: return null - if (metadata !is KotlinClassMetadata) { - messager.printMessage( - ERROR, "@JsonClass can't be applied to $model: must be a Kotlin class", model) - return null + val properties = mutableMapOf() + for (property in type.properties.values) { + val generator = property.generator(messager) + if (generator != null) { + properties[property.name] = generator + } } - val classData = metadata.data - val (nameResolver, classProto) = classData - - when { - classProto.classKind != Class.Kind.CLASS -> { + for ((name, parameter) in type.constructor.parameters) { + if (type.properties[parameter.name] == null && !parameter.proto.declaresDefaultValue) { messager.printMessage( - ERROR, "@JsonClass can't be applied to $model: must be a Kotlin class", model) - return null - } - classProto.isInnerClass -> { - messager.printMessage( - ERROR, "@JsonClass can't be applied to $model: must not be an inner class", - model) - return null - } - classProto.modality == Modality.ABSTRACT -> { - messager.printMessage( - ERROR, "@JsonClass can't be applied to $model: must not be abstract", model) - return null - } - classProto.visibility == Visibility.LOCAL -> { - messager.printMessage( - ERROR, "@JsonClass can't be applied to $model: must not be local", model) - return null - } - } - - val typeName = model.asType().asTypeName() - val className = when (typeName) { - is ClassName -> typeName - is ParameterizedTypeName -> typeName.rawType - else -> throw IllegalStateException("unexpected TypeName: ${typeName::class}") - } - - val hasCompanionObject = classProto.hasCompanionObjectName() - // todo allow custom constructor - val protoConstructor = classProto.constructorList - .single { it.isPrimary } - val constructorJvmSignature = protoConstructor.getJvmConstructorSignature(nameResolver, - classProto.typeTable) - val constructor = classProto.fqName - .let(nameResolver::getString) - .replace('/', '.') - .let(elementUtils::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: Map = protoConstructor.valueParameterList.associateBy { - nameResolver.getString(it.name) - } - - val properties = classData.classProto.propertyList.associateBy { - nameResolver.getString(it.name) - } - - val annotationHolders = mutableMapOf() - val fields = mutableMapOf() - val setters = mutableMapOf() - val getters = mutableMapOf() - for (element in model.enclosedElements) { - if (element is VariableElement) { - fields[element.name] = element - } else if (element is ExecutableElement) { - when { - element.name.startsWith("get") -> { - getters[element.name.substring("get".length).decapitalizeAsciiOnly()] = element - } - element.name.startsWith("is") -> { - getters[element.name.substring("is".length).decapitalizeAsciiOnly()] = element - } - element.name.startsWith("set") -> { - setters[element.name.substring("set".length).decapitalizeAsciiOnly()] = element - } - } - - val property = classData.getPropertyOrNull(element) - if (property != null) { - annotationHolders[property] = element - } - } - } - - val propertiesByName = mutableMapOf() - for (property in properties.values) { - val name = nameResolver.getString(property.name) - - val fieldElement = fields[name] - val setterElement = setters[name] - val getterElement = getters[name] - val element = fieldElement ?: setterElement ?: getterElement!! - - val parameter = parameters[name] - var parameterIndex: Int = -1 - var parameterElement: VariableElement? = null - if (parameter != null) { - parameterIndex = protoConstructor.valueParameterList.indexOf(parameter) - parameterElement = constructor.parameters[parameterIndex] - } - - val annotationHolder = annotationHolders[property] - - if (property.visibility != Visibility.INTERNAL - && property.visibility != Visibility.PROTECTED - && property.visibility != Visibility.PUBLIC) { - messager.printMessage(ERROR, "property $name is not visible", element) - return null - } - - val hasDefault = parameter?.declaresDefaultValue ?: true - - if (Modifier.TRANSIENT in element.modifiers) { - if (!hasDefault) { - messager.printMessage( - ERROR, "No default value for transient property $name", element) - return null - } - continue // This property is transient and has a default value. Ignore it. - } - - if (!property.hasSetter && parameter == null) { - continue // This property is not settable. Ignore it. - } - - val delegateKey = DelegateKey( - property.returnType.asTypeName(nameResolver, classProto::getTypeParameter, true), - jsonQualifiers(element, annotationHolder, parameterElement)) - - propertiesByName[name] = PropertyGenerator( - delegateKey, - name, - jsonName(name, element, annotationHolder, parameterElement), - parameterIndex, - hasDefault, - property.returnType.asTypeName(nameResolver, classProto::getTypeParameter)) - } - - for (parameterElement in constructor.parameters) { - val name = parameterElement.name - val valueParameter = parameters[name]!! - if (properties[name] == null && !valueParameter.declaresDefaultValue) { - messager.printMessage( - ERROR, "No property for required constructor parameter $name", parameterElement) + ERROR, "No property for required constructor parameter $name", parameter.element) return null } } // Sort properties so that those with constructor parameters come first. - val propertyGenerators = propertiesByName.values.toMutableList() - propertyGenerators.sortBy { + val sortedProperties = properties.values.toMutableList() + sortedProperties.sortBy { if (it.hasConstructorParameter) { - it.parameterIndex + it.target.parameterIndex } else { Integer.MAX_VALUE } } - val genericTypeNames = classProto.typeParameterList - .map { - val variance = it.variance.asKModifier().let { - // We don't redeclare out variance here - if (it == OUT) { - null - } else { - it - } - } - TypeVariableName( - name = nameResolver.getString(it.name), - bounds = *(it.upperBoundList - .map { it.asTypeName(nameResolver, classProto::getTypeParameter) } - .toTypedArray()), - variance = variance) - .reified(it.reified) - }.let { - if (it.isEmpty()) { - null - } else { - it - } - } - - return AdapterGenerator( - className = className, - propertyList = propertyGenerators, - originalElement = model, - hasCompanionObject = hasCompanionObject, - visibility = classProto.visibility!!, - genericTypeNames = genericTypeNames, - elements = elementUtils, - isDataClass = classProto.isDataClass) - } - - /** Returns the JsonQualifiers on the field and parameter of a property. */ - private fun jsonQualifiers( - element: Element, - annotationHolder: ExecutableElement?, - parameter: VariableElement? - ): Set { - val elementQualifiers = element.qualifiers - val annotationHolderQualifiers = annotationHolder.qualifiers - val parameterQualifiers = parameter.qualifiers - - // TODO(jwilson): union the qualifiers somehow? - return when { - elementQualifiers.isNotEmpty() -> elementQualifiers - annotationHolderQualifiers.isNotEmpty() -> annotationHolderQualifiers - parameterQualifiers.isNotEmpty() -> parameterQualifiers - else -> setOf() - } - } - - /** Returns the @Json name of a property, or `propertyName` if none is provided. */ - private fun jsonName( - propertyName: String, - element: Element, - annotationHolder: ExecutableElement?, - parameter: VariableElement? - ): String { - val fieldJsonName = element.jsonName - val annotationHolderJsonName = annotationHolder.jsonName - val parameterJsonName = parameter.jsonName - - return when { - fieldJsonName != null -> fieldJsonName - annotationHolderJsonName != null -> annotationHolderJsonName - parameterJsonName != null -> parameterJsonName - else -> propertyName - } + return AdapterGenerator(type, sortedProperties, elementUtils) } private fun AdapterGenerator.generateAndWrite(generatedOption: TypeElement?) { @@ -376,21 +137,4 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils val file = filer.createSourceFile(adapterName).toUri().let(::File) return file.parentFile.also { file.delete() } } - - 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 - } } - -private val Element.name: String - get() { - return simpleName.toString() - } \ No newline at end of file diff --git a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/PropertyGenerator.kt b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/PropertyGenerator.kt index 6ef0d21..61e85a3 100644 --- a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/PropertyGenerator.kt +++ b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/PropertyGenerator.kt @@ -18,29 +18,23 @@ package com.squareup.moshi import com.squareup.kotlinpoet.BOOLEAN import com.squareup.kotlinpoet.NameAllocator import com.squareup.kotlinpoet.PropertySpec -import com.squareup.kotlinpoet.TypeName /** Generates functions to encode and decode a property as JSON. */ -internal class PropertyGenerator( - val delegateKey: DelegateKey, - val name: String, - val serializedName: String, - val parameterIndex: Int, - val hasDefault: Boolean, - val typeName: TypeName -) { +internal class PropertyGenerator(val target: TargetProperty) { + val delegateKey = target.delegateKey() + val name = target.name + val jsonName = target.jsonName() + val hasDefault = target.hasDefault + lateinit var localName: String lateinit var localIsPresentName: String - val isRequired - get() = !delegateKey.nullable && !hasDefault + val isRequired get() = !delegateKey.nullable && !hasDefault - val hasConstructorParameter - get() = parameterIndex != -1 + val hasConstructorParameter get() = target.parameterIndex != -1 /** We prefer to use 'null' to mean absent, but for some properties those are distinct. */ - val differentiateAbsentFromNull - get() = delegateKey.nullable && hasDefault + val differentiateAbsentFromNull get() = delegateKey.nullable && hasDefault fun allocateNames(nameAllocator: NameAllocator) { localName = nameAllocator.newName(name) @@ -48,7 +42,7 @@ internal class PropertyGenerator( } fun generateLocalProperty(): PropertySpec { - return PropertySpec.builder(localName, typeName.asNullable()) + return PropertySpec.builder(localName, target.type.asNullable()) .mutable(true) .initializer("null") .build() diff --git a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/TargetConstructor.kt b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/TargetConstructor.kt new file mode 100644 index 0000000..9512f8d --- /dev/null +++ b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/TargetConstructor.kt @@ -0,0 +1,63 @@ +/* + * 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 + +import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata +import me.eugeniomarletti.kotlin.metadata.isPrimary +import me.eugeniomarletti.kotlin.metadata.jvm.getJvmConstructorSignature +import org.jetbrains.kotlin.serialization.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) + } + } +} \ No newline at end of file diff --git a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/TargetParameter.kt b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/TargetParameter.kt new file mode 100644 index 0000000..fe15fbd --- /dev/null +++ b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/TargetParameter.kt @@ -0,0 +1,27 @@ +/* + * 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 + +import org.jetbrains.kotlin.serialization.ProtoBuf.ValueParameter +import javax.lang.model.element.VariableElement + +/** 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 +) \ No newline at end of file diff --git a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/TargetProperty.kt b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/TargetProperty.kt new file mode 100644 index 0000000..8a03435 --- /dev/null +++ b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/TargetProperty.kt @@ -0,0 +1,135 @@ +/* + * 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 + +import com.google.auto.common.AnnotationMirrors +import com.squareup.kotlinpoet.TypeName +import me.eugeniomarletti.kotlin.metadata.declaresDefaultValue +import me.eugeniomarletti.kotlin.metadata.hasSetter +import me.eugeniomarletti.kotlin.metadata.visibility +import org.jetbrains.kotlin.serialization.ProtoBuf.Property +import org.jetbrains.kotlin.serialization.ProtoBuf.Visibility.INTERNAL +import org.jetbrains.kotlin.serialization.ProtoBuf.Visibility.PROTECTED +import org.jetbrains.kotlin.serialization.ProtoBuf.Visibility.PUBLIC +import javax.annotation.processing.Messager +import javax.lang.model.element.AnnotationMirror +import javax.lang.model.element.Element +import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.Modifier +import javax.lang.model.element.VariableElement +import javax.tools.Diagnostic + +/** A property in user code that maps to JSON. */ +internal data class TargetProperty( + val name: String, + val type: TypeName, + private val typeWithResolvedAliases: 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 (!isVisible) { + messager.printMessage(Diagnostic.Kind.ERROR, "property ${this} is not visible", element) + return null + } + + if (isTransient) { + if (!hasDefault) { + messager.printMessage( + Diagnostic.Kind.ERROR, "No default value for transient property ${this}", element) + return null + } + return null // This property is transient and has a default value. Ignore it. + } + + if (!isSettable) { + return null // This property is not settable. Ignore it. + } + + return PropertyGenerator(this) + } + + fun delegateKey() = DelegateKey(typeWithResolvedAliases, jsonQualifiers()) + + /** 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 + } + + override fun toString() = name +} \ No newline at end of file diff --git a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/TargetType.kt b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/TargetType.kt new file mode 100644 index 0000000..73c72dc --- /dev/null +++ b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/TargetType.kt @@ -0,0 +1,186 @@ +/* + * 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 + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.TypeVariableName +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.visibility +import org.jetbrains.kotlin.serialization.ProtoBuf.Class +import org.jetbrains.kotlin.serialization.ProtoBuf.Modality.ABSTRACT +import org.jetbrains.kotlin.serialization.ProtoBuf.TypeParameter +import org.jetbrains.kotlin.serialization.ProtoBuf.Visibility.LOCAL +import org.jetbrains.kotlin.serialization.deserialization.NameResolver +import org.jetbrains.kotlin.util.capitalizeDecapitalize.decapitalizeAsciiOnly +import javax.annotation.processing.Messager +import javax.lang.model.element.Element +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.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 genericTypeNames: List +) { + val name = element.className + val hasCompanionObject = proto.hasCompanionObjectName() + + companion object { + /** Returns a target type for `element`, or null if it cannot be used with code gen. */ + fun get(messager: Messager, elementUtils: Elements, 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.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 constructor = TargetConstructor.primary(typeMetadata, elementUtils) + val properties = properties(element, constructor) + val genericTypeNames = genericTypeNames(proto, typeMetadata.data.nameResolver) + return TargetType(proto, element, constructor, properties, genericTypeNames) + } + + private fun properties( + model: TypeElement, + constructor: TargetConstructor + ): Map { + val typeMetadata: KotlinClassMetadata = model.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 model.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 = property.returnType.asTypeName( + nameResolver, classProto::getTypeParameter, false) + val typeWithResolvedAliases = property.returnType.asTypeName( + nameResolver, classProto::getTypeParameter, true) + result[name] = TargetProperty(name, type, typeWithResolvedAliases, 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 { + TypeVariableName( + name = nameResolver.getString(it.name), + bounds = *(it.upperBoundList + .map { it.asTypeName(nameResolver, proto::getTypeParameter) } + .toTypedArray()), + variance = it.varianceModifier) + .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 + } + } + } + } +} \ No newline at end of file diff --git a/kotlin-codegen/compiler/src/test/java/com/squareup/moshi/CompilerTest.kt b/kotlin-codegen/compiler/src/test/java/com/squareup/moshi/CompilerTest.kt index ded58fb..092847d 100644 --- a/kotlin-codegen/compiler/src/test/java/com/squareup/moshi/CompilerTest.kt +++ b/kotlin-codegen/compiler/src/test/java/com/squareup/moshi/CompilerTest.kt @@ -206,4 +206,25 @@ class CompilerTest { assertThat(result.systemErr).contains( "Invalid option value for ${JsonClassCodeGenProcessor.OPTION_GENERATED}") } + + @Test fun multipleErrors() { + val call = KotlinCompilerCall(temporaryFolder.root) + call.inheritClasspath = true + call.addService(Processor::class, JsonClassCodeGenProcessor::class) + call.addKt("source.kt", """ + |import com.squareup.moshi.JsonClass + | + |@JsonClass(generateAdapter = true) + |class Class1(private var a: Int, private var b: Int) + | + |@JsonClass(generateAdapter = true) + |class Class2(private var c: Int) + |""".trimMargin()) + + val result = call.execute() + assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) + assertThat(result.systemErr).contains("property a is not visible") + assertThat(result.systemErr).contains("property b is not visible") + assertThat(result.systemErr).contains("property c is not visible") + } } diff --git a/kotlin-codegen/compiler/src/test/java/com/squareup/moshi/KotlinCompilerCall.kt b/kotlin-codegen/compiler/src/test/java/com/squareup/moshi/KotlinCompilerCall.kt index 76f6d19..d533768 100644 --- a/kotlin-codegen/compiler/src/test/java/com/squareup/moshi/KotlinCompilerCall.kt +++ b/kotlin-codegen/compiler/src/test/java/com/squareup/moshi/KotlinCompilerCall.kt @@ -20,14 +20,12 @@ import okio.Buffer import okio.Okio import org.jetbrains.kotlin.cli.common.CLITool import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler -import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream import java.io.ObjectOutputStream import java.io.PrintStream import java.net.URLClassLoader import java.net.URLDecoder -import java.util.Base64 import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream import kotlin.reflect.KClass @@ -178,18 +176,18 @@ class KotlinCompilerCall(var scratchDir: File) { } /** - * Base64 encodes a mapping of annotation processor args for kapt, borrowed from + * Base64 encodes a mapping of annotation processor args for kapt, as specified by * https://kotlinlang.org/docs/reference/kapt.html#apjavac-options-encoding */ private fun encodeOptions(options: Map): String { - val os = ByteArrayOutputStream() - ObjectOutputStream(os).use { oos -> + val buffer = Buffer() + ObjectOutputStream(buffer.outputStream()).use { oos -> oos.writeInt(options.size) for ((key, value) in options.entries) { oos.writeUTF(key) oos.writeUTF(value) } } - return Base64.getEncoder().encodeToString(os.toByteArray()) + return buffer.readByteString().base64() } }