diff options
author | Eugene Susla <eugenesusla@google.com> | 2019-07-25 14:05:12 -0700 |
---|---|---|
committer | Eugene Susla <eugenesusla@google.com> | 2019-08-05 16:54:41 -0700 |
commit | 3156a4ce21cb4de46f84b8c7264a3dc31dd8db8b (patch) | |
tree | cf2a361a56d6f45de239da3ff5f4fa58e1d59431 /tools/codegen/src | |
parent | 2eaec69928b0394b7e6979c71a707d1b2418365c (diff) |
Addresses further review comments from ag/8000041
Including:
- An API to opt out of Int/StringDefs generation on per-field basis
- A way to customize Builder
- Non-optional fields are passed in Builder constructor
- Various adjustments to SampleDataclass examples, as requested
Test: . $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/runTest.sh
Change-Id: I32d2eec52f05d505ff07779d923e4793d3036579
Diffstat (limited to 'tools/codegen/src')
8 files changed, 343 insertions, 188 deletions
diff --git a/tools/codegen/src/com/android/codegen/ClassInfo.kt b/tools/codegen/src/com/android/codegen/ClassInfo.kt index 7ee79f651274..578fb2898480 100644 --- a/tools/codegen/src/com/android/codegen/ClassInfo.kt +++ b/tools/codegen/src/com/android/codegen/ClassInfo.kt @@ -18,13 +18,7 @@ open class ClassInfo(val sourceLines: List<String>) { 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 nestedClasses = classAst.members.filterIsInstance<ClassOrInterfaceDeclaration>() val superInterfaces = (fileAst.types[0] as ClassOrInterfaceDeclaration) .implementedTypes.map { it.asString() } @@ -42,8 +36,4 @@ open class ClassInfo(val sourceLines: List<String>) { .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 index 33256b787964..f1645ea9a3bb 100644 --- a/tools/codegen/src/com/android/codegen/ClassPrinter.kt +++ b/tools/codegen/src/com/android/codegen/ClassPrinter.kt @@ -1,9 +1,9 @@ package com.android.codegen +import com.github.javaparser.ast.Modifier 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.expr.* import com.github.javaparser.ast.type.ClassOrInterfaceType /** @@ -32,10 +32,31 @@ class ClassPrinter( 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 DataClassSuppressConstDefs by lazy { classRef("com.android.internal.util.DataClass.SuppressConstDefsGeneration") } + val DataClassSuppress by lazy { classRef("com.android.internal.util.DataClass.Suppress") } val GeneratedMember by lazy { classRef("com.android.internal.util.DataClass.Generated.Member") } val Parcelling by lazy { classRef("com.android.internal.util.Parcelling") } + val Parcelable by lazy { classRef("android.os.Parcelable") } val UnsupportedAppUsage by lazy { classRef("android.annotation.UnsupportedAppUsage") } + init { + val fieldsWithMissingNullablity = fields.filter { field -> + !field.isPrimitive + && Modifier.TRANSIENT !in field.fieldAst.modifiers + && "@$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") + } + } /** * Optionally shortens a class reference if there's a corresponding import present @@ -54,7 +75,7 @@ class ClassPrinter( return simpleName } else { val outerClass = pkg.substringAfterLast(".", "") - if (outerClass.firstOrNull()?.isUpperCase() ?: false) { + if (outerClass.firstOrNull()?.isUpperCase() == true) { return classRef(pkg) + "." + simpleName } } @@ -89,7 +110,9 @@ class ClassPrinter( ?.toMap() ?: emptyMap() - val internalAnnotations = setOf(ParcelWith, DataClassEnum, PluralOf, Each, UnsupportedAppUsage) + val internalAnnotations = setOf(ParcelWith, DataClassEnum, PluralOf, UnsupportedAppUsage, + DataClassSuppressConstDefs) + val knownNonValidationAnnotations = internalAnnotations + Each + Nullable /** * @return whether the given feature is enabled @@ -109,7 +132,9 @@ class ClassPrinter( return when (this) { FeatureFlag.SETTERS -> !FeatureFlag.CONSTRUCTOR() && !FeatureFlag.BUILDER() && fields.any { !it.isFinal } - FeatureFlag.BUILDER -> cliArgs.contains(FLAG_BUILDER_PROTECTED_SETTERS) || onByDefault + FeatureFlag.BUILDER -> cliArgs.contains(FLAG_BUILDER_PROTECTED_SETTERS) + || fields.any { it.hasDefault } + || onByDefault FeatureFlag.CONSTRUCTOR -> !FeatureFlag.BUILDER() FeatureFlag.PARCELABLE -> "Parcelable" in superInterfaces FeatureFlag.AIDL -> FeatureFlag.PARCELABLE() @@ -287,6 +312,48 @@ class ClassPrinter( 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) + } + + 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 { + return classAst.methods.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}") } init { val builderFactoryOverride = classAst.methods.find { @@ -301,7 +368,7 @@ class ClassPrinter( it.nameAsString == CANONICAL_BUILDER_CLASS } if (builderExtension != null) { - BuilderClass = GENERATED_BUILDER_CLASS + BuilderClass = BASE_BUILDER_CLASS val tp = (builderExtension as ClassOrInterfaceDeclaration).typeParameters BuilderType = if (tp.isEmpty()) BuilderClass else "$BuilderClass<${tp.map { it.nameAsString }.joinToString(", ")}>" diff --git a/tools/codegen/src/com/android/codegen/FieldInfo.kt b/tools/codegen/src/com/android/codegen/FieldInfo.kt index f326fd5601fe..74e79489ad7d 100644 --- a/tools/codegen/src/com/android/codegen/FieldInfo.kt +++ b/tools/codegen/src/com/android/codegen/FieldInfo.kt @@ -9,7 +9,6 @@ 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, @@ -85,7 +84,7 @@ data class FieldInfo( variableAst.initializer.orElse(null)?.let { return it } classInfo.classAst.methods.find { it.nameAsString == "default$NameUpperCamel" && it.parameters.isEmpty() - }?.run { "$nameAsString()" }?.let { return it } + }?.run { return "$nameAsString()" } if (FieldClass == "List") return "${classPrinter.memberRef("java.util.Collections.emptyList")}()" return null } @@ -95,7 +94,7 @@ data class FieldInfo( // Generic args val isArray = Type.endsWith("[]") val isList = FieldClass == "List" || FieldClass == "ArrayList" - val fieldBit = "0x${Long.toHexString(1L shl index)}" + val fieldBit = bitAtExpr(index) var isLast = false val isFinal = fieldAst.isFinal val fieldTypeGenegicArgs = when (typeAst) { @@ -143,8 +142,10 @@ data class FieldInfo( } val annotationsAndType by lazy { (annotationsNoInternal + Type).joinToString(" ") } val sParcelling by lazy { customParcellingClass?.let { "sParcellingFor$NameUpperCamel" } } + + val SetterParamType = if (isArray) "$FieldInnerType..." else Type val annotatedTypeForSetterParam by lazy { - (annotationsNoInternal + if (isArray) "$FieldInnerType..." else Type).joinToString(" ") + (annotationsNoInternal + SetterParamType).joinToString(" ") } // Utilities diff --git a/tools/codegen/src/com/android/codegen/Generators.kt b/tools/codegen/src/com/android/codegen/Generators.kt index ab64f4efc8d8..c6e0a064f9b4 100644 --- a/tools/codegen/src/com/android/codegen/Generators.kt +++ b/tools/codegen/src/com/android/codegen/Generators.kt @@ -1,6 +1,7 @@ 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 @@ -16,7 +17,7 @@ fun ClassPrinter.generateConstDefs() { 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" } @@ -131,7 +132,7 @@ fun ClassPrinter.generateAidl(javaFile: File) { fun ClassPrinter.generateWithers() { fields.forEachApply { val metodName = "with$NameUpperCamel" - if (!hasMethod(metodName, Type)) { + if (!isMethodGenerationSuppressed(metodName, Type)) { generateFieldJavadoc(forceHide = FeatureFlag.WITHERS.hidden) """@$NonNull $GENERATED_MEMBER_HEADER @@ -171,7 +172,7 @@ fun ClassPrinter.generateCopyConstructor() { * ``` */ fun ClassPrinter.generateBuildUpon() { - if (hasMethod("buildUpon")) return + if (isMethodGenerationSuppressed("buildUpon")) return +"/**" +" * Provides an instance of {@link $BuilderClass} with state corresponding to this instance." @@ -195,7 +196,15 @@ fun ClassPrinter.generateBuilder() { val constructorVisibility = if (BuilderClass == CANONICAL_BUILDER_CLASS) "public" else "/* package-*/" - val OneTimeUseBuilder = classRef("android.provider.OneTimeUseBuilder") + val providedSubclassAst = nestedClasses.find { + it.extendedTypes.any { it.nameAsString == BASE_BUILDER_CLASS } + } + + val BuilderSupertype = if (customBaseBuilderAst != null) { + customBaseBuilderAst!!.nameAsString + } else { + "Object" + } +"/**" +" * A builder for {@link $ClassName}" @@ -203,104 +212,155 @@ fun ClassPrinter.generateBuilder() { +" */" +"@SuppressWarnings(\"WeakerAccess\")" +GENERATED_MEMBER_HEADER - "public static class $BuilderClass$genericArgs" { - +"extends $OneTimeUseBuilder<$ClassType>" + !"public static class $BuilderClass$genericArgs" + if (BuilderSupertype != "Object") { + appendSameLine(" extends $BuilderSupertype") } " {" { +"" fields.forEachApply { - +"protected $annotationsAndType $name;" + +"private $annotationsAndType $name;" } +"" - +"protected long mBuilderFieldsSet = 0L;" - +"" - +"$constructorVisibility $BuilderClass() {};" + +"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 + "$visibility @$NonNull $ReturnType $name(${if_(!ParamAnnotations.isNullOrEmpty(), "$ParamAnnotations ")}${ + paramTypes.zip(paramNames).joinToString(", ") { (Type, paramName) -> "$Type $paramName" } + })" { + genBody() + } + } +} + 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)" { + val setterName = "set$NameUpperCamel" + + generateBuilderMethod( + name = setterName, + defVisibility = visibility, + ParamAnnotations = annotationsNoInternal.joinToString(" "), + paramTypes = listOf(SetterParamType), + genJavadoc = { generateFieldJavadoc() }) { +"checkNotUsed();" +"mBuilderFieldsSet |= $fieldBit;" +"$name = value;" +"return$maybeCast this;" } + val javadocSeeSetter = "/** @see #$setterName */" + val adderName = "add$SingularName" - val 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) { + generateBuilderMethod( + name = adderName, + defVisibility = visibility, + paramTypes = listOf(FieldInnerType), + genJavadoc = { +javadocSeeSetter }) { - +javadocSeeSetter - +GENERATED_MEMBER_HEADER - "$visibility $CANONICAL_BUILDER_CLASS add$SingularName(@$NonNull $FieldInnerType value)" { !singularNameCustomizationHint - +"if ($name == null) set$NameUpperCamel(new $ArrayList<>());" + +"if ($name == null) $setterName(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)" { + generateBuilderMethod( + name = adderName, + defVisibility = visibility, + paramTypes = fieldTypeGenegicArgs, + paramNames = listOf("key", "value"), + genJavadoc = { +javadocSeeSetter }) { !singularNameCustomizationHint - +"if ($name == null) set$NameUpperCamel(new $LinkedHashMap());" + +"if ($name == null) $setterName(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();" + +"checkNotUsed();" + +"mBuilderFieldsSet |= ${bitAtExpr(fields.size)}; // Mark builder used" + +"" fields.forEachApply { - if (!isNullable || hasDefault) { + if (hasDefault) { "if ((mBuilderFieldsSet & $fieldBit) == 0)" { - if (!isNullable && !hasDefault) { - +"throw new IllegalStateException(\"Required field not set: $nameLowerCamel\");" - } else { - +"$name = $defaultExpr;" - } + +"$name = $defaultExpr;" } } } @@ -348,7 +408,7 @@ fun ClassPrinter.generateParcelable() { } val Parcel = classRef("android.os.Parcel") - if (!hasMethod("writeToParcel", Parcel, "int")) { + if (!isMethodGenerationSuppressed("writeToParcel", Parcel, "int")) { +"@Override" +GENERATED_MEMBER_HEADER "public void writeToParcel($Parcel dest, int flags)" { @@ -390,7 +450,7 @@ fun ClassPrinter.generateParcelable() { } } - if (!hasMethod("describeContents")) { + if (!isMethodGenerationSuppressed("describeContents")) { +"@Override" +GENERATED_MEMBER_HEADER +"public int describeContents() { return 0; }" @@ -442,8 +502,6 @@ fun ClassPrinter.generateParcelable() { 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() @@ -519,7 +577,7 @@ fun ClassPrinter.generateParcelable() { } fun ClassPrinter.generateEqualsHashcode() { - if (!hasMethod("equals", "Object")) { + if (!isMethodGenerationSuppressed("equals", "Object")) { +"@Override" +GENERATED_MEMBER_HEADER "public boolean equals(Object o)" { @@ -546,7 +604,7 @@ fun ClassPrinter.generateEqualsHashcode() { } } - if (!hasMethod("hashCode")) { + if (!isMethodGenerationSuppressed("hashCode")) { +"@Override" +GENERATED_MEMBER_HEADER "public int hashCode()" { @@ -572,7 +630,7 @@ fun ClassPrinter.generateEqualsHashcode() { //TODO support IntDef flags? fun ClassPrinter.generateToString() { - if (!hasMethod("toString")) { + if (!isMethodGenerationSuppressed("toString")) { +"@Override" +GENERATED_MEMBER_HEADER "public String toString()" { @@ -599,7 +657,7 @@ fun ClassPrinter.generateToString() { fun ClassPrinter.generateSetters() { fields.forEachApply { - if (!hasMethod("set$NameUpperCamel", Type) + if (!isMethodGenerationSuppressed("set$NameUpperCamel", Type) && !fieldAst.isPublic && !isFinal) { @@ -618,7 +676,7 @@ fun ClassPrinter.generateGetters() { val methodPrefix = if (Type == "boolean") "is" else "get" val methodName = methodPrefix + NameUpperCamel - if (!hasMethod(methodName) && !fieldAst.isPublic) { + if (!isMethodGenerationSuppressed(methodName) && !fieldAst.isPublic) { generateFieldJavadoc(forceHide = FeatureFlag.GETTERS.hidden) +GENERATED_MEMBER_HEADER @@ -662,23 +720,8 @@ fun FieldInfo.generateFieldJavadoc(forceHide: Boolean = false) = classPrinter { } 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\");" - } - } + +"$name = $source;" + generateFieldValidation(field = this@generateSetFrom) } fun ClassPrinter.generateConstructor(visibility: String = "public") { @@ -697,15 +740,18 @@ fun ClassPrinter.generateConstructor(visibility: String = "public") { generateSetFrom(nameLowerCamel) } - generateStateValidation() - generateOnConstructedCallback() } } -private fun ClassPrinter.generateConstructorJavadoc() { +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" @@ -718,87 +764,97 @@ private fun ClassPrinter.generateConstructorJavadoc() { +" */" } -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" +private fun ClassPrinter.appendLinesWithContinuationIndent(text: String) { + val lines = text.lines() + if (lines.isNotEmpty()) { + !lines[0] + } + if (lines.size >= 2) { + "" { + lines.drop(1).forEach { + +it } } - +";" } +} - fields.forEachApply { - if (intOrStringDef != null) { - if (intOrStringDef!!.type == ConstDef.Type.INT_FLAGS) { - +"" - +"//noinspection PointlessBitwiseExpression" - "$Preconditions.checkFlagsArgument(" { - "$name, 0" { - intOrStringDef!!.CONST_NAMES.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, ", ")}"""" } } } +";" - } 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 && + 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) + val Size = classRef("android.annotation.Size") + 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")) + } } + } +} - 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" + 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" } } + +";" } private fun ClassPrinter.generateOnConstructedCallback(prefix: String = "") { @@ -845,3 +901,15 @@ fun ClassPrinter.generateForEachField() { } } } + +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/InputSignaturesComputation.kt b/tools/codegen/src/com/android/codegen/InputSignaturesComputation.kt index d1dc88f4a773..1e7a2674006b 100644 --- a/tools/codegen/src/com/android/codegen/InputSignaturesComputation.kt +++ b/tools/codegen/src/com/android/codegen/InputSignaturesComputation.kt @@ -1,6 +1,6 @@ package com.android.codegen -import com.github.javaparser.ast.body.TypeDeclaration +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 @@ -8,9 +8,17 @@ 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.asString()}) + append(fieldAst.modifiers.joinToString(" ") { it.asString() }) append(" ") append(annotationsToString(fieldAst)) append(" ") @@ -20,7 +28,7 @@ fun ClassPrinter.getInputSignatures(): List<String> { } } + classAst.methods.map { methodAst -> buildString { - append(methodAst.modifiers.joinToString(" ") {it.asString()}) + append(methodAst.modifiers.joinToString(" ") { it.asString() }) append(" ") append(annotationsToString(methodAst)) append(" ") @@ -28,19 +36,26 @@ fun ClassPrinter.getInputSignatures(): List<String> { append(" ") append(methodAst.nameAsString) append("(") - append(methodAst.parameters.joinToString(",") {getFullClassName(it.type)}) + 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.joinToString(" ") { - annotationToString(it) - } + return annotatedAst + .annotations + .groupBy { it.nameAsString } // dedupe annotations by name (javaparser bug?) + .values + .joinToString(" ") { + annotationToString(it[0]) + } } -private fun ClassPrinter.annotationToString(ann: AnnotationExpr): String { +private fun ClassPrinter.annotationToString(ann: AnnotationExpr?): String { + if (ann == null) return "" return buildString { append("@") append(getFullClassName(ann.nameAsString)) @@ -78,9 +93,9 @@ private fun ClassPrinter.appendExpr(sb: StringBuilder, ex: Expression?) { 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()) @@ -100,10 +115,16 @@ private fun ClassPrinter.getFullClassName(className: String): String { val thisPackagePrefix = fileAst.packageDeclaration.map { it.nameAsString + "." }.orElse("") val thisClassPrefix = thisPackagePrefix + classAst.nameAsString + "." - classAst.childNodes.filterIsInstance<TypeDeclaration<*>>().find { + 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) { diff --git a/tools/codegen/src/com/android/codegen/Main.kt b/tools/codegen/src/com/android/codegen/Main.kt index 8fafa7ce9b1e..f71bfd302d2e 100755 --- a/tools/codegen/src/com/android/codegen/Main.kt +++ b/tools/codegen/src/com/android/codegen/Main.kt @@ -9,9 +9,6 @@ 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" @@ -66,10 +63,10 @@ Special methods/etc. you can define: 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 + 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. - $GENERATED_BUILDER_CLASS's constructor(s) will be package-private to encourage using $CANONICAL_BUILDER_CLASS instead + $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 @@ -131,7 +128,6 @@ fun main(args: Array<String>) { // $GENERATED_WARNING_PREFIX v$CODEGEN_VERSION. - // on ${currentTimestamp()} // // DO NOT MODIFY! // @@ -143,14 +139,6 @@ fun main(args: Array<String>) { 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") @@ -160,6 +148,7 @@ fun main(args: Array<String>) { || FeatureFlag.PARCELABLE()) { generateConstructor("/* package-private */") } + if (FeatureFlag.COPY_CONSTRUCTOR()) generateCopyConstructor() if (FeatureFlag.GETTERS()) generateGetters() if (FeatureFlag.SETTERS()) generateSetters() @@ -168,7 +157,6 @@ fun main(args: Array<String>) { if (FeatureFlag.FOR_EACH_FIELD()) generateForEachField() - if (FeatureFlag.COPY_CONSTRUCTOR()) generateCopyConstructor() if (FeatureFlag.WITHERS()) generateWithers() if (FeatureFlag.PARCELABLE()) generateParcelable() @@ -178,6 +166,8 @@ fun main(args: Array<String>) { if (FeatureFlag.AIDL()) generateAidl(file) + generateMetadata(file) + rmEmptyLine() } stringBuilder.append("\n}\n") diff --git a/tools/codegen/src/com/android/codegen/SharedConstants.kt b/tools/codegen/src/com/android/codegen/SharedConstants.kt index 175eea6ef0d0..7d50ad10de00 100644 --- a/tools/codegen/src/com/android/codegen/SharedConstants.kt +++ b/tools/codegen/src/com/android/codegen/SharedConstants.kt @@ -1,4 +1,7 @@ package com.android.codegen const val CODEGEN_NAME = "codegen" -const val CODEGEN_VERSION = "1.0.0"
\ No newline at end of file +const val CODEGEN_VERSION = "1.0.0" + +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 index 95c99092e2ab..73ceac41682e 100644 --- a/tools/codegen/src/com/android/codegen/Utils.kt +++ b/tools/codegen/src/com/android/codegen/Utils.kt @@ -1,8 +1,10 @@ package com.android.codegen +import com.github.javaparser.ast.Modifier import com.github.javaparser.ast.expr.AnnotationExpr import com.github.javaparser.ast.expr.Expression import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr +import com.github.javaparser.ast.nodeTypes.NodeWithModifiers import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -22,6 +24,8 @@ 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() } @@ -73,4 +77,15 @@ inline operator fun <reified T> Array<T>.minus(item: T) = toList().minus(item).t fun currentTimestamp() = DateTimeFormatter .ofLocalizedDateTime(/* date */ FormatStyle.MEDIUM, /* time */ FormatStyle.LONG) .withZone(ZoneId.systemDefault()) - .format(Instant.now())
\ No newline at end of file + .format(Instant.now()) + +val NodeWithModifiers<*>.visibility get() = Modifier.getAccessSpecifier(modifiers) + +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)}" + |