diff options
Diffstat (limited to 'tools/codegen/src')
| -rw-r--r-- | tools/codegen/src/com/android/codegen/ClassInfo.kt | 40 | ||||
| -rw-r--r-- | tools/codegen/src/com/android/codegen/ClassPrinter.kt | 282 | ||||
| -rw-r--r-- | tools/codegen/src/com/android/codegen/FieldInfo.kt | 12 | ||||
| -rw-r--r-- | tools/codegen/src/com/android/codegen/FileInfo.kt | 289 | ||||
| -rw-r--r-- | tools/codegen/src/com/android/codegen/Generators.kt | 6 | ||||
| -rw-r--r-- | tools/codegen/src/com/android/codegen/ImportsProvider.kt | 91 | ||||
| -rwxr-xr-x | tools/codegen/src/com/android/codegen/Main.kt | 77 | ||||
| -rw-r--r-- | tools/codegen/src/com/android/codegen/Printer.kt | 186 | ||||
| -rw-r--r-- | tools/codegen/src/com/android/codegen/Utils.kt | 52 | 
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 |