Model target types, parameters, constructors and properties (#504)

This is intended to make it easier to implement support for subtypes.
This commit is contained in:
Jesse Wilson
2018-04-15 13:46:58 -04:00
committed by GitHub
parent 78091aeb46
commit 8d24d89abf
10 changed files with 496 additions and 331 deletions

View File

@@ -29,26 +29,28 @@ import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName 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 java.lang.reflect.Type
import javax.lang.model.element.Element
import javax.lang.model.element.TypeElement import javax.lang.model.element.TypeElement
import javax.lang.model.util.Elements import javax.lang.model.util.Elements
/** Generates a JSON adapter for a target type. */ /** Generates a JSON adapter for a target type. */
internal class AdapterGenerator( internal class AdapterGenerator(
val className: ClassName, target: TargetType,
val propertyList: List<PropertyGenerator>, private val propertyList: List<PropertyGenerator>,
val originalElement: Element, val elements: Elements
val isDataClass: Boolean,
val hasCompanionObject: Boolean,
val visibility: ProtoBuf.Visibility,
val elements: Elements,
val genericTypeNames: List<TypeVariableName>?
) { ) {
val nameAllocator = NameAllocator() private val className = target.name
val adapterName = "${className.simpleNames().joinToString(separator = "_")}JsonAdapter" private val isDataClass = target.proto.isDataClass
val originalTypeName = originalElement.asType().asTypeName() 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( val moshiParam = ParameterSpec.builder(
nameAllocator.newName("moshi"), nameAllocator.newName("moshi"),
@@ -58,30 +60,30 @@ internal class AdapterGenerator(
ParameterizedTypeName.get(ARRAY, ParameterizedTypeName.get(ARRAY,
Type::class.asTypeName())) Type::class.asTypeName()))
.build() .build()
val readerParam = ParameterSpec.builder( private val readerParam = ParameterSpec.builder(
nameAllocator.newName("reader"), nameAllocator.newName("reader"),
JsonReader::class) JsonReader::class)
.build() .build()
val writerParam = ParameterSpec.builder( private val writerParam = ParameterSpec.builder(
nameAllocator.newName("writer"), nameAllocator.newName("writer"),
JsonWriter::class) JsonWriter::class)
.build() .build()
val valueParam = ParameterSpec.builder( private val valueParam = ParameterSpec.builder(
nameAllocator.newName("value"), nameAllocator.newName("value"),
originalTypeName.asNullable()) originalTypeName.asNullable())
.build() .build()
val jsonAdapterTypeName = ParameterizedTypeName.get( private val jsonAdapterTypeName = ParameterizedTypeName.get(
JsonAdapter::class.asClassName(), originalTypeName) JsonAdapter::class.asClassName(), originalTypeName)
// selectName() API setup // selectName() API setup
val optionsProperty = PropertySpec.builder( private val optionsProperty = PropertySpec.builder(
nameAllocator.newName("options"), JsonReader.Options::class.asTypeName(), nameAllocator.newName("options"), JsonReader.Options::class.asTypeName(),
KModifier.PRIVATE) KModifier.PRIVATE)
.initializer("%T.of(${propertyList.map { it.serializedName } .initializer("%T.of(${propertyList.map { it.jsonName }
.joinToString(", ") { "\"$it\"" }})", JsonReader.Options::class.asTypeName()) .joinToString(", ") { "\"$it\"" }})", JsonReader.Options::class.asTypeName())
.build() .build()
val delegateAdapters = propertyList.distinctBy { it.delegateKey } private val delegateAdapters = propertyList.distinctBy { it.delegateKey }
fun generateFile(generatedOption: TypeElement?): FileSpec { fun generateFile(generatedOption: TypeElement?): FileSpec {
for (property in delegateAdapters) { for (property in delegateAdapters) {
@@ -112,12 +114,12 @@ internal class AdapterGenerator(
result.superclass(jsonAdapterTypeName) result.superclass(jsonAdapterTypeName)
genericTypeNames?.let { if (genericTypeNames.isNotEmpty()) {
result.addTypeVariables(genericTypeNames) result.addTypeVariables(genericTypeNames)
} }
// TODO make this configurable. Right now it just matches the source model // TODO make this configurable. Right now it just matches the source model
if (visibility == ProtoBuf.Visibility.INTERNAL) { if (visibility == Visibility.INTERNAL) {
result.addModifiers(KModifier.INTERNAL) result.addModifiers(KModifier.INTERNAL)
} }
@@ -139,7 +141,7 @@ internal class AdapterGenerator(
val result = FunSpec.constructorBuilder() val result = FunSpec.constructorBuilder()
result.addParameter(moshiParam) result.addParameter(moshiParam)
genericTypeNames?.let { if (genericTypeNames.isNotEmpty()) {
result.addParameter(typesParam) result.addParameter(typesParam)
} }
@@ -279,7 +281,7 @@ internal class AdapterGenerator(
result.addStatement("%N.beginObject()", writerParam) result.addStatement("%N.beginObject()", writerParam)
propertyList.forEach { property -> 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)", result.addStatement("%N.toJson(%N, %N.%L)",
nameAllocator.get(property.delegateKey), writerParam, valueParam, property.name) nameAllocator.get(property.delegateKey), writerParam, valueParam, property.name)
} }
@@ -301,16 +303,13 @@ internal class AdapterGenerator(
.addParameter(moshiParam) .addParameter(moshiParam)
// TODO make this configurable. Right now it just matches the source model // TODO make this configurable. Right now it just matches the source model
if (visibility == ProtoBuf.Visibility.INTERNAL) { if (visibility == Visibility.INTERNAL) {
result.addModifiers(KModifier.INTERNAL) result.addModifiers(KModifier.INTERNAL)
} }
genericTypeNames?.let { if (genericTypeNames.isNotEmpty()) {
result.addParameter(typesParam) result.addParameter(typesParam)
result.addTypeVariables(it) result.addTypeVariables(genericTypeNames)
}
if (genericTypeNames != null) {
result.addStatement("return %N(%N, %N)", adapterName, moshiParam, typesParam) result.addStatement("return %N(%N, %N)", adapterName, moshiParam, typesParam)
} else { } else {
result.addStatement("return %N(%N)", adapterName, moshiParam) result.addStatement("return %N(%N)", adapterName, moshiParam)

View File

@@ -29,11 +29,10 @@ import javax.lang.model.element.AnnotationMirror
/** A JsonAdapter that can be used to encode and decode a particular field. */ /** A JsonAdapter that can be used to encode and decode a particular field. */
internal data class DelegateKey( internal data class DelegateKey(
val type: TypeName, private val type: TypeName,
val jsonQualifiers: Set<AnnotationMirror> private val jsonQualifiers: Set<AnnotationMirror>
) { ) {
val nullable val nullable get() = type.nullable || type is TypeVariableName
get() = type.nullable || type is TypeVariableName
fun reserveName(nameAllocator: NameAllocator) { fun reserveName(nameAllocator: NameAllocator) {
val qualifierNames = jsonQualifiers.joinToString("") { val qualifierNames = jsonQualifiers.joinToString("") {
@@ -53,8 +52,7 @@ internal data class DelegateKey(
} else { } else {
CodeBlock.of("<%T>", type) CodeBlock.of("<%T>", type)
}, },
type.makeType( type.makeType(enclosing.elements, enclosing.typesParam, enclosing.genericTypeNames))
enclosing.elements, enclosing.typesParam, enclosing.genericTypeNames ?: emptyList()))
val standardArgsSize = standardArgs.size + 1 val standardArgsSize = standardArgs.size + 1
val (initializerString, args) = when { val (initializerString, args) = when {
qualifiers.isEmpty() -> "" to emptyArray() qualifiers.isEmpty() -> "" to emptyArray()

View File

@@ -15,46 +15,18 @@
*/ */
package com.squareup.moshi package com.squareup.moshi
import com.google.auto.common.AnnotationMirrors
import com.google.auto.service.AutoService 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.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.KotlinMetadataUtils
import me.eugeniomarletti.kotlin.metadata.classKind
import me.eugeniomarletti.kotlin.metadata.declaresDefaultValue 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 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 java.io.File
import javax.annotation.processing.ProcessingEnvironment import javax.annotation.processing.ProcessingEnvironment
import javax.annotation.processing.Processor import javax.annotation.processing.Processor
import javax.annotation.processing.RoundEnvironment import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion import javax.lang.model.SourceVersion
import javax.lang.model.element.AnnotationMirror
import javax.lang.model.element.Element 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.TypeElement
import javax.lang.model.element.VariableElement
import javax.tools.Diagnostic.Kind.ERROR import javax.tools.Diagnostic.Kind.ERROR
/** /**
@@ -112,255 +84,44 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
for (type in roundEnv.getElementsAnnotatedWith(annotation)) { for (type in roundEnv.getElementsAnnotatedWith(annotation)) {
val jsonClass = type.getAnnotation(annotation) val jsonClass = type.getAnnotation(annotation)
if (jsonClass.generateAdapter) { if (jsonClass.generateAdapter) {
val adapterGenerator = processElement(type) ?: continue val generator = adapterGenerator(type) ?: continue
adapterGenerator.generateAndWrite(generatedType) generator.generateAndWrite(generatedType)
} }
} }
return true return true
} }
private fun processElement(model: Element): AdapterGenerator? { private fun adapterGenerator(element: Element): AdapterGenerator? {
val metadata = model.kotlinMetadata val type = TargetType.get(messager, elementUtils, element) ?: return null
if (metadata !is KotlinClassMetadata) { val properties = mutableMapOf<String, PropertyGenerator>()
messager.printMessage( for (property in type.properties.values) {
ERROR, "@JsonClass can't be applied to $model: must be a Kotlin class", model) val generator = property.generator(messager)
return null if (generator != null) {
properties[property.name] = generator
}
} }
val classData = metadata.data for ((name, parameter) in type.constructor.parameters) {
val (nameResolver, classProto) = classData if (type.properties[parameter.name] == null && !parameter.proto.declaresDefaultValue) {
when {
classProto.classKind != Class.Kind.CLASS -> {
messager.printMessage( messager.printMessage(
ERROR, "@JsonClass can't be applied to $model: must be a Kotlin class", model) ERROR, "No property for required constructor parameter $name", parameter.element)
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<String, ValueParameter> = protoConstructor.valueParameterList.associateBy {
nameResolver.getString(it.name)
}
val properties = classData.classProto.propertyList.associateBy {
nameResolver.getString(it.name)
}
val annotationHolders = mutableMapOf<Property, ExecutableElement>()
val fields = mutableMapOf<String, VariableElement>()
val setters = mutableMapOf<String, ExecutableElement>()
val getters = mutableMapOf<String, ExecutableElement>()
for (element in model.enclosedElements) {
if (element is VariableElement) {
fields[element.name] = element
} else if (element is ExecutableElement) {
when {
element.name.startsWith("get") -> {
getters[element.name.substring("get".length).decapitalizeAsciiOnly()] = element
}
element.name.startsWith("is") -> {
getters[element.name.substring("is".length).decapitalizeAsciiOnly()] = element
}
element.name.startsWith("set") -> {
setters[element.name.substring("set".length).decapitalizeAsciiOnly()] = element
}
}
val property = classData.getPropertyOrNull(element)
if (property != null) {
annotationHolders[property] = element
}
}
}
val propertiesByName = mutableMapOf<String, PropertyGenerator>()
for (property in properties.values) {
val name = nameResolver.getString(property.name)
val fieldElement = fields[name]
val setterElement = setters[name]
val getterElement = getters[name]
val element = fieldElement ?: setterElement ?: getterElement!!
val 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)
return null return null
} }
} }
// Sort properties so that those with constructor parameters come first. // Sort properties so that those with constructor parameters come first.
val propertyGenerators = propertiesByName.values.toMutableList() val sortedProperties = properties.values.toMutableList()
propertyGenerators.sortBy { sortedProperties.sortBy {
if (it.hasConstructorParameter) { if (it.hasConstructorParameter) {
it.parameterIndex it.target.parameterIndex
} else { } else {
Integer.MAX_VALUE Integer.MAX_VALUE
} }
} }
val genericTypeNames = classProto.typeParameterList return AdapterGenerator(type, sortedProperties, elementUtils)
.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<AnnotationMirror> {
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
}
} }
private fun AdapterGenerator.generateAndWrite(generatedOption: TypeElement?) { private fun AdapterGenerator.generateAndWrite(generatedOption: TypeElement?) {
@@ -376,21 +137,4 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
val file = filer.createSourceFile(adapterName).toUri().let(::File) val file = filer.createSourceFile(adapterName).toUri().let(::File)
return file.parentFile.also { file.delete() } return file.parentFile.also { file.delete() }
} }
private val Element?.qualifiers: Set<AnnotationMirror>
get() {
if (this == null) return setOf()
return AnnotationMirrors.getAnnotatedAnnotations(this, JsonQualifier::class.java)
}
private val Element?.jsonName: String?
get() {
if (this == null) return null
return getAnnotation(Json::class.java)?.name
}
} }
private val Element.name: String
get() {
return simpleName.toString()
}

View File

@@ -18,29 +18,23 @@ package com.squareup.moshi
import com.squareup.kotlinpoet.BOOLEAN import com.squareup.kotlinpoet.BOOLEAN
import com.squareup.kotlinpoet.NameAllocator import com.squareup.kotlinpoet.NameAllocator
import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeName
/** Generates functions to encode and decode a property as JSON. */ /** Generates functions to encode and decode a property as JSON. */
internal class PropertyGenerator( internal class PropertyGenerator(val target: TargetProperty) {
val delegateKey: DelegateKey, val delegateKey = target.delegateKey()
val name: String, val name = target.name
val serializedName: String, val jsonName = target.jsonName()
val parameterIndex: Int, val hasDefault = target.hasDefault
val hasDefault: Boolean,
val typeName: TypeName
) {
lateinit var localName: String lateinit var localName: String
lateinit var localIsPresentName: String lateinit var localIsPresentName: String
val isRequired val isRequired get() = !delegateKey.nullable && !hasDefault
get() = !delegateKey.nullable && !hasDefault
val hasConstructorParameter val hasConstructorParameter get() = target.parameterIndex != -1
get() = parameterIndex != -1
/** We prefer to use 'null' to mean absent, but for some properties those are distinct. */ /** We prefer to use 'null' to mean absent, but for some properties those are distinct. */
val differentiateAbsentFromNull val differentiateAbsentFromNull get() = delegateKey.nullable && hasDefault
get() = delegateKey.nullable && hasDefault
fun allocateNames(nameAllocator: NameAllocator) { fun allocateNames(nameAllocator: NameAllocator) {
localName = nameAllocator.newName(name) localName = nameAllocator.newName(name)
@@ -48,7 +42,7 @@ internal class PropertyGenerator(
} }
fun generateLocalProperty(): PropertySpec { fun generateLocalProperty(): PropertySpec {
return PropertySpec.builder(localName, typeName.asNullable()) return PropertySpec.builder(localName, target.type.asNullable())
.mutable(true) .mutable(true)
.initializer("null") .initializer("null")
.build() .build()

View File

@@ -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<String, TargetParameter>
) {
companion object {
fun primary(metadata: KotlinClassMetadata, elements: Elements): TargetConstructor {
val (nameResolver, classProto) = metadata.data
// todo allow custom constructor
val proto = classProto.constructorList
.single { it.isPrimary }
val constructorJvmSignature = proto.getJvmConstructorSignature(
nameResolver, classProto.typeTable)
val element = classProto.fqName
.let(nameResolver::getString)
.replace('/', '.')
.let(elements::getTypeElement)
.enclosedElements
.mapNotNull {
it.takeIf { it.kind == ElementKind.CONSTRUCTOR }?.let { it as ExecutableElement }
}
.first()
// TODO Temporary until JVM method signature matching is better
// .single { it.jvmMethodSignature == constructorJvmSignature }
val parameters = mutableMapOf<String, TargetParameter>()
for (parameter in proto.valueParameterList) {
val name = nameResolver.getString(parameter.name)
val index = proto.valueParameterList.indexOf(parameter)
parameters[name] = TargetParameter(name, parameter, index, element.parameters[index])
}
return TargetConstructor(element, proto, parameters)
}
}
}

View File

@@ -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
)

View File

@@ -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<AnnotationMirror> {
val elementQualifiers = element.qualifiers
val annotationHolderQualifiers = annotationHolder.qualifiers
val parameterQualifiers = parameter?.element.qualifiers
// TODO(jwilson): union the qualifiers somehow?
return when {
elementQualifiers.isNotEmpty() -> elementQualifiers
annotationHolderQualifiers.isNotEmpty() -> annotationHolderQualifiers
parameterQualifiers.isNotEmpty() -> parameterQualifiers
else -> setOf()
}
}
private val Element?.qualifiers: Set<AnnotationMirror>
get() {
if (this == null) return setOf()
return AnnotationMirrors.getAnnotatedAnnotations(this,
JsonQualifier::class.java)
}
/** Returns the @Json name of this property, or this property's name if none is provided. */
fun jsonName(): String {
val fieldJsonName = element.jsonName
val annotationHolderJsonName = annotationHolder.jsonName
val parameterJsonName = parameter?.element.jsonName
return when {
fieldJsonName != null -> fieldJsonName
annotationHolderJsonName != null -> annotationHolderJsonName
parameterJsonName != null -> parameterJsonName
else -> name
}
}
private val Element?.jsonName: String?
get() {
if (this == null) return null
return getAnnotation(Json::class.java)?.name
}
override fun toString() = name
}

View File

@@ -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<String, TargetProperty>,
val genericTypeNames: List<TypeVariableName>
) {
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<String, TargetProperty> {
val typeMetadata: KotlinClassMetadata = model.kotlinMetadata as KotlinClassMetadata
val nameResolver = typeMetadata.data.nameResolver
val classProto = typeMetadata.data.classProto
val annotationHolders = mutableMapOf<String, ExecutableElement>()
val fields = mutableMapOf<String, VariableElement>()
val setters = mutableMapOf<String, ExecutableElement>()
val getters = mutableMapOf<String, ExecutableElement>()
for (element in 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<String, TargetProperty>()
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<TypeVariableName> {
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
}
}
}
}
}

View File

@@ -206,4 +206,25 @@ class CompilerTest {
assertThat(result.systemErr).contains( assertThat(result.systemErr).contains(
"Invalid option value for ${JsonClassCodeGenProcessor.OPTION_GENERATED}") "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")
}
} }

View File

@@ -20,14 +20,12 @@ import okio.Buffer
import okio.Okio import okio.Okio
import org.jetbrains.kotlin.cli.common.CLITool import org.jetbrains.kotlin.cli.common.CLITool
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.ObjectOutputStream import java.io.ObjectOutputStream
import java.io.PrintStream import java.io.PrintStream
import java.net.URLClassLoader import java.net.URLClassLoader
import java.net.URLDecoder import java.net.URLDecoder
import java.util.Base64
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
import kotlin.reflect.KClass 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 * https://kotlinlang.org/docs/reference/kapt.html#apjavac-options-encoding
*/ */
private fun encodeOptions(options: Map<String, String>): String { private fun encodeOptions(options: Map<String, String>): String {
val os = ByteArrayOutputStream() val buffer = Buffer()
ObjectOutputStream(os).use { oos -> ObjectOutputStream(buffer.outputStream()).use { oos ->
oos.writeInt(options.size) oos.writeInt(options.size)
for ((key, value) in options.entries) { for ((key, value) in options.entries) {
oos.writeUTF(key) oos.writeUTF(key)
oos.writeUTF(value) oos.writeUTF(value)
} }
} }
return Base64.getEncoder().encodeToString(os.toByteArray()) return buffer.readByteString().base64()
} }
} }