summaryrefslogtreecommitdiff
path: root/tools/codegen/src
diff options
context:
space:
mode:
Diffstat (limited to 'tools/codegen/src')
-rw-r--r--tools/codegen/src/com/android/codegen/ClassInfo.kt27
-rw-r--r--tools/codegen/src/com/android/codegen/ClassPrinter.kt234
-rw-r--r--tools/codegen/src/com/android/codegen/ConstDef.kt17
-rw-r--r--tools/codegen/src/com/android/codegen/FeatureFlag.kt27
-rw-r--r--tools/codegen/src/com/android/codegen/FieldInfo.kt220
-rw-r--r--tools/codegen/src/com/android/codegen/FileInfo.kt289
-rw-r--r--tools/codegen/src/com/android/codegen/Generators.kt949
-rw-r--r--tools/codegen/src/com/android/codegen/ImportsProvider.kt91
-rw-r--r--tools/codegen/src/com/android/codegen/InputSignaturesComputation.kt151
-rwxr-xr-xtools/codegen/src/com/android/codegen/Main.kt136
-rw-r--r--tools/codegen/src/com/android/codegen/Printer.kt186
-rw-r--r--tools/codegen/src/com/android/codegen/SharedConstants.kt7
-rw-r--r--tools/codegen/src/com/android/codegen/Utils.kt146
13 files changed, 2480 insertions, 0 deletions
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 000000000000..bf95a2eb2193
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/ClassInfo.kt
@@ -0,0 +1,27 @@
+package com.android.codegen
+
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+
+open class ClassInfo(val classAst: ClassOrInterfaceDeclaration, val fileInfo: FileInfo) {
+
+ val fileAst = fileInfo.fileAst
+
+ val nestedClasses = classAst.members.filterIsInstance<ClassOrInterfaceDeclaration>()
+
+ val superInterfaces = classAst.implementedTypes.map { it.asString() }
+ val superClass = classAst.extendedTypes.getOrNull(0)
+
+ 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 }
+} \ 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 000000000000..c7c80bab67bf
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/ClassPrinter.kt
@@ -0,0 +1,234 @@
+package com.android.codegen
+
+import com.github.javaparser.ast.Modifier
+import com.github.javaparser.ast.body.CallableDeclaration
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+import com.github.javaparser.ast.body.TypeDeclaration
+import com.github.javaparser.ast.expr.*
+import com.github.javaparser.ast.type.ClassOrInterfaceType
+
+/**
+ * [ClassInfo] + utilities for printing out new class code with proper indentation and imports
+ */
+class ClassPrinter(
+ classAst: ClassOrInterfaceDeclaration,
+ fileInfo: FileInfo
+) : ClassInfo(classAst, fileInfo), Printer<ClassPrinter>, ImportsProvider {
+
+ val GENERATED_MEMBER_HEADER by lazy { "@$GeneratedMember" }
+
+ init {
+ val fieldsWithMissingNullablity = fields.filter { field ->
+ !field.isPrimitive
+ && field.fieldAst.modifiers.none { it.keyword == Modifier.Keyword.TRANSIENT }
+ && "@$Nullable" !in field.annotations
+ && "@$NonNull" !in field.annotations
+ }
+ if (fieldsWithMissingNullablity.isNotEmpty()) {
+ abort("Non-primitive fields must have @$Nullable or @$NonNull annotation.\n" +
+ "Missing nullability annotations on: "
+ + fieldsWithMissingNullablity.joinToString(", ") { it.name })
+ }
+
+ if (!classAst.isFinal &&
+ classAst.extendedTypes.any { it.nameAsString == Parcelable }) {
+ abort("Parcelable classes must be final")
+ }
+ }
+
+ val cliArgs get() = fileInfo.cliArgs
+
+ fun print() {
+ currentIndent = fileInfo.sourceLines
+ .find { "class $ClassName" in it }!!
+ .takeWhile { it.isWhitespace() }
+ .plus(INDENT_SINGLE)
+
+ +fileInfo.generatedWarning
+
+ if (FeatureFlag.CONST_DEFS()) generateConstDefs()
+
+
+ if (FeatureFlag.CONSTRUCTOR()) {
+ generateConstructor("public")
+ } else if (FeatureFlag.BUILDER()
+ || FeatureFlag.COPY_CONSTRUCTOR()
+ || FeatureFlag.WITHERS()) {
+ generateConstructor("/* package-private */")
+ }
+ if (FeatureFlag.COPY_CONSTRUCTOR()) generateCopyConstructor()
+
+ 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.WITHERS()) generateWithers()
+
+ if (FeatureFlag.PARCELABLE()) generateParcelable()
+
+ if (FeatureFlag.BUILDER() && FeatureFlag.BUILD_UPON()) generateBuildUpon()
+ if (FeatureFlag.BUILDER()) generateBuilder()
+
+ if (FeatureFlag.AIDL()) fileInfo.generateAidl() //TODO guard against nested classes requesting aidl
+
+ generateMetadata(fileInfo.file)
+
+ +"""
+ //@formatter:on
+ $GENERATED_END
+
+ """
+
+ rmEmptyLine()
+ }
+
+ override var currentIndent: String
+ get() = fileInfo.currentIndent
+ set(value) { fileInfo.currentIndent = value }
+ override val stringBuilder get() = fileInfo.stringBuilder
+
+
+ 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, UnsupportedAppUsage,
+ DataClassSuppressConstDefs)
+ val knownNonValidationAnnotations = internalAnnotations + Each + Nullable
+
+ /**
+ * @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"
+ val annotationHiddenKey = "genHidden$upperCamelCase"
+ if (dataClassAnnotationFeatures.containsKey(annotationKey)) {
+ return dataClassAnnotationFeatures[annotationKey]!!
+ }
+ if (dataClassAnnotationFeatures.containsKey(annotationHiddenKey)) {
+ return dataClassAnnotationFeatures[annotationHiddenKey]!!
+ }
+
+ 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)
+ || fields.any { it.hasDefault }
+ || onByDefault
+ FeatureFlag.CONSTRUCTOR -> !FeatureFlag.BUILDER()
+ FeatureFlag.PARCELABLE -> "Parcelable" in superInterfaces
+ FeatureFlag.AIDL -> fileInfo.mainClass.nameAsString == ClassName && FeatureFlag.PARCELABLE()
+ FeatureFlag.IMPLICIT_NONNULL -> fields.any { it.isNullable }
+ && fields.none { "@$NonNull" in it.annotations }
+ else -> onByDefault
+ }
+ }
+
+ val FeatureFlag.hidden: Boolean
+ get(): Boolean {
+ val annotationHiddenKey = "genHidden$upperCamelCase"
+ if (dataClassAnnotationFeatures.containsKey(annotationHiddenKey)) {
+ return dataClassAnnotationFeatures[annotationHiddenKey]!!
+ }
+ return when {
+ cliArgs.contains("--hidden-$kebabCase") -> true
+ this == FeatureFlag.BUILD_UPON -> FeatureFlag.BUILDER.hidden
+ else -> false
+ }
+ }
+
+
+
+ inline operator fun <R> invoke(f: ClassPrinter.() -> R): R = run(f)
+
+ var BuilderClass = CANONICAL_BUILDER_CLASS
+ var BuilderType = BuilderClass + genericArgs
+ val customBaseBuilderAst: ClassOrInterfaceDeclaration? by lazy {
+ nestedClasses.find { it.nameAsString == BASE_BUILDER_CLASS }
+ }
+
+ val suppressedMembers by lazy {
+ getSuppressedMembers(classAst)
+ }
+ val builderSuppressedMembers by lazy {
+ getSuppressedMembers(customBaseBuilderAst) + suppressedMembers.mapNotNull {
+ if (it.startsWith("$CANONICAL_BUILDER_CLASS.")) {
+ it.removePrefix("$CANONICAL_BUILDER_CLASS.")
+ } else {
+ null
+ }
+ }
+ }
+
+ private fun getSuppressedMembers(clazz: ClassOrInterfaceDeclaration?): List<String> {
+ return clazz
+ ?.annotations
+ ?.find { it.nameAsString == DataClassSuppress }
+ ?.as_<SingleMemberAnnotationExpr>()
+ ?.memberValue
+ ?.run {
+ when (this) {
+ is ArrayInitializerExpr -> values.map { it.asLiteralStringValueExpr().value }
+ is StringLiteralExpr -> listOf(value)
+ else -> abort("Can't parse annotation arg: $this")
+ }
+ }
+ ?: emptyList()
+ }
+
+ fun isMethodGenerationSuppressed(name: String, vararg argTypes: String): Boolean {
+ return name in suppressedMembers || hasMethod(name, *argTypes)
+ }
+
+ fun hasMethod(name: String, vararg argTypes: String): Boolean {
+ val members: List<CallableDeclaration<*>> =
+ if (name == ClassName) classAst.constructors else classAst.methods
+ return members.any {
+ it.name.asString() == name &&
+ it.parameters.map { it.type.asString() } == argTypes.toList()
+ }
+ }
+
+ val lazyTransientFields = classAst.fields
+ .filter { it.isTransient && !it.isStatic }
+ .mapIndexed { i, node -> FieldInfo(index = i, fieldAst = node, classInfo = this) }
+ .filter { hasMethod("lazyInit${it.NameUpperCamel}") }
+
+ val extendsParcelableClass by lazy {
+ Parcelable !in superInterfaces && superClass != null
+ }
+
+ 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 = classAst
+ .childNodes
+ .filterIsInstance(TypeDeclaration::class.java)
+ .find { it.nameAsString == CANONICAL_BUILDER_CLASS }
+ if (builderExtension != null) {
+ BuilderClass = BASE_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 000000000000..f559d6f87027
--- /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 000000000000..24150d637a7b
--- /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 000000000000..ebfbbd8163b5
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/FieldInfo.kt
@@ -0,0 +1,220 @@
+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
+
+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 { return "$nameAsString()" }
+ return null
+ }
+ val hasDefault get() = defaultExpr != null
+
+
+ // Generic args
+ val isArray = Type.endsWith("[]")
+ val isList = FieldClass == "List" || FieldClass == "ArrayList"
+ val isMap = FieldClass == "Map" || FieldClass == "ArrayMap"
+ || FieldClass == "HashMap" || FieldClass == "LinkedHashMap"
+ val fieldBit = bitAtExpr(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 {
+ fileInfo.apply {
+ fieldAst.addAnnotation(SingleMemberAnnotationExpr(
+ Name(ParcelWith),
+ ClassExpr(parseJava(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 SetterParamType = if (isArray) "$FieldInnerType..." else Type
+ val annotatedTypeForSetterParam by lazy {
+ (annotationsNoInternal + SetterParamType).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" + BOXED_PRIMITIVE_TYPES +
+ listOf("String", "CharSequence", "Exception", "Size", "SizeF", "Bundle",
+ "FileDescriptor", "SparseBooleanArray", "SparseIntArray", "SparseArray") ->
+ FieldClass
+ isMap && 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/FileInfo.kt b/tools/codegen/src/com/android/codegen/FileInfo.kt
new file mode 100644
index 000000000000..909472640f29
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/FileInfo.kt
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.codegen
+
+import com.github.javaparser.JavaParser
+import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+import com.github.javaparser.ast.body.TypeDeclaration
+import java.io.File
+
+/**
+ * File-level parsing & printing logic
+ *
+ * @see [main] entrypoint
+ */
+class FileInfo(
+ val sourceLines: List<String>,
+ val cliArgs: Array<String>,
+ val file: File)
+ : Printer<FileInfo>, ImportsProvider {
+
+ override val fileAst: CompilationUnit
+ = parseJava(JavaParser::parse, sourceLines.joinToString("\n"))
+
+ override val stringBuilder = StringBuilder()
+ override var currentIndent = INDENT_SINGLE
+
+
+ val generatedWarning = run {
+ val fileEscaped = file.absolutePath.replace(
+ System.getenv("ANDROID_BUILD_TOP"), "\$ANDROID_BUILD_TOP")
+
+ """
+
+
+ // $GENERATED_WARNING_PREFIX v$CODEGEN_VERSION.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ $THIS_SCRIPT_LOCATION$CODEGEN_NAME ${cliArgs.dropLast(1).joinToString("") { "$it " }}$fileEscaped
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+ """
+ }
+ private val generatedWarningNumPrecedingEmptyLines
+ = generatedWarning.lines().takeWhile { it.isBlank() }.size
+
+ val classes = fileAst.types
+ .filterIsInstance<ClassOrInterfaceDeclaration>()
+ .flatMap { it.plusNested() }
+ .filterNot { it.isInterface }
+
+ val mainClass = classes.find { it.nameAsString == file.nameWithoutExtension }!!
+
+ // Parse stage 1
+ val classBounds: List<ClassBounds> = classes.map { ast ->
+ ClassBounds(ast, fileInfo = this)
+ }.apply {
+ forEachApply {
+ if (ast.isNestedType) {
+ val parent = find {
+ it.name == (ast.parentNode.get()!! as TypeDeclaration<*>).nameAsString
+ }!!
+ parent.nested.add(this)
+ nestedIn = parent
+ }
+ }
+ }
+
+ // Parse Stage 2
+ var codeChunks = buildList<CodeChunk> {
+ val mainClassBounds = classBounds.find { it.nestedIn == null }!!
+ add(CodeChunk.FileHeader(
+ mainClassBounds.fileInfo.sourceLines.subList(0, mainClassBounds.range.start)))
+ add(CodeChunk.DataClass.parse(mainClassBounds))
+ }
+
+ // Output stage
+ fun main() {
+ codeChunks.forEach { print(it) }
+ }
+
+ fun print(chunk: CodeChunk) {
+ when(chunk) {
+ is CodeChunk.GeneratedCode -> {
+ // Re-parse class code, discarding generated code and nested dataclasses
+ val ast = chunk.owner.chunks
+ .filter {
+ it.javaClass == CodeChunk.Code::class.java
+ || it.javaClass == CodeChunk.ClosingBrace::class.java
+ }
+ .flatMap { (it as CodeChunk.Code).lines }
+ .joinToString("\n")
+ .let {
+ parseJava(JavaParser::parseTypeDeclaration, it)
+ as ClassOrInterfaceDeclaration
+ }
+
+ // Write new generated code
+ ClassPrinter(ast, fileInfo = this).print()
+ }
+ is CodeChunk.ClosingBrace -> {
+ // Special case - print closing brace with -1 indent
+ rmEmptyLine()
+ popIndent()
+ +"\n}"
+ }
+ // Print general code as-is
+ is CodeChunk.Code -> chunk.lines.forEach { stringBuilder.appendln(it) }
+ // Recursively render data classes
+ is CodeChunk.DataClass -> chunk.chunks.forEach { print(it) }
+ }
+ }
+
+ /**
+ * Output of stage 1 of parsing a file:
+ * Recursively nested ranges of code line numbers containing nested classes
+ */
+ data class ClassBounds(
+ val ast: ClassOrInterfaceDeclaration,
+ val fileInfo: FileInfo,
+ val name: String = ast.nameAsString,
+ val range: ClosedRange<Int> = ast.range.get()!!.let { rng -> rng.begin.line-1..rng.end.line-1 },
+ val nested: MutableList<ClassBounds> = mutableListOf(),
+ var nestedIn: ClassBounds? = null) {
+
+ val nestedDataClasses: List<ClassBounds> by lazy {
+ nested.filter { it.isDataclass }.sortedBy { it.range.start }
+ }
+ val isDataclass = ast.annotations.any { it.nameAsString.endsWith("DataClass") }
+
+ val baseIndentLength = fileInfo.sourceLines.find { "class $name" in it }!!.takeWhile { it == ' ' }.length
+ val baseIndent = buildString { repeat(baseIndentLength) { append(' ') } }
+
+ val sourceNoPrefix = fileInfo.sourceLines.drop(range.start)
+ val generatedCodeRange = sourceNoPrefix
+ .indexOfFirst { it.startsWith("$baseIndent$INDENT_SINGLE// $GENERATED_WARNING_PREFIX") }
+ .let { start ->
+ if (start < 0) {
+ null
+ } else {
+ var endInclusive = sourceNoPrefix.indexOfFirst {
+ it.startsWith("$baseIndent$INDENT_SINGLE$GENERATED_END")
+ }
+ if (endInclusive == -1) {
+ // Legacy generated code doesn't have end markers
+ endInclusive = sourceNoPrefix.size - 2
+ }
+ IntRange(
+ range.start + start - fileInfo.generatedWarningNumPrecedingEmptyLines,
+ range.start + endInclusive)
+ }
+ }
+
+ /** Debug info */
+ override fun toString(): String {
+ return buildString {
+ appendln("class $name $range")
+ nested.forEach {
+ appendln(it)
+ }
+ appendln("end $name")
+ }
+ }
+ }
+
+ /**
+ * Output of stage 2 of parsing a file
+ */
+ sealed class CodeChunk {
+ /** General code */
+ open class Code(val lines: List<String>): CodeChunk() {}
+
+ /** Copyright + package + imports + main javadoc */
+ class FileHeader(lines: List<String>): Code(lines)
+
+ /** Code to be discarded and refreshed */
+ open class GeneratedCode(lines: List<String>): Code(lines) {
+ lateinit var owner: DataClass
+
+ class Placeholder: GeneratedCode(emptyList())
+ }
+
+ object ClosingBrace: Code(listOf("}"))
+
+ data class DataClass(
+ val ast: ClassOrInterfaceDeclaration,
+ val chunks: List<CodeChunk>,
+ val generatedCode: GeneratedCode?): CodeChunk() {
+
+ companion object {
+ fun parse(classBounds: ClassBounds): DataClass {
+ val initial = Code(lines = classBounds.fileInfo.sourceLines.subList(
+ fromIndex = classBounds.range.start,
+ toIndex = findLowerBound(
+ thisClass = classBounds,
+ nextNestedClass = classBounds.nestedDataClasses.getOrNull(0))))
+
+ val chunks = mutableListOf<CodeChunk>(initial)
+
+ classBounds.nestedDataClasses.forEachSequentialPair {
+ nestedDataClass, nextNestedDataClass ->
+ chunks += DataClass.parse(nestedDataClass)
+ chunks += Code(lines = classBounds.fileInfo.sourceLines.subList(
+ fromIndex = nestedDataClass.range.endInclusive + 1,
+ toIndex = findLowerBound(
+ thisClass = classBounds,
+ nextNestedClass = nextNestedDataClass)))
+ }
+
+ var generatedCode = classBounds.generatedCodeRange?.let { rng ->
+ GeneratedCode(classBounds.fileInfo.sourceLines.subList(
+ rng.start, rng.endInclusive+1))
+ }
+ if (generatedCode != null) {
+ chunks += generatedCode
+ chunks += ClosingBrace
+ } else if (classBounds.isDataclass) {
+
+ // Insert placeholder for generated code to be inserted for the 1st time
+ chunks.last = (chunks.last as Code)
+ .lines
+ .dropLastWhile { it.isBlank() }
+ .run {
+ if (last().dropWhile { it.isWhitespace() }.startsWith("}")) {
+ dropLast(1)
+ } else {
+ this
+ }
+ }.let { Code(it) }
+ generatedCode = GeneratedCode.Placeholder()
+ chunks += generatedCode
+ chunks += ClosingBrace
+ } else {
+ // Outer class may be not a @DataClass but contain ones
+ // so just skip generated code for them
+ }
+
+ return DataClass(classBounds.ast, chunks, generatedCode).also {
+ generatedCode?.owner = it
+ }
+ }
+
+ private fun findLowerBound(thisClass: ClassBounds, nextNestedClass: ClassBounds?): Int {
+ return nextNestedClass?.range?.start
+ ?: thisClass.generatedCodeRange?.start
+ ?: thisClass.range.endInclusive + 1
+ }
+ }
+ }
+
+ /** Debug info */
+ fun summary(): String = when(this) {
+ is Code -> "${javaClass.simpleName}(${lines.size} lines): ${lines.getOrNull(0)?.take(70) ?: ""}..."
+ is DataClass -> "DataClass ${ast.nameAsString}:\n" +
+ chunks.joinToString("\n") { it.summary() } +
+ "\n//end ${ast.nameAsString}"
+ }
+ }
+
+ private fun ClassOrInterfaceDeclaration.plusNested(): List<ClassOrInterfaceDeclaration> {
+ return mutableListOf<ClassOrInterfaceDeclaration>().apply {
+ add(this@plusNested)
+ childNodes.filterIsInstance<ClassOrInterfaceDeclaration>()
+ .flatMap { it.plusNested() }
+ .let { addAll(it) }
+ }
+ }
+} \ 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 000000000000..8fe243ff68cb
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/Generators.kt
@@ -0,0 +1,949 @@
+package com.android.codegen
+
+import com.github.javaparser.ast.body.FieldDeclaration
+import com.github.javaparser.ast.body.MethodDeclaration
+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")
+ } && it.annotations.none { it.nameAsString == DataClassSuppressConstDefs }
+ }.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-private */"
+
+ val Retention = classRef("java.lang.annotation.Retention")
+ val RetentionPolicySource = memberRef("java.lang.annotation.RetentionPolicy.SOURCE")
+ val ConstDef = classRef("android.annotation.${type.capitalize()}Def")
+
+ if (FeatureFlag.CONST_DEFS.hidden) {
+ +"/** @hide */"
+ }
+ "@$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") {
+ if (FeatureFlag.CONST_DEFS.hidden) {
+ +"/** @hide */"
+ }
+ +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 FileInfo.generateAidl() {
+ val aidl = File(file.path.substringBeforeLast(".java") + ".aidl")
+ if (aidl.exists()) return
+ aidl.writeText(buildString {
+ sourceLines.dropLastWhile { !it.startsWith("package ") }.forEach {
+ appendln(it)
+ }
+ append("\nparcelable ${mainClass.nameAsString};\n")
+ })
+}
+
+/**
+ * ```
+ * Foo newFoo = oldFoo.withBar(newBar);
+ * ```
+ */
+fun ClassPrinter.generateWithers() {
+ fields.forEachApply {
+ val metodName = "with$NameUpperCamel"
+ if (!isMethodGenerationSuppressed(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 (isMethodGenerationSuppressed("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 providedSubclassAst = nestedClasses.find {
+ it.extendedTypes.any { it.nameAsString == BASE_BUILDER_CLASS }
+ }
+
+ val BuilderSupertype = if (customBaseBuilderAst != null) {
+ customBaseBuilderAst!!.nameAsString
+ } else {
+ "Object"
+ }
+
+ val maybeFinal = if_(classAst.isFinal, "final ")
+
+ +"/**"
+ +" * A builder for {@link $ClassName}"
+ if (FeatureFlag.BUILDER.hidden) +" * @hide"
+ +" */"
+ +"@SuppressWarnings(\"WeakerAccess\")"
+ +GENERATED_MEMBER_HEADER
+ !"public static ${maybeFinal}class $BuilderClass$genericArgs"
+ if (BuilderSupertype != "Object") {
+ appendSameLine(" extends $BuilderSupertype")
+ }
+ " {" {
+
+ +""
+ fields.forEachApply {
+ +"private $annotationsAndType $name;"
+ }
+ +""
+ +"private long mBuilderFieldsSet = 0L;"
+ +""
+
+ val requiredFields = fields.filter { !it.hasDefault }
+
+ generateConstructorJavadoc(
+ fields = requiredFields,
+ ClassName = BuilderClass,
+ hidden = false)
+ "$constructorVisibility $BuilderClass(" {
+ requiredFields.forEachLastAware { field, isLast ->
+ +"${field.annotationsAndType} ${field._name}${if_(!isLast, ",")}"
+ }
+ }; " {" {
+ requiredFields.forEachApply {
+ generateSetFrom(_name)
+ }
+ }
+
+ generateBuilderSetters(setterVisibility)
+
+ generateBuilderBuild()
+
+ "private void checkNotUsed() {" {
+ "if ((mBuilderFieldsSet & ${bitAtExpr(fields.size)}) != 0)" {
+ "throw new IllegalStateException(" {
+ +"\"This Builder should not be reused. Use a new Builder instance instead\""
+ }
+ +";"
+ }
+ }
+
+ rmEmptyLine()
+ }
+}
+
+private fun ClassPrinter.generateBuilderMethod(
+ defVisibility: String,
+ name: String,
+ paramAnnotations: String? = null,
+ paramTypes: List<String>,
+ paramNames: List<String> = listOf("value"),
+ genJavadoc: ClassPrinter.() -> Unit,
+ genBody: ClassPrinter.() -> Unit) {
+
+ val providedMethod = customBaseBuilderAst?.members?.find {
+ it is MethodDeclaration
+ && it.nameAsString == name
+ && it.parameters.map { it.typeAsString } == paramTypes.toTypedArray().toList()
+ } as? MethodDeclaration
+
+ if ((providedMethod == null || providedMethod.isAbstract)
+ && name !in builderSuppressedMembers) {
+ val visibility = providedMethod?.visibility?.asString() ?: defVisibility
+ val ReturnType = providedMethod?.typeAsString ?: CANONICAL_BUILDER_CLASS
+ val Annotations = providedMethod?.annotations?.joinToString("\n")
+
+ genJavadoc()
+ +GENERATED_MEMBER_HEADER
+ if (providedMethod?.isAbstract == true) +"@Override"
+ if (!Annotations.isNullOrEmpty()) +Annotations
+ val ParamAnnotations = if (!paramAnnotations.isNullOrEmpty()) "$paramAnnotations " else ""
+
+ "$visibility @$NonNull $ReturnType $name(${
+ paramTypes.zip(paramNames).joinToString(", ") { (Type, paramName) ->
+ "$ParamAnnotations$Type $paramName"
+ }
+ })" {
+ genBody()
+ }
+ }
+}
+
+private fun ClassPrinter.generateBuilderSetters(visibility: String) {
+
+ fields.forEachApply {
+ val maybeCast =
+ if_(BuilderClass != CANONICAL_BUILDER_CLASS, " ($CANONICAL_BUILDER_CLASS)")
+
+ val setterName = "set$NameUpperCamel"
+
+ generateBuilderMethod(
+ name = setterName,
+ defVisibility = visibility,
+ paramAnnotations = annotationsNoInternal.joinToString(" "),
+ paramTypes = listOf(SetterParamType),
+ genJavadoc = { generateFieldJavadoc() }) {
+ +"checkNotUsed();"
+ +"mBuilderFieldsSet |= $fieldBit;"
+ +"$name = value;"
+ +"return$maybeCast this;"
+ }
+
+ val javadocSeeSetter = "/** @see #$setterName */"
+ val adderName = "add$SingularName"
+
+ 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) {
+ generateBuilderMethod(
+ name = adderName,
+ defVisibility = visibility,
+ paramAnnotations = "@$NonNull",
+ paramTypes = listOf(FieldInnerType),
+ genJavadoc = { +javadocSeeSetter }) {
+
+ !singularNameCustomizationHint
+ +"if ($name == null) $setterName(new $ArrayList<>());"
+ +"$name.add(value);"
+ +"return$maybeCast this;"
+ }
+ }
+
+ if (isMap && FieldInnerType != null) {
+ generateBuilderMethod(
+ name = adderName,
+ defVisibility = visibility,
+ paramAnnotations = "@$NonNull",
+ paramTypes = fieldTypeGenegicArgs,
+ paramNames = listOf("key", "value"),
+ genJavadoc = { +javadocSeeSetter }) {
+ !singularNameCustomizationHint
+ +"if ($name == null) $setterName(new ${if (FieldClass == "Map") LinkedHashMap else FieldClass}());"
+ +"$name.put(key, value);"
+ +"return$maybeCast this;"
+ }
+ }
+ }
+}
+
+private fun ClassPrinter.generateBuilderBuild() {
+ +"/** Builds the instance. This builder should not be touched after calling this! */"
+ "public @$NonNull $ClassType build()" {
+ +"checkNotUsed();"
+ +"mBuilderFieldsSet |= ${bitAtExpr(fields.size)}; // Mark builder used"
+ +""
+ fields.forEachApply {
+ if (hasDefault) {
+ "if ((mBuilderFieldsSet & $fieldBit) == 0)" {
+ +"$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 (!isMethodGenerationSuppressed("writeToParcel", Parcel, "int")) {
+ +"@Override"
+ +GENERATED_MEMBER_HEADER
+ "public void writeToParcel(@$NonNull $Parcel dest, int flags)" {
+ +"// You can override field parcelling by defining methods like:"
+ +"// void parcelFieldName(Parcel dest, int flags) { ... }"
+ +""
+
+ if (extendsParcelableClass) {
+ +"super.writeToParcel(dest, flags);\n"
+ }
+
+ 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 (!isMethodGenerationSuppressed("describeContents")) {
+ +"@Override"
+ +GENERATED_MEMBER_HEADER
+ +"public int describeContents() { return 0; }"
+ +""
+ }
+
+ if (!hasMethod(ClassName, Parcel)) {
+ val visibility = if (classAst.isFinal) "/* package-private */" else "protected"
+
+ +"/** @hide */"
+ +"@SuppressWarnings({\"unchecked\", \"RedundantCast\"})"
+ +GENERATED_MEMBER_HEADER
+ "$visibility $ClassName(@$NonNull $Parcel in) {" {
+ +"// You can override field unparcelling by defining methods like:"
+ +"// static FieldType unparcelFieldName(Parcel in) { ... }"
+ +""
+
+ if (extendsParcelableClass) {
+ +"super(in);\n"
+ }
+
+ 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 == "Map" -> "new $LinkedHashMap<>()"
+ isMap -> "new $FieldClass()"
+ FieldClass == "List" || FieldClass == "ArrayList" ->
+ "new ${classRef("java.util.ArrayList")}<>()"
+ 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") {
+ !"($FieldClass) "
+ }
+ }
+
+ // Determine method args
+ when {
+ ParcelMethodsSuffix == "Parcelable" ->
+ methodArgs += "$FieldClass.class.getClassLoader()"
+ ParcelMethodsSuffix == "SparseArray" ->
+ methodArgs += "$FieldInnerClass.class.getClassLoader()"
+ ParcelMethodsSuffix == "TypedObject" ->
+ methodArgs += "$FieldClass.CREATOR"
+ ParcelMethodsSuffix == "TypedArray" ->
+ methodArgs += "$FieldInnerClass.CREATOR"
+ ParcelMethodsSuffix == "Map" ->
+ methodArgs += "${fieldTypeGenegicArgs[1].substringBefore("<")}.class.getClassLoader()"
+ ParcelMethodsSuffix.startsWith("Parcelable")
+ || (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}"
+ }
+ }
+ }
+
+ +""
+ fields.forEachApply {
+ !"this."
+ generateSetFrom(_name)
+ }
+
+ generateOnConstructedCallback()
+ }
+ }
+
+ 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"
+ "public $ClassName createFromParcel(@$NonNull $Parcel in)" {
+ +"return new $ClassName(in);"
+ }
+ rmEmptyLine()
+ } + ";"
+ +""
+ }
+}
+
+fun ClassPrinter.generateEqualsHashcode() {
+ if (!isMethodGenerationSuppressed("equals", "Object")) {
+ +"@Override"
+ +GENERATED_MEMBER_HEADER
+ "public boolean equals(@$Nullable 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 (!isMethodGenerationSuppressed("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 (!isMethodGenerationSuppressed("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 (!isMethodGenerationSuppressed("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 (!isMethodGenerationSuppressed(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 = $source;"
+ generateFieldValidation(field = this@generateSetFrom)
+}
+
+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)
+ }
+
+ generateOnConstructedCallback()
+ }
+}
+
+private fun ClassPrinter.generateConstructorJavadoc(
+ fields: List<FieldInfo> = this.fields,
+ ClassName: String = this.ClassName,
+ hidden: Boolean = FeatureFlag.CONSTRUCTOR.hidden) {
+ if (fields.all { it.javadoc == null } && !FeatureFlag.CONSTRUCTOR.hidden) return
+ +"/**"
+ +" * Creates a new $ClassName."
+ +" *"
+ fields.filter { it.javadoc != null }.forEachApply {
+ javadocTextNoAnnotationLines?.apply {
+ +" * @param $nameLowerCamel"
+ forEach {
+ +" * $it"
+ }
+ }
+ }
+ if (FeatureFlag.CONSTRUCTOR.hidden) +" * @hide"
+ +" */"
+}
+
+private fun ClassPrinter.appendLinesWithContinuationIndent(text: String) {
+ val lines = text.lines()
+ if (lines.isNotEmpty()) {
+ !lines[0]
+ }
+ if (lines.size >= 2) {
+ "" {
+ lines.drop(1).forEach {
+ +it
+ }
+ }
+ }
+}
+
+private fun ClassPrinter.generateFieldValidation(field: FieldInfo) = field.run {
+ if (isNonEmpty) {
+ "if ($isEmptyExpr)" {
+ +"throw new IllegalArgumentException(\"$nameLowerCamel cannot be empty\");"
+ }
+ }
+ if (intOrStringDef != null) {
+ if (intOrStringDef!!.type == ConstDef.Type.INT_FLAGS) {
+ +""
+ "$Preconditions.checkFlagsArgument(" {
+ +"$name, "
+ appendLinesWithContinuationIndent(intOrStringDef!!.CONST_NAMES.joinToString("\n| "))
+ }
+ +";"
+ } else {
+ +""
+ !"if ("
+ appendLinesWithContinuationIndent(intOrStringDef!!.CONST_NAMES.joinToString("\n&& ") {
+ "!(${isEqualToExpr(it)})"
+ })
+ 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
+ }
+
+ val Size = classRef("android.annotation.Size")
+ fieldAst.annotations.filterNot {
+ it.nameAsString == intOrStringDef?.AnnotationName
+ || it.nameAsString in knownNonValidationAnnotations
+ || it in perElementValidations
+ || it.args.any { (_, value) -> value is ArrayInitializerExpr }
+ }.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"))
+ }
+ }
+ }
+}
+
+fun ClassPrinter.appendValidateCall(annotation: AnnotationExpr, valueToValidate: String) {
+ val validate = memberRef("com.android.internal.util.AnnotationValidations.validate")
+ "$validate(" {
+ !"${annotation.nameAsString}.class, null, $valueToValidate"
+ annotation.args.forEach { name, value ->
+ !",\n\"$name\", $value"
+ }
+ }
+ +";"
+}
+
+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")
+ +"@$NonNull $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(@$NonNull $PerObjectFieldAction<$ClassType> action)" {
+ fields.forEachApply {
+ +"action.acceptObject(this, \"$nameLowerCamel\", $name);"
+ }
+ }
+ }
+}
+
+fun ClassPrinter.generateMetadata(file: File) {
+ "@$DataClassGenerated(" {
+ +"time = ${System.currentTimeMillis()}L,"
+ +"codegenVersion = \"$CODEGEN_VERSION\","
+ +"sourceFile = \"${file.relativeTo(File(System.getenv("ANDROID_BUILD_TOP")))}\","
+ +"inputSignatures = \"${getInputSignatures().joinToString("\\n")}\""
+ }
+ +""
+ +"@Deprecated"
+ +"private void __metadata() {}\n"
+} \ No newline at end of file
diff --git a/tools/codegen/src/com/android/codegen/ImportsProvider.kt b/tools/codegen/src/com/android/codegen/ImportsProvider.kt
new file mode 100644
index 000000000000..ba0a0318c843
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/ImportsProvider.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.codegen
+
+import com.github.javaparser.ast.CompilationUnit
+
+/**
+ * Mixin for optionally shortening references based on existing imports
+ */
+interface ImportsProvider {
+
+ abstract val fileAst: CompilationUnit
+
+ val NonNull: String get() { return classRef("android.annotation.NonNull") }
+ val NonEmpty: String get() { return classRef("android.annotation.NonEmpty") }
+ val Nullable: String get() { return classRef("android.annotation.Nullable") }
+ val TextUtils: String get() { return classRef("android.text.TextUtils") }
+ val LinkedHashMap: String get() { return classRef("java.util.LinkedHashMap") }
+ val Collections: String get() { return classRef("java.util.Collections") }
+ val Preconditions: String get() { return classRef("com.android.internal.util.Preconditions") }
+ val ArrayList: String get() { return classRef("java.util.ArrayList") }
+ val DataClass: String get() { return classRef("com.android.internal.util.DataClass") }
+ val DataClassEnum: String get() { return classRef("com.android.internal.util.DataClass.Enum") }
+ val ParcelWith: String get() { return classRef("com.android.internal.util.DataClass.ParcelWith") }
+ val PluralOf: String get() { return classRef("com.android.internal.util.DataClass.PluralOf") }
+ val Each: String get() { return classRef("com.android.internal.util.DataClass.Each") }
+ val DataClassGenerated: String get() { return classRef("com.android.internal.util.DataClass.Generated") }
+ val DataClassSuppressConstDefs: String get() { return classRef("com.android.internal.util.DataClass.SuppressConstDefsGeneration") }
+ val DataClassSuppress: String get() { return classRef("com.android.internal.util.DataClass.Suppress") }
+ val GeneratedMember: String get() { return classRef("com.android.internal.util.DataClass.Generated.Member") }
+ val Parcelling: String get() { return classRef("com.android.internal.util.Parcelling") }
+ val Parcelable: String get() { return classRef("android.os.Parcelable") }
+ val Parcel: String get() { return classRef("android.os.Parcel") }
+ val UnsupportedAppUsage: String get() { return classRef("android.annotation.UnsupportedAppUsage") }
+
+ /**
+ * Optionally shortens a class reference if there's a corresponding import present
+ */
+ fun classRef(fullName: String): String {
+
+ 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() == true) {
+ return classRef(pkg) + "." + simpleName
+ }
+ }
+ return fullName
+ }
+
+ /** @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
+ }
+ }
+}
+
+/** @see classRef */
+inline fun <reified T : Any> ImportsProvider.classRef(): String {
+ return classRef(T::class.java.name)
+} \ No newline at end of file
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 000000000000..d6953c00fc0b
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/InputSignaturesComputation.kt
@@ -0,0 +1,151 @@
+package com.android.codegen
+
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+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 generateInputSignaturesForClass(classAst) +
+ annotationToString(classAst.annotations.find { it.nameAsString == DataClass }) +
+ generateInputSignaturesForClass(customBaseBuilderAst)
+}
+
+private fun ClassPrinter.generateInputSignaturesForClass(classAst: ClassOrInterfaceDeclaration?): List<String> {
+ if (classAst == null) return emptyList()
+
+ return classAst.fields.map { fieldAst ->
+ buildString {
+ append(fieldAst.modifiers.joinToString(" ") { it.keyword.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.keyword.asString() })
+ append(" ")
+ append(annotationsToString(methodAst))
+ append(" ")
+ append(getFullClassName(methodAst.type))
+ append(" ")
+ append(methodAst.nameAsString)
+ append("(")
+ append(methodAst.parameters.joinToString(",") { getFullClassName(it.type) })
+ append(")")
+ }
+ } + ("class ${classAst.nameAsString}" +
+ " extends ${classAst.extendedTypes.map { getFullClassName(it) }.ifEmpty { listOf("java.lang.Object") }.joinToString(", ")}" +
+ " implements [${classAst.implementedTypes.joinToString(", ") { getFullClassName(it) }}]")
+}
+
+private fun ClassPrinter.annotationsToString(annotatedAst: NodeWithAnnotations<*>): String {
+ return annotatedAst
+ .annotations
+ .groupBy { it.nameAsString } // dedupe annotations by name (javaparser bug?)
+ .values
+ .joinToString(" ") {
+ annotationToString(it[0])
+ }
+}
+
+private fun ClassPrinter.annotationToString(ann: AnnotationExpr?): String {
+ if (ann == null) return ""
+ 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())
+ is ArrayInitializerExpr -> {
+ sb.append("{")
+ ex.values.forEachLastAware { arrayElem, isLast ->
+ appendExpr(sb, arrayElem)
+ if (!isLast) sb.append(", ")
+ }
+ sb.append("}")
+ }
+ else -> sb.append(ex)
+ }
+}
+
+private fun ClassPrinter.getFullClassName(type: Type): String {
+ return if (type is ClassOrInterfaceType) {
+
+ getFullClassName(buildString {
+ type.scope.ifPresent { append(it).append(".") }
+ 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 + "."
+
+ if (classAst.nameAsString == className) return thisPackagePrefix + classAst.nameAsString
+
+ nestedClasses.find {
+ it.nameAsString == className
+ }?.let { return thisClassPrefix + it.nameAsString }
+
+ if (className == CANONICAL_BUILDER_CLASS || className == BASE_BUILDER_CLASS) {
+ return thisClassPrefix + className
+ }
+
+ 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 000000000000..4b508d022991
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/Main.kt
@@ -0,0 +1,136 @@
+package com.android.codegen
+
+import com.github.javaparser.JavaParser
+import java.io.File
+
+
+const val THIS_SCRIPT_LOCATION = ""
+const val GENERATED_WARNING_PREFIX = "Code below generated by $CODEGEN_NAME"
+const val GENERATED_END = "// End of generated code"
+const val INDENT_SINGLE = " "
+
+val PRIMITIVE_TYPES = listOf("byte", "short", "int", "long", "char", "float", "double", "boolean")
+val BOXED_PRIMITIVE_TYPES = PRIMITIVE_TYPES.map { it.capitalize() } - "Int" + "Integer" - "Char" + "Character"
+
+val BUILTIN_SPECIAL_PARCELLINGS = listOf("Pattern")
+
+const val FLAG_BUILDER_PROTECTED_SETTERS = "--builder-protected-setters"
+const val FLAG_NO_FULL_QUALIFIERS = "--no-full-qualifiers"
+
+val JAVA_PARSER = JavaParser()
+
+/** @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 $BASE_BUILDER_CLASS
+ If a class extending $BASE_BUILDER_CLASS is specified, generated builder's setters will
+ return the provided $CANONICAL_BUILDER_CLASS type.
+ $BASE_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@
+Bug/feature request: http://go/codegen-bug
+
+Slides: http://go/android-codegen
+In-depth example: http://go/SampleDataClass
+"""
+
+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()).absoluteFile
+ val sourceLisnesOriginal = file.readLines()
+ val sourceLinesNoClosingBrace = sourceLisnesOriginal.dropLastWhile {
+ it.startsWith("}") || it.all(Char::isWhitespace)
+ }
+ val cliArgs = handleUpdateFlag(args, sourceLinesNoClosingBrace)
+
+ val fileInfo = FileInfo(sourceLisnesOriginal, cliArgs, file)
+ fileInfo.main()
+ file.writeText(fileInfo.stringBuilder.toString().mapLines { trimEnd() })
+}
+
+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/Printer.kt b/tools/codegen/src/com/android/codegen/Printer.kt
new file mode 100644
index 000000000000..b30e3f68b307
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/Printer.kt
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.codegen
+
+/**
+ * Mixin for syntactic sugar around indent-aware printing into [stringBuilder]
+ */
+interface Printer<SELF: Printer<SELF>> {
+
+ val stringBuilder: StringBuilder
+
+ var currentIndent: String
+
+ fun pushIndent() {
+ currentIndent += INDENT_SINGLE
+ }
+
+ fun popIndent() {
+ currentIndent = if (currentIndent.length >= INDENT_SINGLE.length) {
+ currentIndent.substring(0, currentIndent.length - INDENT_SINGLE.length)
+ } else {
+ ""
+ }
+ }
+
+ 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
+ */
+ operator fun String.invoke(block: SELF.() -> 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)
+ }
+ }
+
+ fun indentedBy(level: Int, block: SELF.() -> Unit) {
+ append("\n")
+ level times {
+ append(INDENT_SINGLE)
+ pushIndent()
+ }
+ (this as SELF).block()
+ level times { popIndent() }
+ rmEmptyLine()
+ +""
+ }
+
+ fun Iterable<FieldInfo>.forEachTrimmingTrailingComma(b: FieldInfo.() -> Unit) {
+ forEachApply {
+ b()
+ if (isLast) {
+ while (lastChar == ' ' || lastChar == '\n') backspace()
+ if (lastChar == ',') backspace()
+ }
+ }
+ }
+} \ 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 000000000000..74c86f4551f8
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/SharedConstants.kt
@@ -0,0 +1,7 @@
+package com.android.codegen
+
+const val CODEGEN_NAME = "codegen"
+const val CODEGEN_VERSION = "1.0.14"
+
+const val CANONICAL_BUILDER_CLASS = "Builder"
+const val BASE_BUILDER_CLASS = "BaseBuilder"
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 000000000000..c19ae3b0b11f
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/Utils.kt
@@ -0,0 +1,146 @@
+package com.android.codegen
+
+import com.github.javaparser.JavaParser
+import com.github.javaparser.ParseProblemException
+import com.github.javaparser.ParseResult
+import com.github.javaparser.ast.Node
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+import com.github.javaparser.ast.body.TypeDeclaration
+import com.github.javaparser.ast.expr.*
+import com.github.javaparser.ast.nodeTypes.NodeWithModifiers
+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 ""
+
+fun <T> Any?.as_(): T = this as T
+
+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())
+
+val NodeWithModifiers<*>.visibility get() = accessSpecifier
+
+fun abort(msg: String): Nothing {
+ System.err.println("ERROR: $msg")
+ System.exit(1)
+ throw InternalError() // can't get here
+}
+
+fun bitAtExpr(bitIndex: Int) = "0x${java.lang.Long.toHexString(1L shl bitIndex)}"
+
+val AnnotationExpr.args: Map<String, Expression> get() = when (this) {
+ is MarkerAnnotationExpr -> emptyMap()
+ is SingleMemberAnnotationExpr -> mapOf("value" to memberValue)
+ is NormalAnnotationExpr -> pairs.map { it.name.asString() to it.value }.toMap()
+ else -> throw IllegalArgumentException("Unknown annotation expression: $this")
+}
+
+val TypeDeclaration<*>.nestedTypes get() = childNodes.filterIsInstance<TypeDeclaration<*>>()
+val TypeDeclaration<*>.nestedDataClasses get()
+ = nestedTypes.filterIsInstance<ClassOrInterfaceDeclaration>()
+ .filter { it.annotations.any { it.nameAsString.endsWith("DataClass") } }
+val TypeDeclaration<*>.startLine get() = range.get()!!.begin.line
+
+inline fun <T> List<T>.forEachSequentialPair(action: (T, T?) -> Unit) {
+ forEachIndexed { index, t ->
+ action(t, getOrNull(index + 1))
+ }
+}
+
+fun <T: Node> parseJava(fn: JavaParser.(String) -> ParseResult<T>, source: String): T = try {
+ val parse = JAVA_PARSER.fn(source)
+ if (parse.problems.isNotEmpty()) {
+ throw parseFailed(
+ source,
+ desc = parse.problems.joinToString("\n"),
+ cause = parse.problems.mapNotNull { it.cause.orElse(null) }.firstOrNull())
+ }
+ parse.result.get()
+} catch (e: ParseProblemException) {
+ throw parseFailed(source, cause = e)
+}
+
+private fun parseFailed(source: String, cause: Throwable? = null, desc: String = ""): RuntimeException {
+ return RuntimeException("Failed to parse code:\n" +
+ source
+ .lines()
+ .mapIndexed { lnNum, ln -> "/*$lnNum*/$ln" }
+ .joinToString("\n") + "\n$desc",
+ cause)
+}
+
+var <T> MutableList<T>.last
+ get() = last()
+ set(value) {
+ if (isEmpty()) {
+ add(value)
+ } else {
+ this[size - 1] = value
+ }
+ }
+
+inline fun <T> buildList(init: MutableList<T>.() -> Unit) = mutableListOf<T>().apply(init) \ No newline at end of file