diff --git a/src/main/kotlin/com/slack/auto/value/kotlin/AutoValueKotlinExtension.kt b/src/main/kotlin/com/slack/auto/value/kotlin/AutoValueKotlinExtension.kt index 4d22480..75e94a8 100644 --- a/src/main/kotlin/com/slack/auto/value/kotlin/AutoValueKotlinExtension.kt +++ b/src/main/kotlin/com/slack/auto/value/kotlin/AutoValueKotlinExtension.kt @@ -448,14 +448,24 @@ private fun AvkBuilder.Companion.from( propertyTypes: Map, parseDocs: Element.() -> String? ): AvkBuilder { + + // Merge the set of all properties with setters and builders + val allProperties = builderContext.setters().keys + builderContext.propertyBuilders().keys + // Setters val props = - builderContext.setters().entries.map { (prop, setters) -> + allProperties.map { prop -> + val setters = builderContext.setters()[prop] ?: emptyList() + val propertyBuilder = + builderContext.propertyBuilders()[prop]?.let { propertyBuilder -> + FunSpec.copyOf(propertyBuilder).withDocsFrom(propertyBuilder, parseDocs).build() + } val type = propertyTypes.getValue(prop) BuilderProperty( prop, type, - setters.mapTo(LinkedHashSet()) { FunSpec.copyOf(it).withDocsFrom(it, parseDocs).build() } + setters.mapTo(LinkedHashSet()) { FunSpec.copyOf(it).withDocsFrom(it, parseDocs).build() }, + propertyBuilder ) } @@ -466,11 +476,12 @@ private fun AvkBuilder.Companion.from( builderMethods += builderContext.buildMethod().get() } - // TODO propertyBuilders + val propertyBuilders = builderContext.propertyBuilders().values.toSet() val remainingMethods = ElementFilter.methodsIn(builderContext.builderType().enclosedElements) .asSequence() + .filterNot { it in propertyBuilders } .filterNot { it in builderMethods } .filterNot { it == builderContext.autoBuildMethod() } .map { "${it.modifiers.joinToString(" ")} ${it.returnType} ${it.simpleName}(...)" } diff --git a/src/main/kotlin/com/slack/auto/value/kotlin/AvkBuilder.kt b/src/main/kotlin/com/slack/auto/value/kotlin/AvkBuilder.kt index 88c08ea..a929ff5 100644 --- a/src/main/kotlin/com/slack/auto/value/kotlin/AvkBuilder.kt +++ b/src/main/kotlin/com/slack/auto/value/kotlin/AvkBuilder.kt @@ -23,6 +23,7 @@ import com.squareup.kotlinpoet.KModifier.INTERNAL import com.squareup.kotlinpoet.KModifier.PRIVATE import com.squareup.kotlinpoet.NOTHING import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeSpec @@ -42,7 +43,7 @@ public data class AvkBuilder( val classAnnotations: List ) { - @Suppress("LongMethod") + @Suppress("LongMethod", "CyclomaticComplexMethod", "NestedBlockDepth") public fun createType(messager: Messager): TypeSpec { val builder = TypeSpec.classBuilder(name).addModifiers(visibility).addAnnotations(classAnnotations) @@ -58,7 +59,9 @@ public data class AvkBuilder( val propsToCreateWith = mutableListOf() - for ((propName, type, setters) in builderProps) { + @Suppress("DestructuringDeclarationWithTooManyEntries") + for (builderProp in builderProps) { + val (propName, type, setters, propertyBuilder) = builderProp // Add param to constructor val defaultValue = if (type.isNullable) { @@ -82,6 +85,65 @@ public data class AvkBuilder( .build() builder.addProperty(propSpec) + if (propertyBuilder != null) { + val builderPropSpec = + PropertySpec.builder( + builderProp.builderPropName, + propertyBuilder.returnType.copy(nullable = true) + ) + .addModifiers(PRIVATE) + .mutable() + .initializer("null") + .build() + builder.addProperty(builderPropSpec) + + // Fill in the builder body + // Example: + // if (reactionsBuilder$ == null) { + // reactionsBuilder$ = ImmutableList.builder(); + // } + // return reactionsBuilder$; + val rawType = + if (propSpec.type is ParameterizedTypeName) { + (propSpec.type as ParameterizedTypeName).rawType + } else { + propSpec.type + } + val nonNullType = rawType.copy(nullable = false) + val funSpec = + propertyBuilder + .toBuilder() + .beginControlFlow("if (%N == null)", builderPropSpec) + .apply { + addStatement("%N·= %T.builder()", builderPropSpec, nonNullType) + if (setters.isNotEmpty()) { + // Add the previous set value if one is present + // if (files == null) { + // filesBuilder$ = ImmutableList.builder(); + // } else { + // filesBuilder$ = ImmutableList.builder(); + // filesBuilder$.addAll(files); + // files = null; + // } + beginControlFlow("if (%N != null)", propSpec) + // TODO hacky but works for our cases + val addMethod = + if (type.toString().contains("Map")) { + "putAll" + } else { + "addAll" + } + addStatement("%N!!.$addMethod(%N)", builderPropSpec, propSpec) + addStatement("%N = null", propSpec) + endControlFlow() + } + } + .endControlFlow() + .addStatement("return·%N!!", builderPropSpec) + .build() + builder.addFunction(funSpec) + } + // Add build() assignment block val extraCheck = if (type.isNullable || !useNullablePropType) { @@ -92,14 +154,34 @@ public data class AvkBuilder( propsToCreateWith += CodeBlock.of("%1N·=·%1N%2L", propSpec, extraCheck) for (setter in setters) { + // TODO if there's a builder, check the builder is null first if (setter.parameters.size != 1) { messager.printMessage(WARNING, "Setter with surprising params: ${setter.name}") } + val setterBlock = CodeBlock.of("this.%N·= %N", propSpec, setter.parameters[0]) val setterSpec = setter .toBuilder() - // Assume this is a normal setter - .addStatement("return·apply·{·this.%N·= %N }", propSpec, setter.parameters[0]) + .apply { + if (propertyBuilder != null) { + // Need to check if the builder is null + // if (reactionsBuilder$ != null) { + // throw new IllegalStateException("Cannot set reactions after calling + // reactionsBuilder()"); + // } + beginControlFlow("check(%N == null)", builderProp.builderPropName) + addStatement( + "%S", + "Cannot set ${propSpec.name} after calling ${builderProp.builderPropName}()" + ) + endControlFlow() + addStatement("%L", setterBlock) + addStatement("return·this") + } else { + // Assume this is a normal setter + addStatement("return·apply·{·%L }", setterBlock) + } + } .build() builder.addFunction(setterSpec) } @@ -113,11 +195,32 @@ public data class AvkBuilder( builder.addFunction( autoBuildFun .toBuilder() - .addStatement( - "return·%T(%L)", - autoBuildFun.returnType!!, - propsToCreateWith.joinToCode(",·") - ) + .apply { + // For all builder types, we need to init or assign them first + // Example: + // if (reactionsBuilder$ != null) { + // this.reactions = reactionsBuilder$.build(); + // } else if (this.reactions == null) { + // this.reactions = ImmutableList.of(); + // } + for (builderProp in builderProps) { + if (builderProp.builder != null) { + beginControlFlow("if (%N != null)", builderProp.builderPropName) + addStatement("this.%N = %N!!.build()", builderProp.name, builderProp.builderPropName) + // property builders can never be nullable + nextControlFlow("else if (this.%N == null)", builderProp.name) + val rawType = + if (builderProp.type is ParameterizedTypeName) { + builderProp.type.rawType + } else { + builderProp.type + } + addStatement("this.%N = %T.of()", builderProp.name, rawType) + endControlFlow() + } + } + } + .addStatement("return·%T(%L)", autoBuildFun.returnType, propsToCreateWith.joinToCode(",·")) .build() ) @@ -145,8 +248,11 @@ public data class AvkBuilder( public data class BuilderProperty( val name: String, val type: TypeName, - val setters: Set - ) + val setters: Set, + val builder: FunSpec?, + ) { + val builderPropName: String = "${name}Builder" + } // Public for extension public companion object diff --git a/src/main/kotlin/com/slack/auto/value/kotlin/utils.kt b/src/main/kotlin/com/slack/auto/value/kotlin/utils.kt index 90fe141..8ce876f 100644 --- a/src/main/kotlin/com/slack/auto/value/kotlin/utils.kt +++ b/src/main/kotlin/com/slack/auto/value/kotlin/utils.kt @@ -101,6 +101,7 @@ internal const val MAX_PARAMS = 7 internal val JSON_CN = Json::class.asClassName() internal val JSON_CLASS_CN = JsonClass::class.asClassName() +@OptIn(DelicateKotlinPoetApi::class) @ExperimentalAvkApi public fun TypeMirror.asSafeTypeName(): TypeName { return asTypeName().copy(nullable = false).normalize() @@ -135,7 +136,7 @@ public fun TypeName.normalize(): TypeName { @ExperimentalAvkApi public fun TypeElement.classAnnotations(): List { return annotationMirrors - .map { @Suppress("DEPRECATION") AnnotationSpec.get(it) } + .map { AnnotationSpec.get(it) } .filterNot { (it.typeName as ClassName).packageName == "com.google.auto.value" } .filterNot { (it.typeName as ClassName).simpleName == "Metadata" } .map { spec -> @@ -175,7 +176,7 @@ public fun deprecatedAnnotation(message: String, replaceWith: String): Annotatio .build() } -@Suppress("DEPRECATION", "SpreadOperator") +@Suppress("SpreadOperator") @ExperimentalAvkApi public fun FunSpec.Companion.copyOf(method: ExecutableElement): FunSpec.Builder { var modifiers: Set = method.modifiers @@ -216,7 +217,6 @@ public fun ParameterSpec.Companion.parametersWithNullabilityOf( method: ExecutableElement ): List = method.parameters.map(ParameterSpec.Companion::getWithNullability) -@Suppress("DEPRECATION") @ExperimentalAvkApi public fun ParameterSpec.Companion.getWithNullability(element: VariableElement): ParameterSpec { val name = element.simpleName.toString() diff --git a/src/test/kotlin/com/slack/auto/value/kotlin/AutoValueKotlinExtensionTest.kt b/src/test/kotlin/com/slack/auto/value/kotlin/AutoValueKotlinExtensionTest.kt index 7489da9..89036a1 100644 --- a/src/test/kotlin/com/slack/auto/value/kotlin/AutoValueKotlinExtensionTest.kt +++ b/src/test/kotlin/com/slack/auto/value/kotlin/AutoValueKotlinExtensionTest.kt @@ -57,6 +57,7 @@ class AutoValueKotlinExtensionTest { import com.squareup.moshi.Json; import com.squareup.moshi.JsonClass; import java.util.List; + import com.google.common.collect.ImmutableList; import org.jetbrains.annotations.Nullable; @JsonClass(generateAdapter = true, generator = "avm") @@ -76,6 +77,8 @@ class AutoValueKotlinExtensionTest { @Nullable abstract List nullableCollection(); + abstract ImmutableList requiredBuildableCollection(); + abstract boolean aBoolean(); abstract char aChar(); abstract byte aByte(); @@ -110,6 +113,8 @@ class AutoValueKotlinExtensionTest { abstract Builder nullableValue(@Nullable String value); abstract Builder collection(List value); abstract Builder nullableCollection(@Nullable List value); + abstract Builder requiredBuildableCollection(ImmutableList value); + abstract ImmutableList.Builder requiredBuildableCollectionBuilder(); abstract Builder aBoolean(boolean value); abstract Builder aChar(char value); abstract Builder aByte(byte value); @@ -140,6 +145,7 @@ class AutoValueKotlinExtensionTest { package test import android.os.Parcelable + import com.google.common.collect.ImmutableList import com.slack.auto.`value`.kotlin.Redacted import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -160,6 +166,8 @@ class AutoValueKotlinExtensionTest { val collection: List, @get:JvmName("nullableCollection") val nullableCollection: List? = null, + @get:JvmName("requiredBuildableCollection") + val requiredBuildableCollection: ImmutableList, @get:JvmName("aBoolean") val aBoolean: Boolean = false, @get:JvmName("aChar") @@ -224,6 +232,17 @@ class AutoValueKotlinExtensionTest { TODO("Remove this function. Use the above line to auto-migrate.") } + @JvmSynthetic + @JvmName("-requiredBuildableCollection") + @Deprecated( + message = "Use the property", + replaceWith = ReplaceWith("requiredBuildableCollection"), + ) + fun requiredBuildableCollection(): ImmutableList { + requiredBuildableCollection() + TODO("Remove this function. Use the above line to auto-migrate.") + } + @JvmSynthetic @JvmName("-aBoolean") @Deprecated( @@ -330,7 +349,7 @@ class AutoValueKotlinExtensionTest { } internal fun toBuilder(): Builder = - Builder(value = value, nullableValue = nullableValue, collection = collection, nullableCollection = nullableCollection, aBoolean = aBoolean, aChar = aChar, aByte = aByte, aShort = aShort, aInt = aInt, aFloat = aFloat, aLong = aLong, aDouble = aDouble, redactedString = redactedString) + Builder(value = value, nullableValue = nullableValue, collection = collection, nullableCollection = nullableCollection, requiredBuildableCollection = requiredBuildableCollection, aBoolean = aBoolean, aChar = aChar, aByte = aByte, aShort = aShort, aInt = aInt, aFloat = aFloat, aLong = aLong, aDouble = aDouble, redactedString = redactedString) @Suppress("LongParameterList") internal class Builder internal constructor( @@ -338,6 +357,7 @@ class AutoValueKotlinExtensionTest { private var nullableValue: String? = null, private var collection: List? = null, private var nullableCollection: List? = null, + private var requiredBuildableCollection: ImmutableList? = null, private var aBoolean: Boolean = false, private var aChar: Char = 0.toChar(), private var aByte: Byte = 0.toByte(), @@ -348,6 +368,8 @@ class AutoValueKotlinExtensionTest { private var aDouble: Double = 0.0, private var redactedString: String? = null, ) { + private var requiredBuildableCollectionBuilder: ImmutableList.Builder? = null + internal fun `value`(`value`: String): Builder = apply { this.`value` = `value` } internal fun nullableValue(`value`: String?): Builder = apply { this.nullableValue = `value` } @@ -357,6 +379,25 @@ class AutoValueKotlinExtensionTest { internal fun nullableCollection(`value`: List?): Builder = apply { this.nullableCollection = `value` } + internal fun requiredBuildableCollectionBuilder(): ImmutableList.Builder { + if (requiredBuildableCollectionBuilder == null) { + requiredBuildableCollectionBuilder = ImmutableList.builder() + if (requiredBuildableCollection != null) { + requiredBuildableCollectionBuilder!!.addAll(requiredBuildableCollection) + requiredBuildableCollection = null + } + } + return requiredBuildableCollectionBuilder!! + } + + internal fun requiredBuildableCollection(`value`: ImmutableList): Builder { + check(requiredBuildableCollectionBuilder == null) { + "Cannot set requiredBuildableCollection after calling requiredBuildableCollectionBuilder()" + } + this.requiredBuildableCollection = `value` + return this + } + internal fun aBoolean(`value`: Boolean): Builder = apply { this.aBoolean = `value` } internal fun aChar(`value`: Char): Builder = apply { this.aChar = `value` } @@ -375,8 +416,14 @@ class AutoValueKotlinExtensionTest { internal fun redactedString(`value`: String): Builder = apply { this.redactedString = `value` } - internal fun build(): Example = - Example(`value` = `value` ?: error("value == null"), nullableValue = nullableValue, collection = collection ?: error("collection == null"), nullableCollection = nullableCollection, aBoolean = aBoolean, aChar = aChar, aByte = aByte, aShort = aShort, aInt = aInt, aFloat = aFloat, aLong = aLong, aDouble = aDouble, redactedString = redactedString ?: error("redactedString == null")) + internal fun build(): Example { + if (requiredBuildableCollectionBuilder != null) { + this.requiredBuildableCollection = requiredBuildableCollectionBuilder!!.build() + } else if (this.requiredBuildableCollection == null) { + this.requiredBuildableCollection = ImmutableList.of() + } + return Example(`value` = `value` ?: error("value == null"), nullableValue = nullableValue, collection = collection ?: error("collection == null"), nullableCollection = nullableCollection, requiredBuildableCollection = requiredBuildableCollection ?: error("requiredBuildableCollection == null"), aBoolean = aBoolean, aChar = aChar, aByte = aByte, aShort = aShort, aInt = aInt, aFloat = aFloat, aLong = aLong, aDouble = aDouble, redactedString = redactedString ?: error("redactedString == null")) + } fun placeholder(): Nothing { // TODO This is a placeholder to mention the following methods need to be moved manually over: