Handle enclosing visibility + cache metadata parsing (#1026)

* Only allow public or internal classes

* Ensure consistent ordering of supertypes

* Eagerly load supertype typespecs for reuse

* Add regression test for https://github.com/square/moshi/issues/1022

* Add MoshiCachedClassInspector, wire in

* Check visibility of enclosed types too

Resolves #1022

* Fix comments
This commit is contained in:
Zac Sweers
2019-11-16 22:27:25 -05:00
committed by Jesse Wilson
parent ca207b2e06
commit 4241918d6b
6 changed files with 136 additions and 44 deletions

View File

@@ -30,8 +30,8 @@ internal class AppliedType private constructor(
/** Returns all supertypes of this, recursively. Includes both interface and class supertypes. */ /** Returns all supertypes of this, recursively. Includes both interface and class supertypes. */
fun supertypes( fun supertypes(
types: Types, types: Types,
result: MutableSet<AppliedType> = mutableSetOf() result: LinkedHashSet<AppliedType> = LinkedHashSet()
): Set<AppliedType> { ): LinkedHashSet<AppliedType> {
result.add(this) result.add(this)
for (supertype in types.directSupertypes(mirror)) { for (supertype in types.directSupertypes(mirror)) {
val supertypeDeclaredType = supertype as DeclaredType val supertypeDeclaredType = supertype as DeclaredType

View File

@@ -18,6 +18,7 @@ package com.squareup.moshi.kotlin.codegen
import com.google.auto.service.AutoService import com.google.auto.service.AutoService
import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.classinspector.elements.ElementsClassInspector
import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.squareup.moshi.kotlin.codegen.api.AdapterGenerator import com.squareup.moshi.kotlin.codegen.api.AdapterGenerator
@@ -95,6 +96,8 @@ class JsonClassCodegenProcessor : AbstractProcessor() {
} }
override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean { override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
val classInspector = ElementsClassInspector.create(elements, types)
val cachedClassInspector = MoshiCachedClassInspector(classInspector)
for (type in roundEnv.getElementsAnnotatedWith(annotation)) { for (type in roundEnv.getElementsAnnotatedWith(annotation)) {
if (type !is TypeElement) { if (type !is TypeElement) {
messager.printMessage( messager.printMessage(
@@ -104,7 +107,7 @@ class JsonClassCodegenProcessor : AbstractProcessor() {
} }
val jsonClass = type.getAnnotation(annotation) val jsonClass = type.getAnnotation(annotation)
if (jsonClass.generateAdapter && jsonClass.generator.isEmpty()) { if (jsonClass.generateAdapter && jsonClass.generator.isEmpty()) {
val generator = adapterGenerator(type) ?: continue val generator = adapterGenerator(type, cachedClassInspector) ?: continue
generator generator
.generateFile { .generateFile {
it.toBuilder() it.toBuilder()
@@ -129,8 +132,8 @@ class JsonClassCodegenProcessor : AbstractProcessor() {
return false return false
} }
private fun adapterGenerator(element: TypeElement): AdapterGenerator? { private fun adapterGenerator(element: TypeElement, cachedClassInspector: MoshiCachedClassInspector): AdapterGenerator? {
val type = targetType(messager, elements, types, element) ?: return null val type = targetType(messager, elements, types, element, cachedClassInspector) ?: return null
val properties = mutableMapOf<String, PropertyGenerator>() val properties = mutableMapOf<String, PropertyGenerator>()
for (property in type.properties.values) { for (property in type.properties.values) {

View File

@@ -0,0 +1,37 @@
package com.squareup.moshi.kotlin.codegen
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.TypeElement
/**
* This cached API over [ClassInspector] that caches certain lookups Moshi does potentially multiple
* times. This is useful mostly because it avoids duplicate reloads in cases like common base
* classes, common enclosing types, etc.
*/
internal class MoshiCachedClassInspector(private val classInspector: ClassInspector) {
private val elementToSpecCache = mutableMapOf<TypeElement, TypeSpec>()
private val kmClassToSpecCache = mutableMapOf<ImmutableKmClass, TypeSpec>()
private val metadataToKmClassCache = mutableMapOf<Metadata, ImmutableKmClass>()
fun toImmutableKmClass(metadata: Metadata): ImmutableKmClass {
return metadataToKmClassCache.getOrPut(metadata) {
metadata.toImmutableKmClass()
}
}
fun toTypeSpec(kmClass: ImmutableKmClass): TypeSpec {
return kmClassToSpecCache.getOrPut(kmClass) {
kmClass.toTypeSpec(classInspector)
}
}
fun toTypeSpec(element: TypeElement): TypeSpec {
return elementToSpecCache.getOrPut(element) {
toTypeSpec(toImmutableKmClass(element.metadata))
}
}
}

View File

@@ -18,27 +18,20 @@ package com.squareup.moshi.kotlin.codegen
import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName import com.squareup.kotlinpoet.asTypeName
import com.squareup.kotlinpoet.classinspector.elements.ElementsClassInspector
import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview
import com.squareup.kotlinpoet.metadata.isAbstract import com.squareup.kotlinpoet.metadata.isAbstract
import com.squareup.kotlinpoet.metadata.isClass import com.squareup.kotlinpoet.metadata.isClass
import com.squareup.kotlinpoet.metadata.isEnum import com.squareup.kotlinpoet.metadata.isEnum
import com.squareup.kotlinpoet.metadata.isInner import com.squareup.kotlinpoet.metadata.isInner
import com.squareup.kotlinpoet.metadata.isInternal
import com.squareup.kotlinpoet.metadata.isLocal import com.squareup.kotlinpoet.metadata.isLocal
import com.squareup.kotlinpoet.metadata.isPublic
import com.squareup.kotlinpoet.metadata.isSealed import com.squareup.kotlinpoet.metadata.isSealed
import com.squareup.kotlinpoet.metadata.specs.ClassInspector
import com.squareup.kotlinpoet.metadata.specs.TypeNameAliasTag import com.squareup.kotlinpoet.metadata.specs.TypeNameAliasTag
import com.squareup.kotlinpoet.metadata.specs.toTypeSpec
import com.squareup.kotlinpoet.metadata.toImmutableKmClass
import com.squareup.kotlinpoet.tag import com.squareup.kotlinpoet.tag
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonQualifier import com.squareup.moshi.JsonQualifier
@@ -55,12 +48,12 @@ import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target import java.lang.annotation.Target
import java.util.TreeSet import java.util.TreeSet
import javax.annotation.processing.Messager import javax.annotation.processing.Messager
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind import javax.lang.model.element.ElementKind
import javax.lang.model.element.TypeElement import javax.lang.model.element.TypeElement
import javax.lang.model.util.Elements import javax.lang.model.util.Elements
import javax.lang.model.util.Types import javax.lang.model.util.Types
import javax.tools.Diagnostic import javax.tools.Diagnostic
import kotlin.reflect.KClass
private val JSON_QUALIFIER = JsonQualifier::class.java private val JSON_QUALIFIER = JsonQualifier::class.java
private val JSON = Json::class.asClassName() private val JSON = Json::class.asClassName()
@@ -101,7 +94,9 @@ internal fun primaryConstructor(kotlinApi: TypeSpec, elements: Elements): Target
internal fun targetType(messager: Messager, internal fun targetType(messager: Messager,
elements: Elements, elements: Elements,
types: Types, types: Types,
element: TypeElement): TargetType? { element: TypeElement,
cachedClassInspector: MoshiCachedClassInspector
): TargetType? {
val typeMetadata = element.getAnnotation(Metadata::class.java) val typeMetadata = element.getAnnotation(Metadata::class.java)
if (typeMetadata == null) { if (typeMetadata == null) {
messager.printMessage( messager.printMessage(
@@ -111,7 +106,7 @@ internal fun targetType(messager: Messager,
} }
val kmClass = try { val kmClass = try {
typeMetadata.toImmutableKmClass() cachedClassInspector.toImmutableKmClass(typeMetadata)
} catch (e: UnsupportedOperationException) { } catch (e: UnsupportedOperationException) {
messager.printMessage( messager.printMessage(
Diagnostic.Kind.ERROR, "@JsonClass can't be applied to $element: must be a Class type", Diagnostic.Kind.ERROR, "@JsonClass can't be applied to $element: must be a Class type",
@@ -141,7 +136,8 @@ internal fun targetType(messager: Messager,
} }
kmClass.isSealed -> { kmClass.isSealed -> {
messager.printMessage( messager.printMessage(
Diagnostic.Kind.ERROR, "@JsonClass can't be applied to $element: must not be sealed", element) Diagnostic.Kind.ERROR, "@JsonClass can't be applied to $element: must not be sealed",
element)
return null return null
} }
kmClass.isAbstract -> { kmClass.isAbstract -> {
@@ -156,10 +152,16 @@ internal fun targetType(messager: Messager,
element) element)
return null return null
} }
!kmClass.isPublic && !kmClass.isInternal -> {
messager.printMessage(
Diagnostic.Kind.ERROR,
"@JsonClass can't be applied to $element: must be internal or public",
element)
return null
}
} }
val elementHandler = ElementsClassInspector.create(elements, types) val kotlinApi = cachedClassInspector.toTypeSpec(kmClass)
val kotlinApi = kmClass.toTypeSpec(elementHandler)
val typeVariables = kotlinApi.typeVariables val typeVariables = kotlinApi.typeVariables
val appliedType = AppliedType.get(element) val appliedType = AppliedType.get(element)
@@ -176,46 +178,62 @@ internal fun targetType(messager: Messager,
} }
val properties = mutableMapOf<String, TargetProperty>() val properties = mutableMapOf<String, TargetProperty>()
for (supertype in appliedType.supertypes(types)) { val superTypes = appliedType.supertypes(types)
if (supertype.element.asClassName() == OBJECT_CLASS) { .filterNot { supertype ->
continue // Don't load properties for java.lang.Object. supertype.element.asClassName() == OBJECT_CLASS || // Don't load properties for java.lang.Object.
} supertype.element.kind != ElementKind.CLASS // Don't load properties for interface types.
if (supertype.element.kind != ElementKind.CLASS) {
continue // Don't load properties for interface types.
} }
.onEach { supertype ->
if (supertype.element.getAnnotation(Metadata::class.java) == null) { if (supertype.element.getAnnotation(Metadata::class.java) == null) {
messager.printMessage(Diagnostic.Kind.ERROR, messager.printMessage(Diagnostic.Kind.ERROR,
"@JsonClass can't be applied to $element: supertype $supertype is not a Kotlin type", "@JsonClass can't be applied to $element: supertype $supertype is not a Kotlin type",
element) element)
return null return null
} }
val supertypeProperties = if (supertype.element == element) {
// We've already parsed this api above, reuse it
declaredProperties(supertype.element, constructor, elementHandler, kotlinApi)
} else {
declaredProperties(
supertype.element, constructor, elementHandler)
} }
.associateWithTo(LinkedHashMap()) { supertype ->
// Load the kotlin API cache into memory eagerly so we can reuse the parsed APIs
if (supertype.element == element) {
// We've already parsed this api above, reuse it
kotlinApi
} else {
cachedClassInspector.toTypeSpec(supertype.element)
}
}
for (supertypeApi in superTypes.values) {
val supertypeProperties = declaredProperties(constructor, supertypeApi)
for ((name, property) in supertypeProperties) { for ((name, property) in supertypeProperties) {
properties.putIfAbsent(name, property) properties.putIfAbsent(name, property)
} }
} }
val visibility = kotlinApi.modifiers.visibility()
// If any class in the enclosing class hierarchy is internal, they must all have internal
// generated adapters.
val resolvedVisibility = if (visibility == KModifier.INTERNAL) {
// Our nested type is already internal, no need to search
visibility
} else {
// Implicitly public, so now look up the hierarchy
val forceInternal = generateSequence<Element>(element) { it.enclosingElement }
.filterIsInstance<TypeElement>()
.map { cachedClassInspector.toImmutableKmClass(it.metadata) }
.any { it.isInternal }
if (forceInternal) KModifier.INTERNAL else visibility
}
return TargetType( return TargetType(
typeName = element.asType().asTypeName(), typeName = element.asType().asTypeName(),
constructor = constructor, constructor = constructor,
properties = properties, properties = properties,
typeVariables = typeVariables, typeVariables = typeVariables,
isDataClass = KModifier.DATA in kotlinApi.modifiers, isDataClass = KModifier.DATA in kotlinApi.modifiers,
visibility = kotlinApi.modifiers.visibility()) visibility = resolvedVisibility)
} }
/** Returns the properties declared by `typeElement`. */ /** Returns the properties declared by `typeElement`. */
@KotlinPoetMetadataPreview @KotlinPoetMetadataPreview
private fun declaredProperties( private fun declaredProperties(
typeElement: TypeElement,
constructor: TargetConstructor, constructor: TargetConstructor,
elementHandler: ClassInspector, kotlinApi: TypeSpec
kotlinApi: TypeSpec = typeElement.toTypeSpec(elementHandler)
): Map<String, TargetProperty> { ): Map<String, TargetProperty> {
val result = mutableMapOf<String, TargetProperty>() val result = mutableMapOf<String, TargetProperty>()
@@ -338,3 +356,9 @@ internal fun TypeName.unwrapTypeAlias(): TypeName {
} }
} }
} }
internal val TypeElement.metadata: Metadata
get() {
return getAnnotation(Metadata::class.java)
?: throw IllegalStateException("Not a kotlin type! $this")
}

View File

@@ -182,6 +182,20 @@ class JsonClassCodegenProcessorTest {
"error: @JsonClass can't be applied to LocalClass: must not be local") "error: @JsonClass can't be applied to LocalClass: must not be local")
} }
@Test fun privateClassesNotSupported() {
val result = compile(kotlin("source.kt",
"""
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
private class PrivateClass(val a: Int)
"""
))
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"error: @JsonClass can't be applied to PrivateClass: must be internal or public")
}
@Test fun objectDeclarationsNotSupported() { @Test fun objectDeclarationsNotSupported() {
val result = compile(kotlin("source.kt", val result = compile(kotlin("source.kt",
""" """

View File

@@ -1171,6 +1171,20 @@ class GeneratedAdaptersTest {
} }
} }
// Regression test for https://github.com/square/moshi/issues/1022
// Compile-only test
@JsonClass(generateAdapter = true)
internal data class MismatchParentAndNestedClassVisibility(
val type: Int,
val name: String? = null
) {
@JsonClass(generateAdapter = true)
data class NestedClass(
val nestedProperty: String
)
}
// Has to be outside to avoid Types seeing an owning class // Has to be outside to avoid Types seeing an owning class
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class NullableTypeParams<T>( data class NullableTypeParams<T>(