/* * 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 = sourceNoPrefix.size - 2 } IntRange( range.start + start - fileInfo.generatedWarningNumPrecedingEmptyLines, range.start + endInclusive) } } /** Debug info */ override fun toString(): String { return buildString { appendln("class $name $range") nested.forEach { appendln(it) } appendln("end $name") } } } /** * Output of stage 2 of parsing a file */ sealed class CodeChunk { /** General code */ open class Code(val lines: List): 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} nested:${ast.nestedTypes.map { it.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) } } } }