Skip to content

Commit 42e8ed7

Browse files
authored
Rework implementation to support nested classes (#8)
1 parent 09c66fd commit 42e8ed7

File tree

9 files changed

+683
-150
lines changed

9 files changed

+683
-150
lines changed

README.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
AutoValue Kotlin
22
================
33

4-
auto-value-kotlin (AVK) is an [AutoValue](https://github.com/google/auto) extension that generates
5-
binary-and-source-compatible, equivalent Kotlin `data` classes. This is intended to help migrations
6-
by doing 95% of the work and just letting the developer come through and clean up the generated file
7-
as-needed.
4+
auto-value-kotlin (AVK) is an [AutoValue](https://github.com/google/auto) extension + processor
5+
that generates binary-and-source-compatible, equivalent Kotlin `data` classes.
86

97
The intended use of this project is to ease migration from AutoValue classes to Kotlin data classes
108
and should be used ad-hoc rather than continuously. The idea is that it does 95% of the work for you
@@ -38,9 +36,10 @@ kapt {
3836
arg("avkTargets", "ClassOne:ClassTwo")
3937

4038
// Boolean option to ignore nested classes. By default, AVK will error out when it encounters
41-
// a nested AutoValue class as it has no means of safely converting the class since its
39+
// a nested non-AutoValue class as it has no means of safely converting the class since its
4240
// references are always qualified. This option can be set to true to make AVK just skip them
4341
// and emit a warning.
42+
// AVK will automatically convert nested AutoValue and enum classes along the way.
4443
// OPTIONAL. False by default.
4544
arg("avkIgnoreNested", "true")
4645
}
@@ -50,8 +49,8 @@ kapt {
5049
## Workflow
5150

5251
_Pre-requisites_
53-
* Move any nested AutoValue classes to top-level first (even if just temporarily for the migration).
54-
* You can optionally choose to ignore nested classes or only specific targets per the configuration
52+
* Move any nested non-AutoValue/non-enum classes to top-level first (even if just temporarily for the migration).
53+
* You can optionally choose to ignore nested non-AutoValue classes or only specific targets per the configuration
5554
options detailed in the Installation section above.
5655
* Ensure no classes outside of the original AutoValue class accesses its generated `AutoValue_` class.
5756
* Clean once to clear up any generated file references.

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ dependencies {
114114
implementation("com.squareup.moshi:moshi:$moshiVersion")
115115
implementation("com.google.auto.service:auto-service:1.0")
116116
implementation("com.squareup:kotlinpoet:1.10.1")
117+
implementation("com.squareup.okio:okio:3.0.0")
117118
implementation("com.google.auto.value:auto-value:1.8.2")
118119
implementation("com.google.auto.value:auto-value-annotations:1.8.2")
119120
testImplementation("junit:junit:4.13.2")

src/main/kotlin/com/slack/auto/value/kotlin/AutoValueKotlinExtension.kt

Lines changed: 63 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818
package com.slack.auto.value.kotlin
1919

20-
import com.google.auto.service.AutoService
20+
import com.google.auto.common.MoreElements
21+
import com.google.auto.common.MoreElements.isAnnotationPresent
22+
import com.google.auto.value.AutoValue
2123
import com.google.auto.value.extension.AutoValueExtension
2224
import com.google.auto.value.extension.AutoValueExtension.BuilderContext
2325
import com.slack.auto.value.kotlin.AvkBuilder.BuilderProperty
@@ -29,13 +31,17 @@ import com.squareup.kotlinpoet.FunSpec
2931
import com.squareup.kotlinpoet.KModifier
3032
import com.squareup.kotlinpoet.PropertySpec
3133
import com.squareup.kotlinpoet.TypeName
34+
import com.squareup.kotlinpoet.TypeSpec
3235
import com.squareup.kotlinpoet.asClassName
3336
import com.squareup.kotlinpoet.asTypeVariableName
3437
import com.squareup.kotlinpoet.joinToCode
3538
import com.squareup.moshi.Json
3639
import java.util.Locale
40+
import java.util.concurrent.ConcurrentHashMap
41+
import javax.annotation.processing.Messager
3742
import javax.annotation.processing.ProcessingEnvironment
3843
import javax.lang.model.element.Element
44+
import javax.lang.model.element.ElementKind
3945
import javax.lang.model.element.ExecutableElement
4046
import javax.lang.model.element.Modifier
4147
import javax.lang.model.element.NestingKind
@@ -44,8 +50,7 @@ import javax.lang.model.util.Elements
4450
import javax.lang.model.util.Types
4551
import javax.tools.Diagnostic
4652

47-
@AutoService(AutoValueExtension::class)
48-
public class AutoValueKotlinExtension : AutoValueExtension() {
53+
public class AutoValueKotlinExtension(private val realMessager: Messager) : AutoValueExtension() {
4954

5055
public companion object {
5156
// Options
@@ -54,6 +59,8 @@ public class AutoValueKotlinExtension : AutoValueExtension() {
5459
public const val OPT_IGNORE_NESTED: String = "avkIgnoreNested"
5560
}
5661

62+
internal val collectedKclassees = ConcurrentHashMap<ClassName, KotlinClass>()
63+
internal val collectedEnums = ConcurrentHashMap<ClassName, TypeSpec>()
5764
private lateinit var elements: Elements
5865
private lateinit var types: Types
5966

@@ -74,14 +81,7 @@ public class AutoValueKotlinExtension : AutoValueExtension() {
7481
}
7582

7683
private fun FunSpec.Builder.withDocsFrom(e: Element): FunSpec.Builder {
77-
return withDocsFrom(e) { parseDocs() }
78-
}
79-
80-
@Suppress("ReturnCount")
81-
private fun Element.parseDocs(): String? {
82-
val doc = elements.getDocComment(this)?.trim() ?: return null
83-
if (doc.isBlank()) return null
84-
return cleanUpDoc(doc)
84+
return withDocsFrom(e) { parseDocs(elements) }
8585
}
8686

8787
@Suppress("DEPRECATION", "LongMethod", "ComplexMethod", "NestedBlockDepth", "ReturnCount")
@@ -91,33 +91,36 @@ public class AutoValueKotlinExtension : AutoValueExtension() {
9191
classToExtend: String,
9292
isFinal: Boolean
9393
): String? {
94-
val targetClasses = context.processingEnvironment().options[OPT_TARGETS]
95-
?.splitToSequence(":")
96-
?.toSet()
97-
?: emptySet()
94+
val options = Options(context.processingEnvironment().options)
9895

9996
val ignoreNested =
10097
context.processingEnvironment().options[OPT_IGNORE_NESTED]?.toBoolean() ?: false
10198

102-
if (targetClasses.isNotEmpty() && context.autoValueClass().simpleName.toString() !in targetClasses) {
99+
if (options.targets.isNotEmpty() && context.autoValueClass().simpleName.toString() !in options.targets) {
103100
return null
104101
}
105102

106103
val avClass = context.autoValueClass()
107104

108-
if (avClass.nestingKind != NestingKind.TOP_LEVEL) {
109-
val diagnosticKind = if (ignoreNested) {
110-
Diagnostic.Kind.WARNING
111-
} else {
112-
Diagnostic.Kind.ERROR
105+
val isTopLevel = avClass.nestingKind == NestingKind.TOP_LEVEL
106+
if (!isTopLevel) {
107+
val isParentAv = isAnnotationPresent(
108+
MoreElements.asType(avClass.enclosingElement),
109+
AutoValue::class.java
110+
)
111+
if (!isParentAv) {
112+
val diagnosticKind = if (ignoreNested) {
113+
Diagnostic.Kind.WARNING
114+
} else {
115+
Diagnostic.Kind.ERROR
116+
}
117+
realMessager
118+
.printMessage(
119+
diagnosticKind,
120+
"Cannot convert nested classes to Kotlin safely. Please move this to top-level first.",
121+
avClass
122+
)
113123
}
114-
context.processingEnvironment().messager
115-
.printMessage(
116-
diagnosticKind,
117-
"Cannot convert nested classes to Kotlin safely. Please move this to top-level first.",
118-
avClass
119-
)
120-
return null
121124
}
122125

123126
// Check for non-builder nested classes, which cannot be converted with this
@@ -128,19 +131,32 @@ public class AutoValueKotlinExtension : AutoValueExtension() {
128131
.orElse(false)
129132
}
130133

131-
if (nonBuilderNestedTypes.isNotEmpty()) {
132-
nonBuilderNestedTypes.forEach {
133-
context.processingEnvironment().messager
134+
val (enums, nonEnums) = nonBuilderNestedTypes.partition { it.kind == ElementKind.ENUM }
135+
136+
val (nestedAvClasses, remainingTypes) = nonEnums.partition { isAnnotationPresent(it, AutoValue::class.java) }
137+
138+
if (remainingTypes.isNotEmpty()) {
139+
remainingTypes.forEach {
140+
realMessager
134141
.printMessage(
135142
Diagnostic.Kind.ERROR,
136-
"Cannot convert nested classes to Kotlin safely. Please move this to top-level first.",
143+
"Cannot convert non-autovalue nested classes to Kotlin safely. Please move this to top-level first.",
137144
it
138145
)
139146
}
140147
return null
141148
}
142149

143-
val classDoc = avClass.parseDocs()
150+
for (enumType in enums) {
151+
val (cn, spec) = EnumConversion.convert(
152+
elements,
153+
realMessager,
154+
enumType
155+
) ?: continue
156+
collectedEnums[cn] = spec
157+
}
158+
159+
val classDoc = avClass.parseDocs(elements)
144160

145161
var redactedClassName: ClassName? = null
146162

@@ -189,7 +205,7 @@ public class AutoValueKotlinExtension : AutoValueExtension() {
189205
isOverride = isAnOverride,
190206
isRedacted = isRedacted,
191207
visibility = if (Modifier.PUBLIC in method.modifiers) KModifier.PUBLIC else KModifier.INTERNAL,
192-
doc = method.parseDocs()
208+
doc = method.parseDocs(elements)
193209
)
194210
}
195211

@@ -229,14 +245,13 @@ public class AutoValueKotlinExtension : AutoValueExtension() {
229245
// Note we don't use context.propertyTypes() here because it doesn't contain nullability
230246
// info, which we did capture
231247
val propertyTypes = properties.mapValues { it.value.type }
232-
avkBuilder = AvkBuilder.from(builder, propertyTypes) { parseDocs() }
248+
avkBuilder = AvkBuilder.from(builder, propertyTypes) { parseDocs(elements) }
233249

234250
builderFactories += builder.builderMethods()
235251
builderFactorySpecs += builder.builderMethods()
236252
.map {
237253
FunSpec.copyOf(it)
238254
.withDocsFrom(it)
239-
.addModifiers(avkBuilder.visibility)
240255
.addStatement("TODO(%S)", "Replace this with the implementation from the source class")
241256
.build()
242257
}
@@ -387,22 +402,19 @@ public class AutoValueKotlinExtension : AutoValueExtension() {
387402
initializer("TODO()")
388403
}
389404

390-
field.parseDocs()?.let { addKdoc(it) }
405+
field.parseDocs(elements)?.let { addKdoc(it) }
391406
}
392407
.build()
393408
}
394409

395410
val superclass = avClass.superclass.asSafeTypeName()
396411
.takeUnless { it == ClassName("java.lang", "Object") }
397412

398-
val srcDir =
399-
context.processingEnvironment().options[OPT_SRC] ?: error("Missing src dir option")
400-
401-
KotlinClass(
413+
val kClass = KotlinClass(
402414
packageName = context.packageName(),
403415
doc = classDoc,
404416
name = avClass.simpleName.toString(),
405-
visibility = if (Modifier.PUBLIC in avClass.modifiers) KModifier.PUBLIC else KModifier.INTERNAL,
417+
visibility = avClass.visibility,
406418
isRedacted = isClassRedacted,
407419
isParcelable = isParcelable,
408420
superClass = superclass,
@@ -417,8 +429,14 @@ public class AutoValueKotlinExtension : AutoValueExtension() {
417429
remainingMethods = remainingMethods,
418430
classAnnotations = avClass.classAnnotations(),
419431
redactedClassName = redactedClassName,
420-
staticConstants = staticConstants
421-
).writeTo(srcDir, context.processingEnvironment().messager)
432+
staticConstants = staticConstants,
433+
isTopLevel = isTopLevel,
434+
children = nestedAvClasses
435+
.mapTo(LinkedHashSet()) { it.asClassName() }
436+
.plus(collectedEnums.keys)
437+
)
438+
439+
collectedKclassees[context.autoValueClass().asClassName()] = kClass
422440

423441
return null
424442
}
@@ -464,11 +482,7 @@ private fun AvkBuilder.Companion.from(
464482
return AvkBuilder(
465483
name = builderContext.builderType().simpleName.toString(),
466484
doc = builderContext.builderType().parseDocs(),
467-
visibility = if (Modifier.PUBLIC in builderContext.builderType().modifiers) {
468-
KModifier.PUBLIC
469-
} else {
470-
KModifier.INTERNAL
471-
},
485+
visibility = builderContext.builderType().visibility,
472486
builderProps = props,
473487
buildFun = builderContext.buildMethod()
474488
.map {

0 commit comments

Comments
 (0)