diff options
Diffstat (limited to 'tools/codegen/src')
-rw-r--r-- | tools/codegen/src/com/android/codegen/ClassInfo.kt | 27 | ||||
-rw-r--r-- | tools/codegen/src/com/android/codegen/ClassPrinter.kt | 234 | ||||
-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 | 230 | ||||
-rw-r--r-- | tools/codegen/src/com/android/codegen/FileInfo.kt | 289 | ||||
-rw-r--r-- | tools/codegen/src/com/android/codegen/Generators.kt | 949 | ||||
-rw-r--r-- | tools/codegen/src/com/android/codegen/ImportsProvider.kt | 92 | ||||
-rw-r--r-- | tools/codegen/src/com/android/codegen/InputSignaturesComputation.kt | 151 | ||||
-rwxr-xr-x | tools/codegen/src/com/android/codegen/Main.kt | 136 | ||||
-rw-r--r-- | tools/codegen/src/com/android/codegen/Printer.kt | 186 | ||||
-rw-r--r-- | tools/codegen/src/com/android/codegen/SharedConstants.kt | 7 | ||||
-rw-r--r-- | tools/codegen/src/com/android/codegen/Utils.kt | 146 |
13 files changed, 2491 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..b90e1bb3e7e7 --- /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, MaySetToNull, Each, DataClass) + 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..02ebaef90f0b --- /dev/null +++ b/tools/codegen/src/com/android/codegen/FieldInfo.kt @@ -0,0 +1,230 @@ +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 annotationsForSetterParam by lazy { + buildList<String> { + addAll(annotationsNoInternal) + classPrinter { + if ("@$Nullable" in annotations + && "@$MaySetToNull" !in annotations) { + remove("@$Nullable") + add("@$NonNull") + } + } + }.joinToString(" ") + } + val annotatedTypeForSetterParam by lazy { "$annotationsForSetterParam $SetterParamType" } + + // 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..5a96cf1d9bdb --- /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 = annotationsForSetterParam, + 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..27dd9587db25 --- /dev/null +++ b/tools/codegen/src/com/android/codegen/ImportsProvider.kt @@ -0,0 +1,92 @@ +/* + * 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 MaySetToNull: String get() { return classRef("com.android.internal.util.DataClass.MaySetToNull") } + 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.compat.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..6f740cd663e3 --- /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.15" + +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 |