From 322e8b17721a6956e00407e8d431ceecbd245b5c Mon Sep 17 00:00:00 2001 From: Eugene Susla Date: Tue, 22 Oct 2019 17:32:08 -0700 Subject: [codegen] Support nested classes Adds support for arbitrarily-nested @DataClasses Only static ones are supported for now See FileInfo for the main implementation piece Fixes: 139833958 Test: . frameworks/base/tests/Codegen/runTest.sh Change-Id: I31cd16969788c47003a7a15a3573a4bf623ab960 --- tools/codegen/src/com/android/codegen/ClassInfo.kt | 40 +-- .../src/com/android/codegen/ClassPrinter.kt | 282 ++++---------------- tools/codegen/src/com/android/codegen/FieldInfo.kt | 12 +- tools/codegen/src/com/android/codegen/FileInfo.kt | 289 +++++++++++++++++++++ .../codegen/src/com/android/codegen/Generators.kt | 6 +- .../src/com/android/codegen/ImportsProvider.kt | 91 +++++++ tools/codegen/src/com/android/codegen/Main.kt | 77 +----- tools/codegen/src/com/android/codegen/Printer.kt | 186 +++++++++++++ tools/codegen/src/com/android/codegen/Utils.kt | 52 ++++ 9 files changed, 695 insertions(+), 340 deletions(-) create mode 100644 tools/codegen/src/com/android/codegen/FileInfo.kt create mode 100644 tools/codegen/src/com/android/codegen/ImportsProvider.kt create mode 100644 tools/codegen/src/com/android/codegen/Printer.kt (limited to 'tools/codegen/src') 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) { +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 ParseResult.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() - 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, - private val stringBuilder: StringBuilder, - var cliArgs: Array -) : ClassInfo(source) { + classAst: ClassOrInterfaceDeclaration, + fileInfo: FileInfo +) : ClassInfo(classAst, fileInfo), Printer, 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 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(" { - * - * } - * ``` - * 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.forEachTrimmingTrailingComma(b: FieldInfo.() -> Unit) { - forEachApply { - b() - if (isLast) { - while (lastChar == ' ' || lastChar == '\n') backspace() - if (lastChar == ',') backspace() - } - } - } inline operator fun 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, + val cliArgs: Array, + val file: File) + : Printer, 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() + .flatMap { it.plusNested() } + .filterNot { it.isInterface } + + val mainClass = classes.find { it.nameAsString == file.nameWithoutExtension }!! + + // Parse stage 1 + val classBounds: List = 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 { + 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 = ast.range.get()!!.let { rng -> rng.begin.line-1..rng.end.line-1 }, + val nested: MutableList = mutableListOf(), + var nestedIn: ClassBounds? = null) { + + val nestedDataClasses: List 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): CodeChunk() {} + + /** Copyright + package + imports + main javadoc */ + class FileHeader(lines: List): Code(lines) + + /** Code to be discarded and refreshed */ + open class GeneratedCode(lines: List): Code(lines) { + lateinit var owner: DataClass + + class Placeholder: GeneratedCode(emptyList()) + } + + object ClosingBrace: Code(listOf("}")) + + data class DataClass( + val ast: ClassOrInterfaceDeclaration, + val chunks: List, + 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(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 { + return mutableListOf().apply { + add(this@plusNested) + childNodes.filterIsInstance() + .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 + 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 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) { 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): List { - 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, sourceLines: List): Array { 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> { + + 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(" { + * + * } + * ``` + * 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.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 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>() +val TypeDeclaration<*>.nestedDataClasses get() + = nestedTypes.filterIsInstance() + .filter { it.annotations.any { it.nameAsString.endsWith("DataClass") } } +val TypeDeclaration<*>.startLine get() = range.get()!!.begin.line + +inline fun List.forEachSequentialPair(action: (T, T?) -> Unit) { + forEachIndexed { index, t -> + action(t, getOrNull(index + 1)) + } +} + +fun parseJava(fn: JavaParser.(String) -> ParseResult, 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 MutableList.last + get() = last() + set(value) { + if (isEmpty()) { + add(value) + } else { + this[size - 1] = value + } + } + +inline fun buildList(init: MutableList.() -> Unit) = mutableListOf().apply(init) \ No newline at end of file -- cgit v1.2.3