summaryrefslogtreecommitdiff
path: root/tools/codegen/src
diff options
context:
space:
mode:
authorTreeHugger Robot <treehugger-gerrit@google.com>2019-11-01 20:52:39 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2019-11-01 20:52:39 +0000
commit184d14428a640fc7a55b2eda31dd351aba3e3195 (patch)
tree215cf7def0f1e622615fac36c6e0dda93e6e2888 /tools/codegen/src
parent419325def00977f1eaba99641ec23e4430e03063 (diff)
parent322e8b17721a6956e00407e8d431ceecbd245b5c (diff)
Merge "[codegen] Support nested classes"
Diffstat (limited to 'tools/codegen/src')
-rw-r--r--tools/codegen/src/com/android/codegen/ClassInfo.kt40
-rw-r--r--tools/codegen/src/com/android/codegen/ClassPrinter.kt282
-rw-r--r--tools/codegen/src/com/android/codegen/FieldInfo.kt12
-rw-r--r--tools/codegen/src/com/android/codegen/FileInfo.kt289
-rw-r--r--tools/codegen/src/com/android/codegen/Generators.kt6
-rw-r--r--tools/codegen/src/com/android/codegen/ImportsProvider.kt91
-rwxr-xr-xtools/codegen/src/com/android/codegen/Main.kt77
-rw-r--r--tools/codegen/src/com/android/codegen/Printer.kt186
-rw-r--r--tools/codegen/src/com/android/codegen/Utils.kt52
9 files changed, 695 insertions, 340 deletions
diff --git a/tools/codegen/src/com/android/codegen/ClassInfo.kt b/tools/codegen/src/com/android/codegen/ClassInfo.kt
index 92da9dab863b..bf95a2eb2193 100644
--- a/tools/codegen/src/com/android/codegen/ClassInfo.kt
+++ b/tools/codegen/src/com/android/codegen/ClassInfo.kt
@@ -1,47 +1,15 @@
package com.android.codegen
-import com.github.javaparser.ParseProblemException
-import com.github.javaparser.ParseResult
-import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
-open class ClassInfo(val sourceLines: List<String>) {
+open class ClassInfo(val classAst: ClassOrInterfaceDeclaration, val fileInfo: FileInfo) {
- private val userSourceCode = (sourceLines + "}").joinToString("\n")
- val fileAst: CompilationUnit = try {
- JAVA_PARSER.parse(userSourceCode).throwIfFailed()
- } catch (e: ParseProblemException) {
- throw parseFailed(cause = e)
- }
-
- fun <T> ParseResult<T>.throwIfFailed(): T {
- if (problems.isNotEmpty()) {
- throw parseFailed(
- desc = this@throwIfFailed.problems.joinToString("\n"),
- cause = this@throwIfFailed.problems.mapNotNull { it.cause.orElse(null) }.firstOrNull())
- }
- return result.get()
- }
+ val fileAst = fileInfo.fileAst
- private fun parseFailed(cause: Throwable? = null, desc: String = ""): RuntimeException {
- return RuntimeException("Failed to parse code:\n" +
- userSourceCode
- .lines()
- .mapIndexed { lnNum, ln -> "/*$lnNum*/$ln" }
- .joinToString("\n") + "\n$desc",
- cause)
- }
-
- val classAst = fileAst.types[0] as ClassOrInterfaceDeclaration
val nestedClasses = classAst.members.filterIsInstance<ClassOrInterfaceDeclaration>()
- val superInterfaces = (fileAst.types[0] as ClassOrInterfaceDeclaration)
- .implementedTypes.map { it.asString() }
-
- val superClass = run {
- val superClasses = (fileAst.types[0] as ClassOrInterfaceDeclaration).extendedTypes
- if (superClasses.isNonEmpty) superClasses[0] else null
- }
+ val superInterfaces = classAst.implementedTypes.map { it.asString() }
+ val superClass = classAst.extendedTypes.getOrNull(0)
val ClassName = classAst.nameAsString
private val genericArgsAst = classAst.typeParameters
diff --git a/tools/codegen/src/com/android/codegen/ClassPrinter.kt b/tools/codegen/src/com/android/codegen/ClassPrinter.kt
index bd72d9e7ec21..a4fd374d0c6e 100644
--- a/tools/codegen/src/com/android/codegen/ClassPrinter.kt
+++ b/tools/codegen/src/com/android/codegen/ClassPrinter.kt
@@ -11,36 +11,12 @@ import com.github.javaparser.ast.type.ClassOrInterfaceType
* [ClassInfo] + utilities for printing out new class code with proper indentation and imports
*/
class ClassPrinter(
- source: List<String>,
- private val stringBuilder: StringBuilder,
- var cliArgs: Array<String>
-) : ClassInfo(source) {
+ classAst: ClassOrInterfaceDeclaration,
+ fileInfo: FileInfo
+) : ClassInfo(classAst, fileInfo), Printer<ClassPrinter>, ImportsProvider {
val GENERATED_MEMBER_HEADER by lazy { "@$GeneratedMember" }
- // Imports
- val NonNull by lazy { classRef("android.annotation.NonNull") }
- val NonEmpty by lazy { classRef("android.annotation.NonEmpty") }
- val Nullable by lazy { classRef("android.annotation.Nullable") }
- val TextUtils by lazy { classRef("android.text.TextUtils") }
- val LinkedHashMap by lazy { classRef("java.util.LinkedHashMap") }
- val Collections by lazy { classRef("java.util.Collections") }
- val Preconditions by lazy { classRef("com.android.internal.util.Preconditions") }
- val ArrayList by lazy { classRef("java.util.ArrayList") }
- val DataClass by lazy { classRef("com.android.internal.util.DataClass") }
- val DataClassEnum by lazy { classRef("com.android.internal.util.DataClass.Enum") }
- val ParcelWith by lazy { classRef("com.android.internal.util.DataClass.ParcelWith") }
- val PluralOf by lazy { classRef("com.android.internal.util.DataClass.PluralOf") }
- val Each by lazy { classRef("com.android.internal.util.DataClass.Each") }
- val DataClassGenerated by lazy { classRef("com.android.internal.util.DataClass.Generated") }
- val 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 Parcel by lazy { classRef("android.os.Parcel") }
- val UnsupportedAppUsage by lazy { classRef("android.annotation.UnsupportedAppUsage") }
-
init {
val fieldsWithMissingNullablity = fields.filter { field ->
!field.isPrimitive
@@ -60,50 +36,61 @@ class ClassPrinter(
}
}
- /**
- * Optionally shortens a class reference if there's a corresponding import present
- */
- fun classRef(fullName: String): String {
- if (cliArgs.contains(FLAG_NO_FULL_QUALIFIERS)) {
- return fullName.split(".").dropWhile { it[0].isLowerCase() }.joinToString(".")
- }
+ val cliArgs get() = fileInfo.cliArgs
- 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
- }
+ fun print() {
+ currentIndent = fileInfo.sourceLines
+ .find { "class $ClassName" in it }!!
+ .takeWhile { it.isWhitespace() }
+ .plus(INDENT_SINGLE)
- /** @see classRef */
- inline fun <reified T : Any> classRef(): String {
- return classRef(T::class.java.name)
- }
+ +fileInfo.generatedWarning
- /** @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
+ 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 }
@@ -143,7 +130,7 @@ class ClassPrinter(
|| onByDefault
FeatureFlag.CONSTRUCTOR -> !FeatureFlag.BUILDER()
FeatureFlag.PARCELABLE -> "Parcelable" in superInterfaces
- FeatureFlag.AIDL -> FeatureFlag.PARCELABLE()
+ FeatureFlag.AIDL -> fileInfo.mainClass.nameAsString == ClassName && FeatureFlag.PARCELABLE()
FeatureFlag.IMPLICIT_NONNULL -> fields.any { it.isNullable }
&& fields.none { "@$NonNull" in it.annotations }
else -> onByDefault
@@ -163,162 +150,7 @@ class ClassPrinter(
}
}
- var currentIndent = INDENT_SINGLE
- private set
-
- fun pushIndent() {
- currentIndent += INDENT_SINGLE
- }
-
- fun popIndent() {
- currentIndent = currentIndent.substring(0, currentIndent.length - INDENT_SINGLE.length)
- }
-
- fun backspace() = stringBuilder.setLength(stringBuilder.length - 1)
- val lastChar get() = stringBuilder[stringBuilder.length - 1]
-
- private fun appendRaw(s: String) {
- stringBuilder.append(s)
- }
-
- fun append(s: String) {
- if (s.isBlank() && s != "\n") {
- appendRaw(s)
- } else {
- appendRaw(s.lines().map { line ->
- if (line.startsWith(" *")) line else line.trimStart()
- }.joinToString("\n$currentIndent"))
- }
- }
-
- fun appendSameLine(s: String) {
- while (lastChar.isWhitespace() || lastChar.isNewline()) {
- backspace()
- }
- appendRaw(s)
- }
-
- fun rmEmptyLine() {
- while (lastChar.isWhitespaceNonNewline()) backspace()
- if (lastChar.isNewline()) backspace()
- }
-
- /**
- * Syntactic sugar for:
- * ```
- * +"code()";
- * ```
- * to append the given string plus a newline
- */
- operator fun String.unaryPlus() = append("$this\n")
-
- /**
- * Syntactic sugar for:
- * ```
- * !"code()";
- * ```
- * to append the given string without a newline
- */
- operator fun String.not() = append(this)
-
- /**
- * Syntactic sugar for:
- * ```
- * ... {
- * ...
- * }+";"
- * ```
- * to append a ';' on same line after a block, and a newline afterwards
- */
- operator fun Unit.plus(s: String) {
- appendSameLine(s)
- +""
- }
-
- /**
- * A multi-purpose syntactic sugar for appending the given string plus anything generated in
- * the given [block], the latter with the appropriate deeper indent,
- * and resetting the indent back to original at the end
- *
- * Usage examples:
- *
- * ```
- * "if (...)" {
- * ...
- * }
- * ```
- * to append a corresponding if block appropriate indentation
- *
- * ```
- * "void foo(...)" {
- * ...
- * }
- * ```
- * similar to the previous one, plus an extra empty line after the function body
- *
- * ```
- * "void foo(" {
- * <args code>
- * }
- * ```
- * to use proper indentation for args code and close the bracket on same line at end
- *
- * ```
- * "..." {
- * ...
- * }
- * to use the correct indentation for inner code, resetting it at the end
- */
- inline operator fun String.invoke(block: ClassPrinter.() -> Unit) {
- if (this == " {") {
- appendSameLine(this)
- } else {
- append(this)
- }
- when {
- endsWith("(") -> {
- indentedBy(2, block)
- appendSameLine(")")
- }
- endsWith("{") || endsWith(")") -> {
- if (!endsWith("{")) appendSameLine(" {")
- indentedBy(1, block)
- +"}"
- if ((endsWith(") {") || endsWith(")") || this == " {")
- && !startsWith("synchronized")
- && !startsWith("switch")
- && !startsWith("if ")
- && !contains(" else ")
- && !contains("new ")
- && !contains("return ")) {
- +"" // extra line after function definitions
- }
- }
- else -> indentedBy(2, block)
- }
- }
-
- inline fun indentedBy(level: Int, block: ClassPrinter.() -> Unit) {
- append("\n")
- level times {
- append(INDENT_SINGLE)
- pushIndent()
- }
- block()
- level times { popIndent() }
- rmEmptyLine()
- +""
- }
- inline fun Iterable<FieldInfo>.forEachTrimmingTrailingComma(b: FieldInfo.() -> Unit) {
- forEachApply {
- b()
- if (isLast) {
- while (lastChar == ' ' || lastChar == '\n') backspace()
- if (lastChar == ',') backspace()
- }
- }
- }
inline operator fun <R> invoke(f: ClassPrinter.() -> R): R = run(f)
@@ -381,10 +213,10 @@ class ClassPrinter(
BuilderClass = (builderFactoryOverride.type as ClassOrInterfaceType).nameAsString
BuilderType = builderFactoryOverride.type.asString()
} else {
- val builderExtension = (fileAst.types
- + classAst.childNodes.filterIsInstance(TypeDeclaration::class.java)).find {
- it.nameAsString == CANONICAL_BUILDER_CLASS
- }
+ 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
diff --git a/tools/codegen/src/com/android/codegen/FieldInfo.kt b/tools/codegen/src/com/android/codegen/FieldInfo.kt
index 1a7fd6e241aa..ed35a1dfc599 100644
--- a/tools/codegen/src/com/android/codegen/FieldInfo.kt
+++ b/tools/codegen/src/com/android/codegen/FieldInfo.kt
@@ -1,5 +1,6 @@
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
@@ -111,11 +112,12 @@ data class FieldInfo(
val annotations by lazy {
if (FieldClass in BUILTIN_SPECIAL_PARCELLINGS) {
classPrinter {
- fieldAst.addAnnotation(SingleMemberAnnotationExpr(
- Name(ParcelWith),
- ClassExpr(JAVA_PARSER
- .parseClassOrInterfaceType("$Parcelling.BuiltIn.For$FieldClass")
- .throwIfFailed())))
+ fileInfo.apply {
+ fieldAst.addAnnotation(SingleMemberAnnotationExpr(
+ Name(ParcelWith),
+ ClassExpr(parseJava(JavaParser::parseClassOrInterfaceType,
+ "$Parcelling.BuiltIn.For$FieldClass"))))
+ }
}
}
fieldAst.annotations.map { it.removeComment().toString() }
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..9c15fbf84223
--- /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 = fileInfo.sourceLines.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
index bd32f9c6d9cd..c25d0c74f251 100644
--- a/tools/codegen/src/com/android/codegen/Generators.kt
+++ b/tools/codegen/src/com/android/codegen/Generators.kt
@@ -119,14 +119,14 @@ fun ClassPrinter.generateConstDef(consts: List<Pair<VariableDeclarator, FieldDec
}
}
-fun ClassPrinter.generateAidl(javaFile: File) {
- val aidl = File(javaFile.path.substringBeforeLast(".java") + ".aidl")
+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 $ClassName;\n")
+ append("\nparcelable ${mainClass.nameAsString};\n")
})
}
diff --git a/tools/codegen/src/com/android/codegen/ImportsProvider.kt b/tools/codegen/src/com/android/codegen/ImportsProvider.kt
new file mode 100644
index 000000000000..ba0a0318c843
--- /dev/null
+++ b/tools/codegen/src/com/android/codegen/ImportsProvider.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.codegen
+
+import com.github.javaparser.ast.CompilationUnit
+
+/**
+ * Mixin for optionally shortening references based on existing imports
+ */
+interface ImportsProvider {
+
+ abstract val fileAst: CompilationUnit
+
+ val NonNull: String get() { return classRef("android.annotation.NonNull") }
+ val NonEmpty: String get() { return classRef("android.annotation.NonEmpty") }
+ val Nullable: String get() { return classRef("android.annotation.Nullable") }
+ val TextUtils: String get() { return classRef("android.text.TextUtils") }
+ val LinkedHashMap: String get() { return classRef("java.util.LinkedHashMap") }
+ val Collections: String get() { return classRef("java.util.Collections") }
+ val Preconditions: String get() { return classRef("com.android.internal.util.Preconditions") }
+ val ArrayList: String get() { return classRef("java.util.ArrayList") }
+ val DataClass: String get() { return classRef("com.android.internal.util.DataClass") }
+ val DataClassEnum: String get() { return classRef("com.android.internal.util.DataClass.Enum") }
+ val ParcelWith: String get() { return classRef("com.android.internal.util.DataClass.ParcelWith") }
+ val PluralOf: String get() { return classRef("com.android.internal.util.DataClass.PluralOf") }
+ val Each: String get() { return classRef("com.android.internal.util.DataClass.Each") }
+ val DataClassGenerated: String get() { return classRef("com.android.internal.util.DataClass.Generated") }
+ val DataClassSuppressConstDefs: String get() { return classRef("com.android.internal.util.DataClass.SuppressConstDefsGeneration") }
+ val DataClassSuppress: String get() { return classRef("com.android.internal.util.DataClass.Suppress") }
+ val GeneratedMember: String get() { return classRef("com.android.internal.util.DataClass.Generated.Member") }
+ val Parcelling: String get() { return classRef("com.android.internal.util.Parcelling") }
+ val Parcelable: String get() { return classRef("android.os.Parcelable") }
+ val Parcel: String get() { return classRef("android.os.Parcel") }
+ val UnsupportedAppUsage: String get() { return classRef("android.annotation.UnsupportedAppUsage") }
+
+ /**
+ * Optionally shortens a class reference if there's a corresponding import present
+ */
+ fun classRef(fullName: String): String {
+
+ val pkg = fullName.substringBeforeLast(".")
+ val simpleName = fullName.substringAfterLast(".")
+ if (fileAst.imports.any { imprt ->
+ imprt.nameAsString == fullName
+ || (imprt.isAsterisk && imprt.nameAsString == pkg)
+ }) {
+ return simpleName
+ } else {
+ val outerClass = pkg.substringAfterLast(".", "")
+ if (outerClass.firstOrNull()?.isUpperCase() == true) {
+ return classRef(pkg) + "." + simpleName
+ }
+ }
+ return fullName
+ }
+
+ /** @see classRef */
+ fun memberRef(fullName: String): String {
+ val className = fullName.substringBeforeLast(".")
+ val methodName = fullName.substringAfterLast(".")
+ return if (fileAst.imports.any {
+ it.isStatic
+ && (it.nameAsString == fullName
+ || (it.isAsterisk && it.nameAsString == className))
+ }) {
+ className.substringAfterLast(".") + "." + methodName
+ } else {
+ classRef(className) + "." + methodName
+ }
+ }
+}
+
+/** @see classRef */
+inline fun <reified T : Any> ImportsProvider.classRef(): String {
+ return classRef(T::class.java.name)
+} \ No newline at end of file
diff --git a/tools/codegen/src/com/android/codegen/Main.kt b/tools/codegen/src/com/android/codegen/Main.kt
index ce83d3dc8e51..4b508d022991 100755
--- a/tools/codegen/src/com/android/codegen/Main.kt
+++ b/tools/codegen/src/com/android/codegen/Main.kt
@@ -6,6 +6,7 @@ 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")
@@ -115,81 +116,15 @@ fun main(args: Array<String>) {
System.exit(0)
}
val file = File(args.last()).absoluteFile
- val sourceLinesNoClosingBrace = file.readLines().dropLastWhile {
+ val sourceLisnesOriginal = file.readLines()
+ val sourceLinesNoClosingBrace = sourceLisnesOriginal.dropLastWhile {
it.startsWith("}") || it.all(Char::isWhitespace)
}
val cliArgs = handleUpdateFlag(args, sourceLinesNoClosingBrace)
- val sourceLinesAsIs = discardGeneratedCode(sourceLinesNoClosingBrace)
- val sourceLines = sourceLinesAsIs
- .filterNot { it.trim().startsWith("//") }
- .map { it.trimEnd().dropWhile { it == '\n' } }
- val stringBuilder = StringBuilder(sourceLinesAsIs.joinToString("\n"))
- ClassPrinter(sourceLines, stringBuilder, cliArgs).run {
-
- val cliExecutable = "$THIS_SCRIPT_LOCATION$CODEGEN_NAME"
- val fileEscaped = file.absolutePath.replace(
- System.getenv("ANDROID_BUILD_TOP"), "\$ANDROID_BUILD_TOP")
-
-
- +"""
-
-
-
- // $GENERATED_WARNING_PREFIX v$CODEGEN_VERSION.
- //
- // DO NOT MODIFY!
- // CHECKSTYLE:OFF Generated code
- //
- // To regenerate run:
- // $ $cliExecutable ${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
-
- """
-
- 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()) generateAidl(file)
-
- generateMetadata(file)
-
- rmEmptyLine()
- }
- stringBuilder.append("\n}\n")
- file.writeText(stringBuilder.toString().mapLines { trimEnd() })
-}
-
-internal fun discardGeneratedCode(sourceLinesNoClosingBrace: List<String>): List<String> {
- return sourceLinesNoClosingBrace
- .takeWhile { GENERATED_WARNING_PREFIX !in it }
- .dropLastWhile(String::isBlank)
+ 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> {
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/Utils.kt b/tools/codegen/src/com/android/codegen/Utils.kt
index e703397214eb..c19ae3b0b11f 100644
--- a/tools/codegen/src/com/android/codegen/Utils.kt
+++ b/tools/codegen/src/com/android/codegen/Utils.kt
@@ -1,5 +1,11 @@
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
@@ -92,3 +98,49 @@ val AnnotationExpr.args: Map<String, Expression> get() = when (this) {
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