diff options
Diffstat (limited to 'tools/codegen/src')
-rw-r--r-- | tools/codegen/src/com/android/codegen/ClassInfo.kt | 49 | ||||
-rw-r--r-- | tools/codegen/src/com/android/codegen/ClassPrinter.kt | 311 | ||||
-rw-r--r-- | tools/codegen/src/com/android/codegen/ConstDef.kt | 17 | ||||
-rw-r--r-- | tools/codegen/src/com/android/codegen/FeatureFlag.kt | 27 | ||||
-rw-r--r-- | tools/codegen/src/com/android/codegen/FieldInfo.kt | 216 | ||||
-rw-r--r-- | tools/codegen/src/com/android/codegen/Generators.kt | 847 | ||||
-rw-r--r-- | tools/codegen/src/com/android/codegen/InputSignaturesComputation.kt | 122 | ||||
-rwxr-xr-x | tools/codegen/src/com/android/codegen/Main.kt | 199 | ||||
-rw-r--r-- | tools/codegen/src/com/android/codegen/SharedConstants.kt | 4 | ||||
-rw-r--r-- | tools/codegen/src/com/android/codegen/Utils.kt | 76 |
10 files changed, 1868 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..7ee79f651274 --- /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 000000000000..33256b787964 --- /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 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..f326fd5601fe --- /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 000000000000..ab64f4efc8d8 --- /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 000000000000..d1dc88f4a773 --- /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 000000000000..8fafa7ce9b1e --- /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 000000000000..175eea6ef0d0 --- /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 = "1.0.0"
\ 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 000000000000..95c99092e2ab --- /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 |