Skip to content

Commit

Permalink
Implement support for generic sealed classes (#152)
Browse files Browse the repository at this point in the history
  • Loading branch information
ansman authored Jan 29, 2021
1 parent 11b9d8e commit a24c00c
Show file tree
Hide file tree
Showing 16 changed files with 585 additions and 206 deletions.
23 changes: 13 additions & 10 deletions compiler/src/main/kotlin/se/ansman/kotshi/AdaptersProcessingStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import com.squareup.kotlinpoet.metadata.isData
import com.squareup.kotlinpoet.metadata.isEnum
import com.squareup.kotlinpoet.metadata.isObject
import com.squareup.kotlinpoet.metadata.isSealed
import com.squareup.kotlinpoet.metadata.specs.ClassInspector
import com.squareup.kotlinpoet.metadata.toImmutableKmClass
import se.ansman.kotshi.generators.DataClassAdapterGenerator
import se.ansman.kotshi.generators.EnumAdapterGenerator
Expand All @@ -26,7 +25,7 @@ import javax.tools.Diagnostic

class AdaptersProcessingStep(
override val processor: KotshiProcessor,
private val classInspector: ClassInspector,
private val metadataAccessor: MetadataAccessor,
private val messager: Messager,
override val filer: Filer,
private val adapters: MutableList<GeneratedAdapter>,
Expand Down Expand Up @@ -60,36 +59,40 @@ class AdaptersProcessingStep(

val generator = when {
metadata.isData -> DataClassAdapterGenerator(
classInspector = classInspector,
metadataAccessor = metadataAccessor,
types = types,
elements = elements,
element = typeElement,
metadata = metadata,
globalConfig = globalConfig
globalConfig = globalConfig,
messager = messager
)
metadata.isEnum -> EnumAdapterGenerator(
classInspector = classInspector,
metadataAccessor = metadataAccessor,
types = types,
elements = elements,
element = typeElement,
metadata = metadata,
globalConfig = globalConfig
globalConfig = globalConfig,
messager = messager
)
metadata.isObject -> ObjectAdapterGenerator(
classInspector = classInspector,
metadataAccessor = metadataAccessor,
types = types,
element = typeElement,
metadata = metadata,
elements = elements,
globalConfig = globalConfig
globalConfig = globalConfig,
messager = messager
)
metadata.isSealed -> SealedClassAdapterGenerator(
classInspector = classInspector,
metadataAccessor = metadataAccessor,
types = types,
element = typeElement,
metadata = metadata,
elements = elements,
globalConfig = globalConfig
globalConfig = globalConfig,
messager = messager
)
else -> throw ProcessingError(
"@JsonSerializable can only be applied to enums, objects, sealed classes and data classes",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,14 @@ class FactoryProcessingStep(
addCode(adapter.typeVariables.joinToString(", ", prefix = "<", postfix = ">") { "Nothing" })
}
addCode("(")
when {
adapter.requiresTypes -> addCode("%N, %N.%M", moshiParam, typeParam, typeArgumentsOrFail)
adapter.requiresMoshi -> addCode("%N", moshiParam)
if (adapter.requiresMoshi) {
addCode("%N", moshiParam)
}
if (adapter.requiresTypes) {
if (adapter.requiresMoshi) {
addCode(", ")
}
addCode("%N.%M", typeParam, typeArgumentsOrFail)
}
addCode(")\n»")
}
Expand Down
12 changes: 3 additions & 9 deletions compiler/src/main/kotlin/se/ansman/kotshi/GeneratedAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ data class GeneratedAdapter(
val targetType: ClassName,
val className: ClassName,
val typeVariables: List<TypeVariableName>,
val requiresMoshi: Boolean = true
) {
val requiresTypes: Boolean = typeVariables.isNotEmpty()
init {
assert(!requiresTypes || requiresMoshi) {
"An adapter requiring types must also require a Moshi instance."
}
}
}
val requiresTypes: Boolean,
val requiresMoshi: Boolean
)
7 changes: 3 additions & 4 deletions compiler/src/main/kotlin/se/ansman/kotshi/KotshiProcessor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.google.common.collect.ImmutableSetMultimap
import com.google.common.collect.Multimaps
import com.google.common.collect.SetMultimap
import com.squareup.kotlinpoet.classinspector.elements.ElementsClassInspector
import com.squareup.kotlinpoet.metadata.specs.ClassInspector
import net.ltgt.gradle.incap.IncrementalAnnotationProcessor
import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType.AGGREGATING
import javax.annotation.processing.AbstractProcessor
Expand All @@ -25,7 +24,7 @@ import javax.lang.model.util.Types
class KotshiProcessor : AbstractProcessor() {
private lateinit var elements: Elements
private lateinit var types: Types
private lateinit var classInspector: ClassInspector
private lateinit var metadataAccessor: MetadataAccessor
private lateinit var steps: ImmutableList<out ProcessingStep>

override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()
Expand All @@ -35,7 +34,7 @@ class KotshiProcessor : AbstractProcessor() {
return listOf(
AdaptersProcessingStep(
processor = this,
classInspector = classInspector,
metadataAccessor = metadataAccessor,
messager = processingEnv.messager,
filer = processingEnv.filer,
adapters = adapters,
Expand All @@ -60,7 +59,7 @@ class KotshiProcessor : AbstractProcessor() {
super.init(processingEnv)
elements = processingEnv.elementUtils
types = processingEnv.typeUtils
classInspector = ElementsClassInspector.create(elements, processingEnv.typeUtils)
metadataAccessor = MetadataAccessor(ElementsClassInspector.create(elements, processingEnv.typeUtils))
steps = ImmutableList.copyOf(initSteps())
}

Expand Down
23 changes: 23 additions & 0 deletions compiler/src/main/kotlin/se/ansman/kotshi/MetadataAccessor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package se.ansman.kotshi

import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.metadata.ImmutableKmClass
import com.squareup.kotlinpoet.metadata.specs.ClassInspector
import com.squareup.kotlinpoet.metadata.specs.toTypeSpec
import com.squareup.kotlinpoet.metadata.toImmutableKmClass
import javax.lang.model.element.Element

class MetadataAccessor(private val classInspector: ClassInspector) {
private val metadataPerType = mutableMapOf<Element, ImmutableKmClass>()
private val typeSpecPerType = mutableMapOf<Element, TypeSpec>()

fun getMetadata(type: Element): ImmutableKmClass =
metadataPerType.getOrPut(type) {
type.metadata.toImmutableKmClass()
}

fun getTypeSpec(type: Element): TypeSpec =
typeSpecPerType.getOrPut(type) {
getMetadata(type).toTypeSpec(classInspector)
}
}
84 changes: 84 additions & 0 deletions compiler/src/main/kotlin/se/ansman/kotshi/SealedClassSubtype.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package se.ansman.kotshi

import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.Dynamic
import com.squareup.kotlinpoet.LambdaTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.tag
import se.ansman.kotshi.generators.typesParameter
import java.lang.reflect.ParameterizedType
import javax.lang.model.element.TypeElement

class SealedClassSubtype(
metadataAccessor: MetadataAccessor,
val type: TypeElement,
val label: String
) : TypeRenderer() {
val className = type.asClassName()
val typeSpec = metadataAccessor.getTypeSpec(type)

override fun renderTypeVariable(typeVariable: TypeVariableName): CodeBlock {
val superParameters = (typeSpec.superclass as? ParameterizedTypeName)
?.typeArguments
?: emptyList()

fun TypeName.findAccessor(typesIndex: Int): CodeBlock? {
return when (this) {
is ClassName,
Dynamic,
is LambdaTypeName -> null
is WildcardTypeName -> {
for (outType in outTypes) {
outType.findAccessor(typesIndex)?.let { return it }
}
for (inType in inTypes) {
inType.findAccessor(typesIndex)?.let { return it }
}
null
}
is TypeVariableName -> {
if (name.contentEquals(typeVariable.name)) {
CodeBlock.of("")
} else {
for (bound in bounds) {
bound.findAccessor(typesIndex)?.let { return it }
}
null
}
}
is ParameterizedTypeName -> {
typeArguments.forEachIndexed { index, typeName ->
val accessor = typeName.findAccessor(typesIndex) ?: return@forEachIndexed
return CodeBlock.builder()
.addControlFlow(".let") {
add("it as? %T\n", ParameterizedType::class.java)
indent()
add("?: throw %T(%P)\n", IllegalArgumentException::class.java, "The type \${${typesParameter.name}[$typesIndex]} is not a valid type constraint for the \$this")
unindent()
}
.add(".actualTypeArguments[%L]", index)
.add(accessor)
.build()
}
null
}
}
}

superParameters.forEachIndexed { index, superParameter ->
val accessor = superParameter.findAccessor(index) ?: return@forEachIndexed
return CodeBlock.builder()
.add("%N[%L]\n", typesParameter, index)
.indent()
.add(accessor)
.unindent()
.build()
}
throw ProcessingError("Could not determine type variable type", typeVariable.tag() ?: type)
}
}
114 changes: 114 additions & 0 deletions compiler/src/main/kotlin/se/ansman/kotshi/TypeRenderer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* 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
*
* https://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 se.ansman.kotshi

import com.squareup.kotlinpoet.ARRAY
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.moshi.Types

/**
* Renders literals like `Types.newParameterizedType(List::class.java, String::class.java)`.
* Rendering is pluggable so that type variables can either be resolved or emitted as other code
* blocks.
*/
abstract class TypeRenderer {
abstract fun renderTypeVariable(typeVariable: TypeVariableName): CodeBlock

fun render(typeName: TypeName, forceBox: Boolean = false): CodeBlock =
when {
typeName.annotations.isNotEmpty() -> render(typeName.copy(annotations = emptyList()), forceBox)
typeName.isNullable -> renderObjectType(typeName.copy(nullable = false))
else -> when (typeName) {
is ClassName -> {
if (forceBox) {
renderObjectType(typeName)
} else {
CodeBlock.of("%T::class.java", typeName)
}
}

is ParameterizedTypeName -> {
// If it's an Array type, we shortcut this to return Types.arrayOf()
if (typeName.rawType == ARRAY) {
CodeBlock.of(
"%T.arrayOf(%L)",
Types::class,
renderObjectType(typeName.typeArguments[0])
)
} else {
val builder = CodeBlock.builder().apply {
add("%T.", Types::class)
val enclosingClassName = typeName.rawType.enclosingClassName()
if (enclosingClassName != null) {
add("newParameterizedTypeWithOwner(%L, ", render(enclosingClassName))
} else {
add("newParameterizedType(")
}
add("%T::class.java", typeName.rawType)
for (typeArgument in typeName.typeArguments) {
add(", %L", renderObjectType(typeArgument))
}
add(")")
}
builder.build()
}
}

is WildcardTypeName -> {
val target: TypeName
val method: String
when {
typeName.inTypes.size == 1 -> {
target = typeName.inTypes[0]
method = "supertypeOf"
}
typeName.outTypes.size == 1 -> {
target = typeName.outTypes[0]
method = "subtypeOf"
}
else -> throw IllegalArgumentException(
"Unrepresentable wildcard type. Cannot have more than one bound: $typeName"
)
}
CodeBlock.of("%T.%L(%L)", Types::class, method, render(target, forceBox = true))
}

is TypeVariableName -> renderTypeVariable(typeName)

else -> throw IllegalArgumentException("Unrepresentable type: $typeName")
}
}

private fun renderObjectType(typeName: TypeName): CodeBlock =
if (typeName.isPrimitive) {
CodeBlock.of("%T::class.javaObjectType", typeName)
} else {
render(typeName)
}

companion object {
operator fun invoke(renderer: (TypeVariableName) -> CodeBlock): TypeRenderer =
object : TypeRenderer() {
override fun renderTypeVariable(typeVariable: TypeVariableName): CodeBlock = renderer(typeVariable)
}
}
}
Loading

0 comments on commit a24c00c

Please sign in to comment.