Codegen for parcelable/dataclass boilerplate
This is the initial implementation of the `codegen` cli utility
for in-place java boilerplate generation
See DataClass and SampleDataClass for documentation/guide/examples.
See tools/codegen/ for implementation and tests/Codegen/ for tests.
Bug: 64221737
Test: . frameworks/base/tests/Codegen/runTest.sh
Change-Id: I75177cb770f1beabc87dbae9e77ce4b93ca08e7f
diff --git a/tools/codegen/.gitignore b/tools/codegen/.gitignore
new file mode 100755
index 0000000..9fb18b4
--- /dev/null
+++ b/tools/codegen/.gitignore
@@ -0,0 +1,2 @@
+.idea
+out
diff --git a/tools/codegen/Android.bp b/tools/codegen/Android.bp
new file mode 100644
index 0000000..805b296
--- /dev/null
+++ b/tools/codegen/Android.bp
@@ -0,0 +1,18 @@
+java_binary_host {
+ name: "codegen",
+ manifest: "manifest.txt",
+ srcs: [
+ "src/**/*.kt",
+ ],
+ static_libs: [
+ "javaparser",
+ ],
+}
+
+java_library_host {
+ name: "codegen-version-info",
+
+ srcs: [
+ "src/**/SharedConstants.kt",
+ ],
+}
diff --git a/tools/codegen/OWNERS b/tools/codegen/OWNERS
new file mode 100644
index 0000000..da723b3
--- /dev/null
+++ b/tools/codegen/OWNERS
@@ -0,0 +1 @@
+eugenesusla@google.com
\ No newline at end of file
diff --git a/tools/codegen/manifest.txt b/tools/codegen/manifest.txt
new file mode 100644
index 0000000..6e1018b
--- /dev/null
+++ b/tools/codegen/manifest.txt
@@ -0,0 +1 @@
+Main-class: com.android.codegen.MainKt
diff --git a/tools/codegen/src/com/android/codegen/ClassInfo.kt b/tools/codegen/src/com/android/codegen/ClassInfo.kt
new file mode 100644
index 0000000..7ee79f6
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/ClassInfo.kt
@@ -0,0 +1,49 @@
+package com.android.codegen
+
+import com.github.javaparser.JavaParser
+import com.github.javaparser.ParseProblemException
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+
+open class ClassInfo(val sourceLines: List<String>) {
+
+ private val userSourceCode = (sourceLines + "}").joinToString("\n")
+ val fileAst = try {
+ JavaParser.parse(userSourceCode)!!
+ } catch (e: ParseProblemException) {
+ throw RuntimeException("Failed to parse code:\n" +
+ userSourceCode
+ .lines()
+ .mapIndexed { lnNum, ln -> "/*$lnNum*/$ln" }
+ .joinToString("\n"),
+ e)
+ }
+ val classAst = fileAst.types[0] as ClassOrInterfaceDeclaration
+
+ fun hasMethod(name: String, vararg argTypes: String): Boolean {
+ return classAst.methods.any {
+ it.name.asString() == name &&
+ it.parameters.map { it.type.asString() } == argTypes.toList()
+ }
+ }
+
+ val superInterfaces = (fileAst.types[0] as ClassOrInterfaceDeclaration)
+ .implementedTypes.map { it.asString() }
+
+ val ClassName = classAst.nameAsString
+ private val genericArgsAst = classAst.typeParameters
+ val genericArgs = if (genericArgsAst.isEmpty()) "" else {
+ genericArgsAst.map { it.nameAsString }.joinToString(", ").let { "<$it>" }
+ }
+ val ClassType = ClassName + genericArgs
+
+ val constDefs = mutableListOf<ConstDef>()
+
+ val fields = classAst.fields
+ .filterNot { it.isTransient || it.isStatic }
+ .mapIndexed { i, node -> FieldInfo(index = i, fieldAst = node, classInfo = this) }
+ .apply { lastOrNull()?.isLast = true }
+ val lazyTransientFields = classAst.fields
+ .filter { it.isTransient && !it.isStatic }
+ .mapIndexed { i, node -> FieldInfo(index = i, fieldAst = node, classInfo = this) }
+ .filter { hasMethod("lazyInit${it.NameUpperCamel}") }
+}
\ No newline at end of file
diff --git a/tools/codegen/src/com/android/codegen/ClassPrinter.kt b/tools/codegen/src/com/android/codegen/ClassPrinter.kt
new file mode 100644
index 0000000..33256b7
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/ClassPrinter.kt
@@ -0,0 +1,311 @@
+package com.android.codegen
+
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+import com.github.javaparser.ast.body.TypeDeclaration
+import com.github.javaparser.ast.expr.BooleanLiteralExpr
+import com.github.javaparser.ast.expr.NormalAnnotationExpr
+import com.github.javaparser.ast.type.ClassOrInterfaceType
+
+/**
+ * [ClassInfo] + utilities for printing out new class code with proper indentation and imports
+ */
+class ClassPrinter(
+ source: List<String>,
+ private val stringBuilder: StringBuilder,
+ var cliArgs: Array<String>
+) : ClassInfo(source) {
+
+ val GENERATED_MEMBER_HEADER by lazy { "@$GeneratedMember" }
+
+ // Imports
+ val NonNull by lazy { classRef("android.annotation.NonNull") }
+ val NonEmpty by lazy { classRef("android.annotation.NonEmpty") }
+ val Nullable by lazy { classRef("android.annotation.Nullable") }
+ val TextUtils by lazy { classRef("android.text.TextUtils") }
+ val LinkedHashMap by lazy { classRef("java.util.LinkedHashMap") }
+ val Collections by lazy { classRef("java.util.Collections") }
+ val Preconditions by lazy { classRef("com.android.internal.util.Preconditions") }
+ val ArrayList by lazy { classRef("java.util.ArrayList") }
+ val DataClass by lazy { classRef("com.android.internal.util.DataClass") }
+ val DataClassEnum by lazy { classRef("com.android.internal.util.DataClass.Enum") }
+ val ParcelWith by lazy { classRef("com.android.internal.util.DataClass.ParcelWith") }
+ val PluralOf by lazy { classRef("com.android.internal.util.DataClass.PluralOf") }
+ val Each by lazy { classRef("com.android.internal.util.DataClass.Each") }
+ val DataClassGenerated by lazy { classRef("com.android.internal.util.DataClass.Generated") }
+ val GeneratedMember by lazy { classRef("com.android.internal.util.DataClass.Generated.Member") }
+ val Parcelling by lazy { classRef("com.android.internal.util.Parcelling") }
+ val UnsupportedAppUsage by lazy { classRef("android.annotation.UnsupportedAppUsage") }
+
+
+ /**
+ * Optionally shortens a class reference if there's a corresponding import present
+ */
+ fun classRef(fullName: String): String {
+ if (cliArgs.contains(FLAG_NO_FULL_QUALIFIERS)) {
+ return fullName.split(".").dropWhile { it[0].isLowerCase() }.joinToString(".")
+ }
+
+ val pkg = fullName.substringBeforeLast(".")
+ val simpleName = fullName.substringAfterLast(".")
+ if (fileAst.imports.any { imprt ->
+ imprt.nameAsString == fullName
+ || (imprt.isAsterisk && imprt.nameAsString == pkg)
+ }) {
+ return simpleName
+ } else {
+ val outerClass = pkg.substringAfterLast(".", "")
+ if (outerClass.firstOrNull()?.isUpperCase() ?: false) {
+ return classRef(pkg) + "." + simpleName
+ }
+ }
+ return fullName
+ }
+
+ /** @see classRef */
+ inline fun <reified T : Any> classRef(): String {
+ return classRef(T::class.java.name)
+ }
+
+ /** @see classRef */
+ fun memberRef(fullName: String): String {
+ val className = fullName.substringBeforeLast(".")
+ val methodName = fullName.substringAfterLast(".")
+ return if (fileAst.imports.any {
+ it.isStatic
+ && (it.nameAsString == fullName
+ || (it.isAsterisk && it.nameAsString == className))
+ }) {
+ className.substringAfterLast(".") + "." + methodName
+ } else {
+ classRef(className) + "." + methodName
+ }
+ }
+
+ val dataClassAnnotationFeatures = classAst.annotations
+ .find { it.nameAsString == DataClass }
+ ?.let { it as? NormalAnnotationExpr }
+ ?.pairs
+ ?.map { pair -> pair.nameAsString to (pair.value as BooleanLiteralExpr).value }
+ ?.toMap()
+ ?: emptyMap()
+
+ val internalAnnotations = setOf(ParcelWith, DataClassEnum, PluralOf, Each, UnsupportedAppUsage)
+
+ /**
+ * @return whether the given feature is enabled
+ */
+ operator fun FeatureFlag.invoke(): Boolean {
+ if (cliArgs.contains("--no-$kebabCase")) return false
+ if (cliArgs.contains("--$kebabCase")) return true
+
+ val annotationKey = "gen$upperCamelCase"
+ if (dataClassAnnotationFeatures.containsKey(annotationKey)) {
+ return dataClassAnnotationFeatures[annotationKey]!!
+ }
+
+ if (cliArgs.contains("--all")) return true
+ if (hidden) return true
+
+ return when (this) {
+ FeatureFlag.SETTERS ->
+ !FeatureFlag.CONSTRUCTOR() && !FeatureFlag.BUILDER() && fields.any { !it.isFinal }
+ FeatureFlag.BUILDER -> cliArgs.contains(FLAG_BUILDER_PROTECTED_SETTERS) || onByDefault
+ FeatureFlag.CONSTRUCTOR -> !FeatureFlag.BUILDER()
+ FeatureFlag.PARCELABLE -> "Parcelable" in superInterfaces
+ FeatureFlag.AIDL -> FeatureFlag.PARCELABLE()
+ FeatureFlag.IMPLICIT_NONNULL -> fields.any { it.isNullable }
+ && fields.none { "@$NonNull" in it.annotations }
+ else -> onByDefault
+ }
+ }
+
+ val FeatureFlag.hidden
+ get(): Boolean = when {
+ cliArgs.contains("--hidden-$kebabCase") -> true
+ this == FeatureFlag.BUILD_UPON -> FeatureFlag.BUILDER.hidden
+ else -> false
+ }
+
+ var currentIndent = INDENT_SINGLE
+ private set
+
+ fun pushIndent() {
+ currentIndent += INDENT_SINGLE
+ }
+
+ fun popIndent() {
+ currentIndent = currentIndent.substring(0, currentIndent.length - INDENT_SINGLE.length)
+ }
+
+ fun backspace() = stringBuilder.setLength(stringBuilder.length - 1)
+ val lastChar get() = stringBuilder[stringBuilder.length - 1]
+
+ private fun appendRaw(s: String) {
+ stringBuilder.append(s)
+ }
+
+ fun append(s: String) {
+ if (s.isBlank() && s != "\n") {
+ appendRaw(s)
+ } else {
+ appendRaw(s.lines().map { line ->
+ if (line.startsWith(" *")) line else line.trimStart()
+ }.joinToString("\n$currentIndent"))
+ }
+ }
+
+ fun appendSameLine(s: String) {
+ while (lastChar.isWhitespace() || lastChar.isNewline()) {
+ backspace()
+ }
+ appendRaw(s)
+ }
+
+ fun rmEmptyLine() {
+ while (lastChar.isWhitespaceNonNewline()) backspace()
+ if (lastChar.isNewline()) backspace()
+ }
+
+ /**
+ * Syntactic sugar for:
+ * ```
+ * +"code()";
+ * ```
+ * to append the given string plus a newline
+ */
+ operator fun String.unaryPlus() = append("$this\n")
+
+ /**
+ * Syntactic sugar for:
+ * ```
+ * !"code()";
+ * ```
+ * to append the given string without a newline
+ */
+ operator fun String.not() = append(this)
+
+ /**
+ * Syntactic sugar for:
+ * ```
+ * ... {
+ * ...
+ * }+";"
+ * ```
+ * to append a ';' on same line after a block, and a newline afterwards
+ */
+ operator fun Unit.plus(s: String) {
+ appendSameLine(s)
+ +""
+ }
+
+ /**
+ * A multi-purpose syntactic sugar for appending the given string plus anything generated in
+ * the given [block], the latter with the appropriate deeper indent,
+ * and resetting the indent back to original at the end
+ *
+ * Usage examples:
+ *
+ * ```
+ * "if (...)" {
+ * ...
+ * }
+ * ```
+ * to append a corresponding if block appropriate indentation
+ *
+ * ```
+ * "void foo(...)" {
+ * ...
+ * }
+ * ```
+ * similar to the previous one, plus an extra empty line after the function body
+ *
+ * ```
+ * "void foo(" {
+ * <args code>
+ * }
+ * ```
+ * to use proper indentation for args code and close the bracket on same line at end
+ *
+ * ```
+ * "..." {
+ * ...
+ * }
+ * to use the correct indentation for inner code, resetting it at the end
+ */
+ inline operator fun String.invoke(block: ClassPrinter.() -> Unit) {
+ if (this == " {") {
+ appendSameLine(this)
+ } else {
+ append(this)
+ }
+ when {
+ endsWith("(") -> {
+ indentedBy(2, block)
+ appendSameLine(")")
+ }
+ endsWith("{") || endsWith(")") -> {
+ if (!endsWith("{")) appendSameLine(" {")
+ indentedBy(1, block)
+ +"}"
+ if ((endsWith(") {") || endsWith(")") || this == " {")
+ && !startsWith("synchronized")
+ && !startsWith("switch")
+ && !startsWith("if ")
+ && !contains(" else ")
+ && !contains("new ")
+ && !contains("return ")) {
+ +"" // extra line after function definitions
+ }
+ }
+ else -> indentedBy(2, block)
+ }
+ }
+
+ inline fun indentedBy(level: Int, block: ClassPrinter.() -> Unit) {
+ append("\n")
+ level times {
+ append(INDENT_SINGLE)
+ pushIndent()
+ }
+ block()
+ level times { popIndent() }
+ rmEmptyLine()
+ +""
+ }
+
+ inline fun Iterable<FieldInfo>.forEachTrimmingTrailingComma(b: FieldInfo.() -> Unit) {
+ forEachApply {
+ b()
+ if (isLast) {
+ while (lastChar == ' ' || lastChar == '\n') backspace()
+ if (lastChar == ',') backspace()
+ }
+ }
+ }
+
+ inline operator fun <R> invoke(f: ClassPrinter.() -> R): R = run(f)
+
+ var BuilderClass = CANONICAL_BUILDER_CLASS
+ var BuilderType = BuilderClass + genericArgs
+
+ init {
+ val builderFactoryOverride = classAst.methods.find {
+ it.isStatic && it.nameAsString == "builder"
+ }
+ if (builderFactoryOverride != null) {
+ BuilderClass = (builderFactoryOverride.type as ClassOrInterfaceType).nameAsString
+ BuilderType = builderFactoryOverride.type.asString()
+ } else {
+ val builderExtension = (fileAst.types
+ + classAst.childNodes.filterIsInstance(TypeDeclaration::class.java)).find {
+ it.nameAsString == CANONICAL_BUILDER_CLASS
+ }
+ if (builderExtension != null) {
+ BuilderClass = GENERATED_BUILDER_CLASS
+ val tp = (builderExtension as ClassOrInterfaceDeclaration).typeParameters
+ BuilderType = if (tp.isEmpty()) BuilderClass
+ else "$BuilderClass<${tp.map { it.nameAsString }.joinToString(", ")}>"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/codegen/src/com/android/codegen/ConstDef.kt b/tools/codegen/src/com/android/codegen/ConstDef.kt
new file mode 100644
index 0000000..f559d6f
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/ConstDef.kt
@@ -0,0 +1,17 @@
+package com.android.codegen
+
+import com.github.javaparser.ast.body.FieldDeclaration
+
+/**
+ * `@IntDef` or `@StringDef`
+ */
+data class ConstDef(val type: Type, val AnnotationName: String, val values: List<FieldDeclaration>) {
+
+ enum class Type {
+ INT, INT_FLAGS, STRING;
+
+ val isInt get() = this == INT || this == INT_FLAGS
+ }
+
+ val CONST_NAMES get() = values.flatMap { it.variables }.map { it.nameAsString }
+}
\ No newline at end of file
diff --git a/tools/codegen/src/com/android/codegen/FeatureFlag.kt b/tools/codegen/src/com/android/codegen/FeatureFlag.kt
new file mode 100644
index 0000000..24150d6
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/FeatureFlag.kt
@@ -0,0 +1,27 @@
+package com.android.codegen
+
+
+/**
+ * See also [ClassPrinter.invoke] for more default flag values resolution rules
+ */
+enum class FeatureFlag(val onByDefault: Boolean, val desc: String = "") {
+ PARCELABLE(false, "implement Parcelable contract"),
+ AIDL(false, "generate a 'parcelable declaration' .aidl file alongside"),
+ CONSTRUCTOR(true, "an all-argument constructor"),
+ BUILDER(false, "e.g. MyClass.builder().setFoo(..).build();"),
+ GETTERS(true, "getters, e.g. getFoo()"),
+ SETTERS(false, "chainable/fluent setters, e.g. setFoo(..).setBar(..)"),
+ WITHERS(false, "'immutable setters' returning a new instance, " +
+ "e.g. newFoo = foo.withBar(barValue)"),
+ EQUALS_HASH_CODE(false, "equals + hashCode based on fields"),
+ TO_STRING(false, "toString based on fields"),
+ BUILD_UPON(false, "builder factory from existing instance, " +
+ "e.g. instance.buildUpon().setFoo(..).build()"),
+ IMPLICIT_NONNULL(true, "treat lack of @Nullable as @NonNull for Object fields"),
+ COPY_CONSTRUCTOR(false, "a constructor for an instance identical to the given one"),
+ CONST_DEFS(true, "@Int/StringDef's based on declared static constants"),
+ FOR_EACH_FIELD(false, "forEachField((name, value) -> ...)");
+
+ val kebabCase = name.toLowerCase().replace("_", "-")
+ val upperCamelCase = name.split("_").map { it.toLowerCase().capitalize() }.joinToString("")
+}
diff --git a/tools/codegen/src/com/android/codegen/FieldInfo.kt b/tools/codegen/src/com/android/codegen/FieldInfo.kt
new file mode 100644
index 0000000..f326fd5
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/FieldInfo.kt
@@ -0,0 +1,216 @@
+package com.android.codegen
+
+import com.github.javaparser.JavaParser
+import com.github.javaparser.ast.body.FieldDeclaration
+import com.github.javaparser.ast.expr.ClassExpr
+import com.github.javaparser.ast.expr.Name
+import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr
+import com.github.javaparser.ast.expr.StringLiteralExpr
+import com.github.javaparser.ast.type.ArrayType
+import com.github.javaparser.ast.type.ClassOrInterfaceType
+import com.github.javaparser.javadoc.Javadoc
+import java.lang.Long
+
+data class FieldInfo(
+ val index: Int,
+ val fieldAst: FieldDeclaration,
+ private val classInfo: ClassInfo
+) {
+
+ val classPrinter = classInfo as ClassPrinter
+
+ // AST
+ internal val variableAst = fieldAst.variables[0]
+ val typeAst = variableAst.type
+
+ // Field type
+ val Type = typeAst.asString()
+ val FieldClass = Type.takeWhile { it != '<' }
+ val isPrimitive = Type in PRIMITIVE_TYPES
+
+ // Javadoc
+ val javadoc: Javadoc? = fieldAst.javadoc.orElse(null)
+ private val javadocText = javadoc?.toText()?.let {
+ // Workaround for a bug in Javaparser for javadocs starting with {
+ if (it.hasUnbalancedCurlyBrace()) "{$it" else it
+ }
+ val javadocTextNoAnnotationLines = javadocText
+ ?.lines()
+ ?.dropLastWhile { it.startsWith("@") || it.isBlank() }
+ ?.let { if (it.isEmpty()) null else it }
+ val javadocFull = javadocText
+ ?.trimBlankLines()
+ ?.mapLines { " * $this" }
+ ?.let { "/**\n$it\n */" }
+
+
+ // Field name
+ val name = variableAst.name.asString()!!
+ private val isNameHungarian = name[0] == 'm' && name[1].isUpperCase()
+ val NameUpperCamel = if (isNameHungarian) name.substring(1) else name.capitalize()
+ val nameLowerCamel = if (isNameHungarian) NameUpperCamel.decapitalize() else name
+ val _name = if (name != nameLowerCamel) nameLowerCamel else "_$nameLowerCamel"
+ val SingularNameOrNull by lazy {
+ classPrinter {
+ fieldAst.annotations
+ .find { it.nameAsString == PluralOf }
+ ?.let { it as? SingleMemberAnnotationExpr }
+ ?.memberValue
+ ?.let { it as? StringLiteralExpr }
+ ?.value
+ ?.toLowerCamel()
+ ?.capitalize()
+ }
+ }
+ val SingularName by lazy { SingularNameOrNull ?: NameUpperCamel }
+
+
+ // Field value
+ val mayBeNull: Boolean
+ get() = when {
+ isPrimitive -> false
+ "@${classPrinter.NonNull}" in annotations -> false
+ "@${classPrinter.NonEmpty}" in annotations -> false
+ isNullable -> true
+ lazyInitializer != null -> true
+ else -> classPrinter { !FeatureFlag.IMPLICIT_NONNULL() }
+ }
+ val lazyInitializer
+ get() = classInfo.classAst.methods.find { method ->
+ method.nameAsString == "lazyInit$NameUpperCamel" && method.parameters.isEmpty()
+ }?.nameAsString
+ val internalGetter get() = if (lazyInitializer != null) "get$NameUpperCamel()" else name
+ val defaultExpr: Any?
+ get() {
+ variableAst.initializer.orElse(null)?.let { return it }
+ classInfo.classAst.methods.find {
+ it.nameAsString == "default$NameUpperCamel" && it.parameters.isEmpty()
+ }?.run { "$nameAsString()" }?.let { return it }
+ if (FieldClass == "List") return "${classPrinter.memberRef("java.util.Collections.emptyList")}()"
+ return null
+ }
+ val hasDefault get() = defaultExpr != null
+
+
+ // Generic args
+ val isArray = Type.endsWith("[]")
+ val isList = FieldClass == "List" || FieldClass == "ArrayList"
+ val fieldBit = "0x${Long.toHexString(1L shl index)}"
+ var isLast = false
+ val isFinal = fieldAst.isFinal
+ val fieldTypeGenegicArgs = when (typeAst) {
+ is ArrayType -> listOf(fieldAst.elementType.asString())
+ is ClassOrInterfaceType -> {
+ typeAst.typeArguments.orElse(null)?.map { it.asString() } ?: emptyList()
+ }
+ else -> emptyList()
+ }
+ val FieldInnerType = fieldTypeGenegicArgs.firstOrNull()
+ val FieldInnerClass = FieldInnerType?.takeWhile { it != '<' }
+
+
+ // Annotations
+ var intOrStringDef = null as ConstDef?
+ val annotations by lazy {
+ if (FieldClass in BUILTIN_SPECIAL_PARCELLINGS) {
+ classPrinter {
+ fieldAst.addAnnotation(SingleMemberAnnotationExpr(
+ Name(ParcelWith),
+ ClassExpr(JavaParser.parseClassOrInterfaceType(
+ "$Parcelling.BuiltIn.For$FieldClass"))))
+ }
+ }
+ fieldAst.annotations.map { it.removeComment().toString() }
+ }
+ val annotationsNoInternal by lazy {
+ annotations.filterNot { ann ->
+ classPrinter {
+ internalAnnotations.any {
+ it in ann
+ }
+ }
+ }
+ }
+
+ fun hasAnnotation(a: String) = annotations.any { it.startsWith(a) }
+ val isNullable by lazy { hasAnnotation("@Nullable") }
+ val isNonEmpty by lazy { hasAnnotation("@${classPrinter.NonEmpty}") }
+ val customParcellingClass by lazy {
+ fieldAst.annotations.find { it.nameAsString == classPrinter.ParcelWith }
+ ?.singleArgAs<ClassExpr>()
+ ?.type
+ ?.asString()
+ }
+ val annotationsAndType by lazy { (annotationsNoInternal + Type).joinToString(" ") }
+ val sParcelling by lazy { customParcellingClass?.let { "sParcellingFor$NameUpperCamel" } }
+ val annotatedTypeForSetterParam by lazy {
+ (annotationsNoInternal + if (isArray) "$FieldInnerType..." else Type).joinToString(" ")
+ }
+
+ // Utilities
+
+ /**
+ * `mFoo.size()`
+ */
+ val ClassPrinter.sizeExpr get() = when {
+ isArray && FieldInnerClass !in PRIMITIVE_TYPES ->
+ memberRef("com.android.internal.util.ArrayUtils.size") + "($name)"
+ isArray -> "$name.length"
+ listOf("List", "Set", "Map").any { FieldClass.endsWith(it) } ->
+ memberRef("com.android.internal.util.CollectionUtils.size") + "($name)"
+ Type == "String" -> memberRef("android.text.TextUtils.length") + "($name)"
+ Type == "CharSequence" -> "$name.length()"
+ else -> "$name.size()"
+ }
+ /**
+ * `mFoo.get(0)`
+ */
+ fun elemAtIndexExpr(indexExpr: String) = when {
+ isArray -> "$name[$indexExpr]"
+ FieldClass == "ArraySet" -> "$name.valueAt($indexExpr)"
+ else -> "$name.get($indexExpr)"
+ }
+ /**
+ * `mFoo.isEmpty()`
+ */
+ val ClassPrinter.isEmptyExpr get() = when {
+ isArray || Type == "CharSequence" -> "$sizeExpr == 0"
+ else -> "$name.isEmpty()"
+ }
+
+ /**
+ * `mFoo == that` or `Objects.equals(mFoo, that)`, etc.
+ */
+ fun ClassPrinter.isEqualToExpr(that: String) = when {
+ Type in PRIMITIVE_TYPES -> "$internalGetter == $that"
+ isArray -> "${memberRef("java.util.Arrays.equals")}($internalGetter, $that)"
+ else -> "${memberRef("java.util.Objects.equals")}($internalGetter, $that)"
+ }
+
+ /**
+ * Parcel.write* and Parcel.read* method name wildcard values
+ */
+ val ParcelMethodsSuffix = when {
+ FieldClass in PRIMITIVE_TYPES - "char" - "boolean" +
+ listOf("String", "CharSequence", "Exception", "Size", "SizeF", "Bundle",
+ "FileDescriptor", "SparseBooleanArray", "SparseIntArray", "SparseArray") ->
+ FieldClass
+ FieldClass == "Map" && fieldTypeGenegicArgs[0] == "String" -> "Map"
+ isArray -> when {
+ FieldInnerType!! in (PRIMITIVE_TYPES + "String") -> FieldInnerType + "Array"
+ isBinder(FieldInnerType) -> "BinderArray"
+ else -> "TypedArray"
+ }
+ isList -> when {
+ FieldInnerType == "String" -> "StringList"
+ isBinder(FieldInnerType!!) -> "BinderList"
+ else -> "ParcelableList"
+ }
+ isIInterface(Type) -> "StrongInterface"
+ isBinder(Type) -> "StrongBinder"
+ else -> "TypedObject"
+ }.capitalize()
+
+ private fun isBinder(type: String) = type == "Binder" || type == "IBinder" || isIInterface(type)
+ private fun isIInterface(type: String) = type.length >= 2 && type[0] == 'I' && type[1].isUpperCase()
+}
\ No newline at end of file
diff --git a/tools/codegen/src/com/android/codegen/Generators.kt b/tools/codegen/src/com/android/codegen/Generators.kt
new file mode 100644
index 0000000..ab64f4e
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/Generators.kt
@@ -0,0 +1,847 @@
+package com.android.codegen
+
+import com.github.javaparser.ast.body.FieldDeclaration
+import com.github.javaparser.ast.body.VariableDeclarator
+import com.github.javaparser.ast.expr.*
+import java.io.File
+
+
+/**
+ * IntDefs and StringDefs based on constants
+ */
+fun ClassPrinter.generateConstDefs() {
+ val consts = classAst.fields.filter {
+ it.isStatic && it.isFinal && it.variables.all { variable ->
+ val initializer = variable.initializer.orElse(null)
+ val isLiteral = initializer is LiteralExpr
+ || (initializer is UnaryExpr && initializer.expression is LiteralExpr)
+ isLiteral && variable.type.asString() in listOf("int", "String")
+ }
+ }.flatMap { field -> field.variables.map { it to field } }
+ val intConsts = consts.filter { it.first.type.asString() == "int" }
+ val strConsts = consts.filter { it.first.type.asString() == "String" }
+ val intGroups = intConsts.groupBy { it.first.nameAsString.split("_")[0] }.values
+ val strGroups = strConsts.groupBy { it.first.nameAsString.split("_")[0] }.values
+ intGroups.forEach {
+ generateConstDef(it)
+ }
+ strGroups.forEach {
+ generateConstDef(it)
+ }
+}
+
+fun ClassPrinter.generateConstDef(consts: List<Pair<VariableDeclarator, FieldDeclaration>>) {
+ if (consts.size <= 1) return
+
+ val names = consts.map { it.first.nameAsString!! }
+ val prefix = names
+ .reduce { a, b -> a.commonPrefixWith(b) }
+ .dropLastWhile { it != '_' }
+ .dropLast(1)
+ if (prefix.isEmpty()) {
+ println("Failed to generate const def for $names")
+ return
+ }
+ var AnnotationName = prefix.split("_")
+ .filterNot { it.isBlank() }
+ .map { it.toLowerCase().capitalize() }
+ .joinToString("")
+ val annotatedConst = consts.find { it.second.annotations.isNonEmpty }
+ if (annotatedConst != null) {
+ AnnotationName = annotatedConst.second.annotations.first().nameAsString
+ }
+ val type = consts[0].first.type.asString()
+ val flag = type == "int" && consts.all { it.first.initializer.get().toString().startsWith("0x") }
+ val constDef = ConstDef(type = when {
+ type == "String" -> ConstDef.Type.STRING
+ flag -> ConstDef.Type.INT_FLAGS
+ else -> ConstDef.Type.INT
+ },
+ AnnotationName = AnnotationName,
+ values = consts.map { it.second }
+ )
+ constDefs += constDef
+ fields.forEachApply {
+ if (fieldAst.annotations.any { it.nameAsString == AnnotationName }) {
+ this.intOrStringDef = constDef
+ }
+ }
+
+ val visibility = if (consts[0].second.isPublic) "public" else "/* package-*/"
+
+ val Retention = classRef("java.lang.annotation.Retention")
+ val RetentionPolicySource = memberRef("java.lang.annotation.RetentionPolicy.SOURCE")
+ val ConstDef = classRef("android.annotation.${type.capitalize()}Def")
+
+ "@$ConstDef(${if_(flag, "flag = true, ")}prefix = \"${prefix}_\", value = {" {
+ names.forEachLastAware { name, isLast ->
+ +"$name${if_(!isLast, ",")}"
+ }
+ } + ")"
+ +"@$Retention($RetentionPolicySource)"
+ +GENERATED_MEMBER_HEADER
+ +"$visibility @interface $AnnotationName {}"
+ +""
+
+ if (type == "int") {
+ +GENERATED_MEMBER_HEADER
+ val methodDefLine = "$visibility static String ${AnnotationName.decapitalize()}ToString(" +
+ "@$AnnotationName int value)"
+ if (flag) {
+ val flg2str = memberRef("com.android.internal.util.BitUtils.flagsToString")
+ methodDefLine {
+ "return $flg2str(" {
+ +"value, $ClassName::single${AnnotationName}ToString"
+ } + ";"
+ }
+ +GENERATED_MEMBER_HEADER
+ !"static String single${AnnotationName}ToString(@$AnnotationName int value)"
+ } else {
+ !methodDefLine
+ }
+ " {" {
+ "switch (value) {" {
+ names.forEach { name ->
+ "case $name:" {
+ +"return \"$name\";"
+ }
+ }
+ +"default: return Integer.toHexString(value);"
+ }
+ }
+ }
+}
+
+fun ClassPrinter.generateAidl(javaFile: File) {
+ val aidl = File(javaFile.path.substringBeforeLast(".java") + ".aidl")
+ if (aidl.exists()) return
+ aidl.writeText(buildString {
+ sourceLines.dropLastWhile { !it.startsWith("package ") }.forEach {
+ appendln(it)
+ }
+ append("\nparcelable $ClassName;\n")
+ })
+}
+
+/**
+ * ```
+ * Foo newFoo = oldFoo.withBar(newBar);
+ * ```
+ */
+fun ClassPrinter.generateWithers() {
+ fields.forEachApply {
+ val metodName = "with$NameUpperCamel"
+ if (!hasMethod(metodName, Type)) {
+ generateFieldJavadoc(forceHide = FeatureFlag.WITHERS.hidden)
+ """@$NonNull
+ $GENERATED_MEMBER_HEADER
+ public $ClassType $metodName($annotatedTypeForSetterParam value)""" {
+ val changedFieldName = name
+
+ "return new $ClassType(" {
+ fields.forEachTrimmingTrailingComma {
+ if (name == changedFieldName) +"value," else +"$name,"
+ }
+ } + ";"
+ }
+ }
+ }
+}
+
+fun ClassPrinter.generateCopyConstructor() {
+ if (classAst.constructors.any {
+ it.parameters.size == 1 &&
+ it.parameters[0].type.asString() == ClassType
+ }) {
+ return
+ }
+
+ +"/** Copy constructor */"
+ +GENERATED_MEMBER_HEADER
+ "public $ClassName(@$NonNull $ClassName orig)" {
+ fields.forEachApply {
+ +"$name = orig.$name;"
+ }
+ }
+}
+
+/**
+ * ```
+ * Foo newFoo = oldFoo.buildUpon().setBar(newBar).build();
+ * ```
+ */
+fun ClassPrinter.generateBuildUpon() {
+ if (hasMethod("buildUpon")) return
+
+ +"/**"
+ +" * Provides an instance of {@link $BuilderClass} with state corresponding to this instance."
+ if (FeatureFlag.BUILD_UPON.hidden) {
+ +" * @hide"
+ }
+ +" */"
+ +GENERATED_MEMBER_HEADER
+ "public $BuilderType buildUpon()" {
+ "return new $BuilderType()" {
+ fields.forEachApply {
+ +".set$NameUpperCamel($internalGetter)"
+ } + ";"
+ }
+ }
+}
+
+fun ClassPrinter.generateBuilder() {
+ val setterVisibility = if (cliArgs.contains(FLAG_BUILDER_PROTECTED_SETTERS))
+ "protected" else "public"
+ val constructorVisibility = if (BuilderClass == CANONICAL_BUILDER_CLASS)
+ "public" else "/* package-*/"
+
+ val OneTimeUseBuilder = classRef("android.provider.OneTimeUseBuilder")
+
+ +"/**"
+ +" * A builder for {@link $ClassName}"
+ if (FeatureFlag.BUILDER.hidden) +" * @hide"
+ +" */"
+ +"@SuppressWarnings(\"WeakerAccess\")"
+ +GENERATED_MEMBER_HEADER
+ "public static class $BuilderClass$genericArgs" {
+ +"extends $OneTimeUseBuilder<$ClassType>"
+ }
+ " {" {
+
+ +""
+ fields.forEachApply {
+ +"protected $annotationsAndType $name;"
+ }
+ +""
+ +"protected long mBuilderFieldsSet = 0L;"
+ +""
+ +"$constructorVisibility $BuilderClass() {};"
+ +""
+
+ generateBuilderSetters(setterVisibility)
+
+ generateBuilderBuild()
+
+ rmEmptyLine()
+ }
+}
+
+private fun ClassPrinter.generateBuilderSetters(visibility: String) {
+
+ fields.forEachApply {
+ val maybeCast =
+ if_(BuilderClass != CANONICAL_BUILDER_CLASS, " ($CANONICAL_BUILDER_CLASS)")
+
+ generateFieldJavadoc()
+ +GENERATED_MEMBER_HEADER
+ "$visibility $CANONICAL_BUILDER_CLASS set$NameUpperCamel($annotatedTypeForSetterParam value)" {
+ +"checkNotUsed();"
+ +"mBuilderFieldsSet |= $fieldBit;"
+ +"$name = value;"
+ +"return$maybeCast this;"
+ }
+
+
+ val javadocSeeSetter = "/** @see #set$NameUpperCamel */"
+ val singularNameCustomizationHint = if (SingularNameOrNull == null) {
+ "// You can refine this method's name by providing item's singular name, e.g.:\n" +
+ "// @DataClass.PluralOf(\"item\")) mItems = ...\n\n"
+ } else ""
+
+ if (isList && FieldInnerType != null) {
+
+ +javadocSeeSetter
+ +GENERATED_MEMBER_HEADER
+ "$visibility $CANONICAL_BUILDER_CLASS add$SingularName(@$NonNull $FieldInnerType value)" {
+ !singularNameCustomizationHint
+ +"if ($name == null) set$NameUpperCamel(new $ArrayList<>());"
+ +"$name.add(value);"
+ +"return$maybeCast this;"
+ }
+ }
+
+ if (Type.contains("Map<")) {
+ val (Key, Value) = fieldTypeGenegicArgs
+
+ +javadocSeeSetter
+ +GENERATED_MEMBER_HEADER
+ "$visibility $CANONICAL_BUILDER_CLASS add$SingularName($Key key, $Value value)" {
+ !singularNameCustomizationHint
+ +"if ($name == null) set$NameUpperCamel(new $LinkedHashMap());"
+ +"$name.put(key, value);"
+ +"return$maybeCast this;"
+ }
+ }
+
+ if (Type == "boolean") {
+ +javadocSeeSetter
+ +GENERATED_MEMBER_HEADER
+ "$visibility $CANONICAL_BUILDER_CLASS mark$NameUpperCamel()" {
+ +"return set$NameUpperCamel(true);"
+ }
+
+ +javadocSeeSetter
+ +GENERATED_MEMBER_HEADER
+ "$visibility $CANONICAL_BUILDER_CLASS markNot$NameUpperCamel()" {
+ +"return set$NameUpperCamel(false);"
+ }
+ }
+ }
+}
+
+private fun ClassPrinter.generateBuilderBuild() {
+ +"/** Builds the instance. This builder should not be touched after calling this! */"
+ "public $ClassType build()" {
+ +"markUsed();"
+ fields.forEachApply {
+ if (!isNullable || hasDefault) {
+ "if ((mBuilderFieldsSet & $fieldBit) == 0)" {
+ if (!isNullable && !hasDefault) {
+ +"throw new IllegalStateException(\"Required field not set: $nameLowerCamel\");"
+ } else {
+ +"$name = $defaultExpr;"
+ }
+ }
+ }
+ }
+ "$ClassType o = new $ClassType(" {
+ fields.forEachTrimmingTrailingComma {
+ +"$name,"
+ }
+ } + ";"
+ +"return o;"
+ }
+}
+
+fun ClassPrinter.generateParcelable() {
+ val booleanFields = fields.filter { it.Type == "boolean" }
+ val objectFields = fields.filter { it.Type !in PRIMITIVE_TYPES }
+ val nullableFields = objectFields.filter { it.mayBeNull }
+ val nonBooleanFields = fields - booleanFields
+
+
+ val flagStorageType = when (fields.size) {
+ in 0..7 -> "byte"
+ in 8..15 -> "int"
+ in 16..31 -> "long"
+ else -> throw NotImplementedError("32+ field classes not yet supported")
+ }
+ val FlagStorageType = flagStorageType.capitalize()
+
+ fields.forEachApply {
+ if (sParcelling != null) {
+ +GENERATED_MEMBER_HEADER
+ "static $Parcelling<$Type> $sParcelling =" {
+ "$Parcelling.Cache.get(" {
+ +"$customParcellingClass.class"
+ } + ";"
+ }
+ "static {" {
+ "if ($sParcelling == null)" {
+ "$sParcelling = $Parcelling.Cache.put(" {
+ +"new $customParcellingClass()"
+ } + ";"
+ }
+ }
+ +""
+ }
+ }
+
+ val Parcel = classRef("android.os.Parcel")
+ if (!hasMethod("writeToParcel", Parcel, "int")) {
+ +"@Override"
+ +GENERATED_MEMBER_HEADER
+ "public void writeToParcel($Parcel dest, int flags)" {
+ +"// You can override field parcelling by defining methods like:"
+ +"// void parcelFieldName(Parcel dest, int flags) { ... }"
+ +""
+
+ if (booleanFields.isNotEmpty() || nullableFields.isNotEmpty()) {
+ +"$flagStorageType flg = 0;"
+ booleanFields.forEachApply {
+ +"if ($internalGetter) flg |= $fieldBit;"
+ }
+ nullableFields.forEachApply {
+ +"if ($internalGetter != null) flg |= $fieldBit;"
+ }
+ +"dest.write$FlagStorageType(flg);"
+ }
+
+ nonBooleanFields.forEachApply {
+ val customParcellingMethod = "parcel$NameUpperCamel"
+ when {
+ hasMethod(customParcellingMethod, Parcel, "int") ->
+ +"$customParcellingMethod(dest, flags);"
+ customParcellingClass != null -> +"$sParcelling.parcel($name, dest, flags);"
+ hasAnnotation("@$DataClassEnum") ->
+ +"dest.writeInt($internalGetter == null ? -1 : $internalGetter.ordinal());"
+ else -> {
+ if (mayBeNull) !"if ($internalGetter != null) "
+ var args = internalGetter
+ if (ParcelMethodsSuffix.startsWith("Parcelable")
+ || ParcelMethodsSuffix.startsWith("TypedObject")
+ || ParcelMethodsSuffix == "TypedArray") {
+ args += ", flags"
+ }
+ +"dest.write$ParcelMethodsSuffix($args);"
+ }
+ }
+ }
+ }
+ }
+
+ if (!hasMethod("describeContents")) {
+ +"@Override"
+ +GENERATED_MEMBER_HEADER
+ +"public int describeContents() { return 0; }"
+ +""
+ }
+
+ if (classAst.fields.none { it.variables[0].nameAsString == "CREATOR" }) {
+ val Creator = classRef("android.os.Parcelable.Creator")
+
+ +GENERATED_MEMBER_HEADER
+ "public static final @$NonNull $Creator<$ClassName> CREATOR" {
+ +"= new $Creator<$ClassName>()"
+ }; " {" {
+
+ +"@Override"
+ "public $ClassName[] newArray(int size)" {
+ +"return new $ClassName[size];"
+ }
+
+ +"@Override"
+ +"@SuppressWarnings({\"unchecked\", \"RedundantCast\"})"
+ "public $ClassName createFromParcel($Parcel in)" {
+ +"// You can override field unparcelling by defining methods like:"
+ +"// static FieldType unparcelFieldName(Parcel in) { ... }"
+ +""
+ if (booleanFields.isNotEmpty() || nullableFields.isNotEmpty()) {
+ +"$flagStorageType flg = in.read$FlagStorageType();"
+ }
+ booleanFields.forEachApply {
+ +"$Type $_name = (flg & $fieldBit) != 0;"
+ }
+ nonBooleanFields.forEachApply {
+
+ // Handle customized parceling
+ val customParcellingMethod = "unparcel$NameUpperCamel"
+ if (hasMethod(customParcellingMethod, Parcel)) {
+ +"$Type $_name = $customParcellingMethod(in);"
+ } else if (customParcellingClass != null) {
+ +"$Type $_name = $sParcelling.unparcel(in);"
+ } else if (hasAnnotation("@$DataClassEnum")) {
+ val ordinal = "${_name}Ordinal"
+ +"int $ordinal = in.readInt();"
+ +"$Type $_name = $ordinal < 0 ? null : $FieldClass.values()[$ordinal];"
+ } else {
+ val methodArgs = mutableListOf<String>()
+
+ // Create container if any
+ val containerInitExpr = when {
+ FieldClass.endsWith("Map") -> "new $LinkedHashMap<>()"
+ FieldClass == "List" || FieldClass == "ArrayList" ->
+ "new ${classRef("java.util.ArrayList")}<>()"
+// isArray && FieldInnerType in (PRIMITIVE_TYPES + "String") ->
+// "new $FieldInnerType[in.readInt()]"
+ else -> ""
+ }
+ val passContainer = containerInitExpr.isNotEmpty()
+
+ // nullcheck +
+ // "FieldType fieldName = (FieldType)"
+ if (passContainer) {
+ methodArgs.add(_name)
+ !"$Type $_name = "
+ if (mayBeNull) {
+ +"null;"
+ !"if ((flg & $fieldBit) != 0) {"
+ pushIndent()
+ +""
+ !"$_name = "
+ }
+ +"$containerInitExpr;"
+ } else {
+ !"$Type $_name = "
+ if (mayBeNull) !"(flg & $fieldBit) == 0 ? null : "
+ if (ParcelMethodsSuffix == "StrongInterface") {
+ !"$FieldClass.Stub.asInterface("
+ } else if (Type !in PRIMITIVE_TYPES + "String" + "Bundle" &&
+ (!isArray || FieldInnerType !in PRIMITIVE_TYPES + "String") &&
+ ParcelMethodsSuffix != "Parcelable") {
+ !"($Type) "
+ }
+ }
+
+ // Determine method args
+ when {
+ ParcelMethodsSuffix == "Parcelable" ->
+ methodArgs += "$FieldClass.class.getClassLoader()"
+ ParcelMethodsSuffix == "TypedObject" ->
+ methodArgs += "$FieldClass.CREATOR"
+ ParcelMethodsSuffix == "TypedArray" ->
+ methodArgs += "$FieldInnerClass.CREATOR"
+ ParcelMethodsSuffix.startsWith("Parcelable")
+ || FieldClass == "Map"
+ || (isList || isArray)
+ && FieldInnerType !in PRIMITIVE_TYPES + "String" ->
+ methodArgs += "$FieldInnerClass.class.getClassLoader()"
+ }
+
+ // ...in.readFieldType(args...);
+ when {
+ ParcelMethodsSuffix == "StrongInterface" -> !"in.readStrongBinder"
+ isArray -> !"in.create$ParcelMethodsSuffix"
+ else -> !"in.read$ParcelMethodsSuffix"
+ }
+ !"(${methodArgs.joinToString(", ")})"
+ if (ParcelMethodsSuffix == "StrongInterface") !")"
+ +";"
+
+ // Cleanup if passContainer
+ if (passContainer && mayBeNull) {
+ popIndent()
+ rmEmptyLine()
+ +"\n}"
+ }
+ }
+ }
+ "return new $ClassType(" {
+ fields.forEachTrimmingTrailingComma {
+ +"$_name,"
+ }
+ } + ";"
+ }
+ rmEmptyLine()
+ } + ";"
+ +""
+ }
+}
+
+fun ClassPrinter.generateEqualsHashcode() {
+ if (!hasMethod("equals", "Object")) {
+ +"@Override"
+ +GENERATED_MEMBER_HEADER
+ "public boolean equals(Object o)" {
+ +"// You can override field equality logic by defining either of the methods like:"
+ +"// boolean fieldNameEquals($ClassName other) { ... }"
+ +"// boolean fieldNameEquals(FieldType otherValue) { ... }"
+ +""
+ """if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ $ClassType that = ($ClassType) o;
+ //noinspection PointlessBooleanExpression
+ return true""" {
+ fields.forEachApply {
+ val sfx = if (isLast) ";" else ""
+ val customEquals = "${nameLowerCamel}Equals"
+ when {
+ hasMethod(customEquals, Type) -> +"&& $customEquals(that.$internalGetter)$sfx"
+ hasMethod(customEquals, ClassType) -> +"&& $customEquals(that)$sfx"
+ else -> +"&& ${isEqualToExpr("that.$internalGetter")}$sfx"
+ }
+ }
+ }
+ }
+ }
+
+ if (!hasMethod("hashCode")) {
+ +"@Override"
+ +GENERATED_MEMBER_HEADER
+ "public int hashCode()" {
+ +"// You can override field hashCode logic by defining methods like:"
+ +"// int fieldNameHashCode() { ... }"
+ +""
+ +"int _hash = 1;"
+ fields.forEachApply {
+ !"_hash = 31 * _hash + "
+ val customHashCode = "${nameLowerCamel}HashCode"
+ when {
+ hasMethod(customHashCode) -> +"$customHashCode();"
+ Type == "int" || Type == "byte" -> +"$internalGetter;"
+ Type in PRIMITIVE_TYPES -> +"${Type.capitalize()}.hashCode($internalGetter);"
+ isArray -> +"${memberRef("java.util.Arrays.hashCode")}($internalGetter);"
+ else -> +"${memberRef("java.util.Objects.hashCode")}($internalGetter);"
+ }
+ }
+ +"return _hash;"
+ }
+ }
+}
+
+//TODO support IntDef flags?
+fun ClassPrinter.generateToString() {
+ if (!hasMethod("toString")) {
+ +"@Override"
+ +GENERATED_MEMBER_HEADER
+ "public String toString()" {
+ +"// You can override field toString logic by defining methods like:"
+ +"// String fieldNameToString() { ... }"
+ +""
+ "return \"$ClassName { \" +" {
+ fields.forEachApply {
+ val customToString = "${nameLowerCamel}ToString"
+ val expr = when {
+ hasMethod(customToString) -> "$customToString()"
+ isArray -> "${memberRef("java.util.Arrays.toString")}($internalGetter)"
+ intOrStringDef?.type?.isInt == true ->
+ "${intOrStringDef!!.AnnotationName.decapitalize()}ToString($name)"
+ else -> internalGetter
+ }
+ +"\"$nameLowerCamel = \" + $expr${if_(!isLast, " + \", \"")} +"
+ }
+ }
+ +"\" }\";"
+ }
+ }
+}
+
+fun ClassPrinter.generateSetters() {
+ fields.forEachApply {
+ if (!hasMethod("set$NameUpperCamel", Type)
+ && !fieldAst.isPublic
+ && !isFinal) {
+
+ generateFieldJavadoc(forceHide = FeatureFlag.SETTERS.hidden)
+ +GENERATED_MEMBER_HEADER
+ "public $ClassType set$NameUpperCamel($annotatedTypeForSetterParam value)" {
+ generateSetFrom("value")
+ +"return this;"
+ }
+ }
+ }
+}
+
+fun ClassPrinter.generateGetters() {
+ (fields + lazyTransientFields).forEachApply {
+ val methodPrefix = if (Type == "boolean") "is" else "get"
+ val methodName = methodPrefix + NameUpperCamel
+
+ if (!hasMethod(methodName) && !fieldAst.isPublic) {
+
+ generateFieldJavadoc(forceHide = FeatureFlag.GETTERS.hidden)
+ +GENERATED_MEMBER_HEADER
+ "public $annotationsAndType $methodName()" {
+ if (lazyInitializer == null) {
+ +"return $name;"
+ } else {
+ +"$Type $_name = $name;"
+ "if ($_name == null)" {
+ if (fieldAst.isVolatile) {
+ "synchronized(this)" {
+ +"$_name = $name;"
+ "if ($_name == null)" {
+ +"$_name = $name = $lazyInitializer();"
+ }
+ }
+ } else {
+ +"// You can mark field as volatile for thread-safe double-check init"
+ +"$_name = $name = $lazyInitializer();"
+ }
+ }
+ +"return $_name;"
+ }
+ }
+ }
+ }
+}
+
+fun FieldInfo.generateFieldJavadoc(forceHide: Boolean = false) = classPrinter {
+ if (javadocFull != null || forceHide) {
+ var hidden = false
+ (javadocFull ?: "/**\n */").lines().forEach {
+ if (it.contains("@hide")) hidden = true
+ if (it.contains("*/") && forceHide && !hidden) {
+ if (javadocFull != null) +" *"
+ +" * @hide"
+ }
+ +it
+ }
+ }
+}
+
+fun FieldInfo.generateSetFrom(source: String) = classPrinter {
+ !"$name = "
+ if (Type in PRIMITIVE_TYPES || mayBeNull) {
+ +"$source;"
+ } else if (defaultExpr != null) {
+ "$source != null" {
+ +"? $source"
+ +": $defaultExpr;"
+ }
+ } else {
+ val checkNotNull = memberRef("com.android.internal.util.Preconditions.checkNotNull")
+ +"$checkNotNull($source);"
+ }
+ if (isNonEmpty) {
+ "if ($isEmptyExpr)" {
+ +"throw new IllegalArgumentException(\"$nameLowerCamel cannot be empty\");"
+ }
+ }
+}
+
+fun ClassPrinter.generateConstructor(visibility: String = "public") {
+ if (visibility == "public") {
+ generateConstructorJavadoc()
+ }
+ +GENERATED_MEMBER_HEADER
+ "$visibility $ClassName(" {
+ fields.forEachApply {
+ +"$annotationsAndType $nameLowerCamel${if_(!isLast, ",")}"
+ }
+ }
+ " {" {
+ fields.forEachApply {
+ !"this."
+ generateSetFrom(nameLowerCamel)
+ }
+
+ generateStateValidation()
+
+ generateOnConstructedCallback()
+ }
+}
+
+private fun ClassPrinter.generateConstructorJavadoc() {
+ if (fields.all { it.javadoc == null } && !FeatureFlag.CONSTRUCTOR.hidden) return
+ +"/**"
+ fields.filter { it.javadoc != null }.forEachApply {
+ javadocTextNoAnnotationLines?.apply {
+ +" * @param $nameLowerCamel"
+ forEach {
+ +" * $it"
+ }
+ }
+ }
+ if (FeatureFlag.CONSTRUCTOR.hidden) +" * @hide"
+ +" */"
+}
+
+private fun ClassPrinter.generateStateValidation() {
+ val Size = classRef("android.annotation.Size")
+ val knownNonValidationAnnotations = internalAnnotations + Nullable
+
+ val validate = memberRef("com.android.internal.util.AnnotationValidations.validate")
+ fun appendValidateCall(annotation: AnnotationExpr, valueToValidate: String) {
+ "$validate(" {
+ !"${annotation.nameAsString}.class, null, $valueToValidate"
+ val params = when (annotation) {
+ is MarkerAnnotationExpr -> emptyMap()
+ is SingleMemberAnnotationExpr -> mapOf("value" to annotation.memberValue)
+ is NormalAnnotationExpr ->
+ annotation.pairs.map { it.name.asString() to it.value }.toMap()
+ else -> throw IllegalStateException()
+ }
+ params.forEach { name, value ->
+ !",\n\"$name\", $value"
+ }
+ }
+ +";"
+ }
+
+ fields.forEachApply {
+ if (intOrStringDef != null) {
+ if (intOrStringDef!!.type == ConstDef.Type.INT_FLAGS) {
+ +""
+ +"//noinspection PointlessBitwiseExpression"
+ "$Preconditions.checkFlagsArgument(" {
+ "$name, 0" {
+ intOrStringDef!!.CONST_NAMES.forEach {
+ +"| $it"
+ }
+ }
+ }
+ +";"
+ } else {
+ +""
+ +"//noinspection PointlessBooleanExpression"
+ "if (true" {
+ intOrStringDef!!.CONST_NAMES.forEach { CONST_NAME ->
+ +"&& !(${isEqualToExpr(CONST_NAME)})"
+ }
+ }; rmEmptyLine(); ") {" {
+ "throw new ${classRef<IllegalArgumentException>()}(" {
+ "\"$nameLowerCamel was \" + $internalGetter + \" but must be one of: \"" {
+
+ intOrStringDef!!.CONST_NAMES.forEachLastAware { CONST_NAME, isLast ->
+ +"""+ "$CONST_NAME(" + $CONST_NAME + ")${if_(!isLast, ", ")}""""
+ }
+ }
+ }
+ +";"
+ }
+ }
+ }
+
+ val eachLine = fieldAst.annotations.find { it.nameAsString == Each }?.range?.orElse(null)?.end?.line
+ val perElementValidations = if (eachLine == null) emptyList() else fieldAst.annotations.filter {
+ it.nameAsString != Each &&
+ it.range.orElse(null)?.begin?.line?.let { it >= eachLine } ?: false
+ }
+
+ fieldAst.annotations.filterNot {
+ it.nameAsString == intOrStringDef?.AnnotationName
+ || it.nameAsString in knownNonValidationAnnotations
+ || it in perElementValidations
+ }.forEach { annotation ->
+ appendValidateCall(annotation,
+ valueToValidate = if (annotation.nameAsString == Size) sizeExpr else name)
+ }
+
+ if (perElementValidations.isNotEmpty()) {
+ +"int ${nameLowerCamel}Size = $sizeExpr;"
+ "for (int i = 0; i < ${nameLowerCamel}Size; i++) {" {
+ perElementValidations.forEach { annotation ->
+ appendValidateCall(annotation,
+ valueToValidate = elemAtIndexExpr("i"))
+ }
+ }
+ }
+ }
+}
+
+private fun ClassPrinter.generateOnConstructedCallback(prefix: String = "") {
+ +""
+ val call = "${prefix}onConstructed();"
+ if (hasMethod("onConstructed")) {
+ +call
+ } else {
+ +"// $call // You can define this method to get a callback"
+ }
+}
+
+fun ClassPrinter.generateForEachField() {
+ val specializations = listOf("Object", "int")
+ val usedSpecializations = fields.map { if (it.Type in specializations) it.Type else "Object" }
+ val usedSpecializationsSet = usedSpecializations.toSet()
+
+ val PerObjectFieldAction = classRef("com.android.internal.util.DataClass.PerObjectFieldAction")
+
+ +GENERATED_MEMBER_HEADER
+ "void forEachField(" {
+ usedSpecializationsSet.toList().forEachLastAware { specType, isLast ->
+ val SpecType = specType.capitalize()
+ val ActionClass = classRef("com.android.internal.util.DataClass.Per${SpecType}FieldAction")
+ +"$ActionClass<$ClassType> action$SpecType${if_(!isLast, ",")}"
+ }
+ }; " {" {
+ usedSpecializations.forEachIndexed { i, specType ->
+ val SpecType = specType.capitalize()
+ fields[i].apply {
+ +"action$SpecType.accept$SpecType(this, \"$nameLowerCamel\", $name);"
+ }
+ }
+ }
+
+ if (usedSpecializationsSet.size > 1) {
+ +"/** @deprecated May cause boxing allocations - use with caution! */"
+ +"@Deprecated"
+ +GENERATED_MEMBER_HEADER
+ "void forEachField($PerObjectFieldAction<$ClassType> action)" {
+ fields.forEachApply {
+ +"action.acceptObject(this, \"$nameLowerCamel\", $name);"
+ }
+ }
+ }
+}
diff --git a/tools/codegen/src/com/android/codegen/InputSignaturesComputation.kt b/tools/codegen/src/com/android/codegen/InputSignaturesComputation.kt
new file mode 100644
index 0000000..d1dc88f
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/InputSignaturesComputation.kt
@@ -0,0 +1,122 @@
+package com.android.codegen
+
+import com.github.javaparser.ast.body.TypeDeclaration
+import com.github.javaparser.ast.expr.*
+import com.github.javaparser.ast.nodeTypes.NodeWithAnnotations
+import com.github.javaparser.ast.type.ClassOrInterfaceType
+import com.github.javaparser.ast.type.Type
+
+
+fun ClassPrinter.getInputSignatures(): List<String> {
+ return classAst.fields.map { fieldAst ->
+ buildString {
+ append(fieldAst.modifiers.joinToString(" ") {it.asString()})
+ append(" ")
+ append(annotationsToString(fieldAst))
+ append(" ")
+ append(getFullClassName(fieldAst.commonType))
+ append(" ")
+ append(fieldAst.variables.joinToString(", ") { it.nameAsString })
+ }
+ } + classAst.methods.map { methodAst ->
+ buildString {
+ append(methodAst.modifiers.joinToString(" ") {it.asString()})
+ append(" ")
+ append(annotationsToString(methodAst))
+ append(" ")
+ append(getFullClassName(methodAst.type))
+ append(" ")
+ append(methodAst.nameAsString)
+ append("(")
+ append(methodAst.parameters.joinToString(",") {getFullClassName(it.type)})
+ append(")")
+ }
+ }
+}
+
+private fun ClassPrinter.annotationsToString(annotatedAst: NodeWithAnnotations<*>): String {
+ return annotatedAst.annotations.joinToString(" ") {
+ annotationToString(it)
+ }
+}
+
+private fun ClassPrinter.annotationToString(ann: AnnotationExpr): String {
+ return buildString {
+ append("@")
+ append(getFullClassName(ann.nameAsString))
+ if (ann is MarkerAnnotationExpr) return@buildString
+
+ append("(")
+
+ when (ann) {
+ is SingleMemberAnnotationExpr -> {
+ appendExpr(this, ann.memberValue)
+ }
+ is NormalAnnotationExpr -> {
+ ann.pairs.forEachLastAware { pair, isLast ->
+ append(pair.nameAsString)
+ append("=")
+ appendExpr(this, pair.value)
+ if (!isLast) append(", ")
+ }
+ }
+ }
+
+ append(")")
+ }.replace("\"", "\\\"")
+}
+
+private fun ClassPrinter.appendExpr(sb: StringBuilder, ex: Expression?) {
+ when (ex) {
+ is ClassExpr -> sb.append(getFullClassName(ex.typeAsString)).append(".class")
+ is IntegerLiteralExpr -> sb.append(ex.asInt()).append("L")
+ is LongLiteralExpr -> sb.append(ex.asLong()).append("L")
+ is DoubleLiteralExpr -> sb.append(ex.asDouble())
+ else -> sb.append(ex)
+ }
+}
+
+private fun ClassPrinter.getFullClassName(type: Type): String {
+ return if (type is ClassOrInterfaceType) {
+ getFullClassName(buildString {
+ type.scope.ifPresent { append(it).append(".") }
+ type.isArrayType
+ append(type.nameAsString)
+ }) + (type.typeArguments.orElse(null)?.let { args -> args.joinToString(", ") {getFullClassName(it)}}?.let { "<$it>" } ?: "")
+ } else getFullClassName(type.asString())
+}
+
+private fun ClassPrinter.getFullClassName(className: String): String {
+ if (className.endsWith("[]")) return getFullClassName(className.removeSuffix("[]")) + "[]"
+
+ if (className.matches("\\.[a-z]".toRegex())) return className //qualified name
+
+ if ("." in className) return getFullClassName(className.substringBeforeLast(".")) + "." + className.substringAfterLast(".")
+
+ fileAst.imports.find { imp ->
+ imp.nameAsString.endsWith(".$className")
+ }?.nameAsString?.let { return it }
+
+ val thisPackagePrefix = fileAst.packageDeclaration.map { it.nameAsString + "." }.orElse("")
+ val thisClassPrefix = thisPackagePrefix + classAst.nameAsString + "."
+
+ classAst.childNodes.filterIsInstance<TypeDeclaration<*>>().find {
+ it.nameAsString == className
+ }?.let { return thisClassPrefix + it.nameAsString }
+
+ constDefs.find { it.AnnotationName == className }?.let { return thisClassPrefix + className }
+
+ if (tryOrNull { Class.forName("java.lang.$className") } != null) {
+ return "java.lang.$className"
+ }
+
+ if (className[0].isLowerCase()) return className //primitive
+
+ return thisPackagePrefix + className
+}
+
+private inline fun <T> tryOrNull(f: () -> T?) = try {
+ f()
+} catch (e: Exception) {
+ null
+}
diff --git a/tools/codegen/src/com/android/codegen/Main.kt b/tools/codegen/src/com/android/codegen/Main.kt
new file mode 100755
index 0000000..8fafa7c
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/Main.kt
@@ -0,0 +1,199 @@
+package com.android.codegen
+
+import java.io.File
+
+
+const val THIS_SCRIPT_LOCATION = ""
+const val GENERATED_WARNING_PREFIX = "Code below generated by $CODEGEN_NAME"
+const val INDENT_SINGLE = " "
+
+val PRIMITIVE_TYPES = listOf("byte", "short", "int", "long", "char", "float", "double", "boolean")
+
+const val CANONICAL_BUILDER_CLASS = "Builder"
+const val GENERATED_BUILDER_CLASS = "GeneratedBuilder"
+
+val BUILTIN_SPECIAL_PARCELLINGS = listOf("Pattern")
+
+const val FLAG_BUILDER_PROTECTED_SETTERS = "--builder-protected-setters"
+const val FLAG_NO_FULL_QUALIFIERS = "--no-full-qualifiers"
+
+
+/** @see [FeatureFlag] */
+val USAGE = """
+Usage: $CODEGEN_NAME [--[PREFIX-]FEATURE...] JAVAFILE
+
+Generates boilerplade parcelable/data class code at the bottom of JAVAFILE, based o fields' declaration in the given JAVAFILE's top-level class
+
+FEATURE represents some generatable code, and can be among:
+${FeatureFlag.values().map { feature ->
+ " ${feature.kebabCase}" to feature.desc
+}.columnize(" - ")}
+
+And PREFIX can be:
+ <empty> - request to generate the feature
+ no - suppress generation of the feature
+ hidden - request to generate the feature with @hide
+
+Extra options:
+ --help - view this help
+ --update-only - auto-detect flags from the previously auto-generated comment within the file
+ $FLAG_NO_FULL_QUALIFIERS
+ - when referring to classes don't use package name prefix; handy with IDE auto-import
+ $FLAG_BUILDER_PROTECTED_SETTERS
+ - make builder's setters protected to expose them as public in a subclass on a whitelist basis
+
+
+Special field modifiers and annotations:
+ transient - ignore the field completely
+ @Nullable - support null value when parcelling, and never throw on null input
+ @NonNull - throw on null input and don't parcel the nullness bit for the field
+ @DataClass.Enum - parcel field as an enum value by ordinal
+ @DataClass.PluralOf(..) - provide a singular version of a collection field name to be used in the builder's 'addFoo(..)'
+ @DataClass.ParcelWith(..) - provide a custom Parcelling class, specifying the custom (un)parcelling logic for this field
+ = <initializer>; - provide default value and never throw if this field was not provided e.g. when using builder
+ /** ... */ - copy given javadoc on field's getters/setters/constructor params/builder setters etc.
+ @hide (in javadoc) - force field's getters/setters/withers/builder setters to be @hide-den if generated
+
+
+Special methods/etc. you can define:
+
+ <any auto-generatable method>
+ For any method to be generated, if a method with same name and argument types is already
+ defined, than that method will not be generated.
+ This allows you to override certain details on granular basis.
+
+ void onConstructed()
+ Will be called in constructor, after all the fields have been initialized.
+ This is a good place to put any custom validation logic that you may have
+
+ static class $CANONICAL_BUILDER_CLASS extends $GENERATED_BUILDER_CLASS
+ If a class extending $GENERATED_BUILDER_CLASS is specified, generated builder's setters will
+ return the provided $CANONICAL_BUILDER_CLASS type.
+ $GENERATED_BUILDER_CLASS's constructor(s) will be package-private to encourage using $CANONICAL_BUILDER_CLASS instead
+ This allows you to extend the generated builder, adding or overriding any methods you may want
+
+
+In addition, for any field mMyField(or myField) of type FieldType you can define the following methods:
+
+ void parcelMyField(Parcel dest, int flags)
+ Allows you to provide custom logic for storing mMyField into a Parcel
+
+ static FieldType unparcelMyField(Parcel in)
+ Allows you to provide custom logic to deserialize the value of mMyField from a Parcel
+
+ String myFieldToString()
+ Allows you to provide a custom toString representation of mMyField's value
+
+ FieldType lazyInitMyField()
+ Requests a lazy initialization in getMyField(), with the provided method being the constructor
+ You may additionally mark the fields as volatile to cause this to generate a thread-safe
+ double-check locking lazy initialization
+
+ FieldType defaultMyField()
+ Allows you to provide a default value to initialize the field to, in case an explicit one
+ was not provided.
+ This is an alternative to providing a field initializer that, unlike the initializer,
+ you can use with final fields.
+
+Version: $CODEGEN_VERSION
+Questions? Feedback? Contact: eugenesusla@
+"""
+
+fun main(args: Array<String>) {
+ if (args.contains("--help")) {
+ println(USAGE)
+ System.exit(0)
+ }
+ if (args.contains("--version")) {
+ println(CODEGEN_VERSION)
+ System.exit(0)
+ }
+ val file = File(args.last())
+ val sourceLinesNoClosingBrace = file.readLines().dropLastWhile {
+ it.startsWith("}") || it.all(Char::isWhitespace)
+ }
+ val cliArgs = handleUpdateFlag(args, sourceLinesNoClosingBrace)
+ val sourceLinesAsIs = discardGeneratedCode(sourceLinesNoClosingBrace)
+ val sourceLines = sourceLinesAsIs
+ .filterNot { it.trim().startsWith("//") }
+ .map { it.trimEnd().dropWhile { it == '\n' } }
+
+ val stringBuilder = StringBuilder(sourceLinesAsIs.joinToString("\n"))
+ ClassPrinter(sourceLines, stringBuilder, cliArgs).run {
+
+ val cliExecutable = "$THIS_SCRIPT_LOCATION$CODEGEN_NAME"
+ val fileEscaped = file.absolutePath.replace(
+ System.getenv("ANDROID_BUILD_TOP"), "\$ANDROID_BUILD_TOP")
+
+
+ +"""
+
+
+
+ // $GENERATED_WARNING_PREFIX v$CODEGEN_VERSION.
+ // on ${currentTimestamp()}
+ //
+ // DO NOT MODIFY!
+ //
+ // To regenerate run:
+ // $ $cliExecutable ${cliArgs.dropLast(1).joinToString("") { "$it " }}$fileEscaped
+ //
+ // CHECKSTYLE:OFF Generated code
+ """
+
+ if (FeatureFlag.CONST_DEFS()) generateConstDefs()
+
+ "@$DataClassGenerated(" {
+ +"time = ${System.currentTimeMillis()}L,"
+ +"codegenVersion = \"$CODEGEN_VERSION\","
+ +"sourceFile = \"${file.relativeTo(File(System.getenv("ANDROID_BUILD_TOP")))}\","
+ +"inputSignatures = \"${getInputSignatures().joinToString("\\n")}\""
+ }
+ +"\n"
+
+
+ if (FeatureFlag.CONSTRUCTOR()) {
+ generateConstructor("public")
+ } else if (FeatureFlag.BUILDER()
+ || FeatureFlag.COPY_CONSTRUCTOR()
+ || FeatureFlag.WITHERS()
+ || FeatureFlag.PARCELABLE()) {
+ generateConstructor("/* package-private */")
+ }
+
+ if (FeatureFlag.GETTERS()) generateGetters()
+ if (FeatureFlag.SETTERS()) generateSetters()
+ if (FeatureFlag.TO_STRING()) generateToString()
+ if (FeatureFlag.EQUALS_HASH_CODE()) generateEqualsHashcode()
+
+ if (FeatureFlag.FOR_EACH_FIELD()) generateForEachField()
+
+ if (FeatureFlag.COPY_CONSTRUCTOR()) generateCopyConstructor()
+ if (FeatureFlag.WITHERS()) generateWithers()
+
+ if (FeatureFlag.PARCELABLE()) generateParcelable()
+
+ if (FeatureFlag.BUILDER() && FeatureFlag.BUILD_UPON()) generateBuildUpon()
+ if (FeatureFlag.BUILDER()) generateBuilder()
+
+ if (FeatureFlag.AIDL()) generateAidl(file)
+
+ rmEmptyLine()
+ }
+ stringBuilder.append("\n}\n")
+ file.writeText(stringBuilder.toString().mapLines { trimEnd() })
+}
+
+internal fun discardGeneratedCode(sourceLinesNoClosingBrace: List<String>): List<String> {
+ return sourceLinesNoClosingBrace
+ .takeWhile { GENERATED_WARNING_PREFIX !in it }
+ .dropLastWhile(String::isBlank)
+}
+
+private fun handleUpdateFlag(cliArgs: Array<String>, sourceLines: List<String>): Array<String> {
+ if ("--update-only" in cliArgs
+ && sourceLines.none { GENERATED_WARNING_PREFIX in it || it.startsWith("@DataClass") }) {
+ System.exit(0)
+ }
+ return cliArgs - "--update-only"
+}
\ No newline at end of file
diff --git a/tools/codegen/src/com/android/codegen/SharedConstants.kt b/tools/codegen/src/com/android/codegen/SharedConstants.kt
new file mode 100644
index 0000000..41641f6
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/SharedConstants.kt
@@ -0,0 +1,4 @@
+package com.android.codegen
+
+const val CODEGEN_NAME = "codegen"
+const val CODEGEN_VERSION = "0.0.1"
\ No newline at end of file
diff --git a/tools/codegen/src/com/android/codegen/Utils.kt b/tools/codegen/src/com/android/codegen/Utils.kt
new file mode 100644
index 0000000..95c9909
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/Utils.kt
@@ -0,0 +1,76 @@
+package com.android.codegen
+
+import com.github.javaparser.ast.expr.AnnotationExpr
+import com.github.javaparser.ast.expr.Expression
+import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr
+import java.time.Instant
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+
+/**
+ * [Iterable.forEach] + [Any.apply]
+ */
+inline fun <T> Iterable<T>.forEachApply(block: T.() -> Unit) = forEach(block)
+
+inline fun String.mapLines(f: String.() -> String?) = lines().mapNotNull(f).joinToString("\n")
+inline fun <T> Iterable<T>.trim(f: T.() -> Boolean) = dropWhile(f).dropLastWhile(f)
+fun String.trimBlankLines() = lines().trim { isBlank() }.joinToString("\n")
+
+fun Char.isNewline() = this == '\n' || this == '\r'
+fun Char.isWhitespaceNonNewline() = isWhitespace() && !isNewline()
+
+fun if_(cond: Boolean, then: String) = if (cond) then else ""
+
+inline infix fun Int.times(action: () -> Unit) {
+ for (i in 1..this) action()
+}
+
+/**
+ * a bbb
+ * cccc dd
+ *
+ * ->
+ *
+ * a bbb
+ * cccc dd
+ */
+fun Iterable<Pair<String, String>>.columnize(separator: String = " | "): String {
+ val col1w = map { (a, _) -> a.length }.max()!!
+ val col2w = map { (_, b) -> b.length }.max()!!
+ return map { it.first.padEnd(col1w) + separator + it.second.padEnd(col2w) }.joinToString("\n")
+}
+
+fun String.hasUnbalancedCurlyBrace(): Boolean {
+ var braces = 0
+ forEach {
+ if (it == '{') braces++
+ if (it == '}') braces--
+ if (braces < 0) return true
+ }
+ return false
+}
+
+fun String.toLowerCamel(): String {
+ if (length >= 2 && this[0] == 'm' && this[1].isUpperCase()) return substring(1).capitalize()
+ if (all { it.isLetterOrDigit() }) return decapitalize()
+ return split("[^a-zA-Z0-9]".toRegex())
+ .map { it.toLowerCase().capitalize() }
+ .joinToString("")
+ .decapitalize()
+}
+
+inline fun <T> List<T>.forEachLastAware(f: (T, Boolean) -> Unit) {
+ forEachIndexed { index, t -> f(t, index == size - 1) }
+}
+
+@Suppress("UNCHECKED_CAST")
+fun <T : Expression> AnnotationExpr.singleArgAs()
+ = ((this as SingleMemberAnnotationExpr).memberValue as T)
+
+inline operator fun <reified T> Array<T>.minus(item: T) = toList().minus(item).toTypedArray()
+
+fun currentTimestamp() = DateTimeFormatter
+ .ofLocalizedDateTime(/* date */ FormatStyle.MEDIUM, /* time */ FormatStyle.LONG)
+ .withZone(ZoneId.systemDefault())
+ .format(Instant.now())
\ No newline at end of file