1 /* <lambda>null2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 18 package com.android.codegen 19 20 import com.github.javaparser.JavaParser 21 import com.github.javaparser.ast.CompilationUnit 22 import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration 23 import com.github.javaparser.ast.body.TypeDeclaration 24 import java.io.File 25 26 /** 27 * File-level parsing & printing logic 28 * 29 * @see [main] entrypoint 30 */ 31 class FileInfo( 32 val sourceLines: List<String>, 33 val cliArgs: Array<String>, 34 val file: File) 35 : Printer<FileInfo>, ImportsProvider { 36 37 override val fileAst: CompilationUnit 38 = parseJava(JavaParser::parse, sourceLines.joinToString("\n")) 39 40 override val stringBuilder = StringBuilder() 41 override var currentIndent = INDENT_SINGLE 42 43 44 val generatedWarning = run { 45 val fileEscaped = file.absolutePath.replace( 46 System.getenv("ANDROID_BUILD_TOP"), "\$ANDROID_BUILD_TOP") 47 48 """ 49 50 51 // $GENERATED_WARNING_PREFIX v$CODEGEN_VERSION. 52 // 53 // DO NOT MODIFY! 54 // CHECKSTYLE:OFF Generated code 55 // 56 // To regenerate run: 57 // $ $THIS_SCRIPT_LOCATION$CODEGEN_NAME ${cliArgs.dropLast(1).joinToString("") { "$it " }}$fileEscaped 58 // 59 // To exclude the generated code from IntelliJ auto-formatting enable (one-time): 60 // Settings > Editor > Code Style > Formatter Control 61 //@formatter:off 62 63 """ 64 } 65 private val generatedWarningNumPrecedingEmptyLines 66 = generatedWarning.lines().takeWhile { it.isBlank() }.size 67 68 val classes = fileAst.types 69 .filterIsInstance<ClassOrInterfaceDeclaration>() 70 .flatMap { it.plusNested() } 71 .filterNot { it.isInterface } 72 73 val mainClass = classes.find { it.nameAsString == file.nameWithoutExtension }!! 74 75 // Parse stage 1 76 val classBounds: List<ClassBounds> = classes.map { ast -> 77 ClassBounds(ast, fileInfo = this) 78 }.apply { 79 forEachApply { 80 if (ast.isNestedType) { 81 val parent = find { 82 it.name == (ast.parentNode.get()!! as TypeDeclaration<*>).nameAsString 83 }!! 84 parent.nested.add(this) 85 nestedIn = parent 86 } 87 } 88 } 89 90 // Parse Stage 2 91 var codeChunks = buildList<CodeChunk> { 92 val mainClassBounds = classBounds.find { it.nestedIn == null }!! 93 add(CodeChunk.FileHeader( 94 mainClassBounds.fileInfo.sourceLines.subList(0, mainClassBounds.range.start))) 95 add(CodeChunk.DataClass.parse(mainClassBounds)) 96 } 97 98 // Output stage 99 fun main() { 100 codeChunks.forEach { print(it) } 101 } 102 103 fun print(chunk: CodeChunk) { 104 when(chunk) { 105 is CodeChunk.GeneratedCode -> { 106 // Re-parse class code, discarding generated code and nested dataclasses 107 val ast = chunk.owner.chunks 108 .filter { 109 it.javaClass == CodeChunk.Code::class.java 110 || it.javaClass == CodeChunk.ClosingBrace::class.java 111 } 112 .flatMap { (it as CodeChunk.Code).lines } 113 .joinToString("\n") 114 .let { 115 parseJava(JavaParser::parseTypeDeclaration, it) 116 as ClassOrInterfaceDeclaration 117 } 118 119 // Write new generated code 120 ClassPrinter(ast, fileInfo = this).print() 121 } 122 is CodeChunk.ClosingBrace -> { 123 // Special case - print closing brace with -1 indent 124 rmEmptyLine() 125 popIndent() 126 +"\n}" 127 } 128 // Print general code as-is 129 is CodeChunk.Code -> chunk.lines.forEach { stringBuilder.appendln(it) } 130 // Recursively render data classes 131 is CodeChunk.DataClass -> chunk.chunks.forEach { print(it) } 132 } 133 } 134 135 /** 136 * Output of stage 1 of parsing a file: 137 * Recursively nested ranges of code line numbers containing nested classes 138 */ 139 data class ClassBounds( 140 val ast: ClassOrInterfaceDeclaration, 141 val fileInfo: FileInfo, 142 val name: String = ast.nameAsString, 143 val range: ClosedRange<Int> = ast.range.get()!!.let { rng -> rng.begin.line-1..rng.end.line-1 }, 144 val nested: MutableList<ClassBounds> = mutableListOf(), 145 var nestedIn: ClassBounds? = null) { 146 147 val nestedDataClasses: List<ClassBounds> by lazy { 148 nested.filter { it.isDataclass }.sortedBy { it.range.start } 149 } 150 val isDataclass = ast.annotations.any { it.nameAsString.endsWith("DataClass") } 151 152 val baseIndentLength = fileInfo.sourceLines.find { "class $name" in it }!!.takeWhile { it == ' ' }.length 153 val baseIndent = buildString { repeat(baseIndentLength) { append(' ') } } 154 155 val sourceNoPrefix = fileInfo.sourceLines.drop(range.start) 156 val generatedCodeRange = sourceNoPrefix 157 .indexOfFirst { it.startsWith("$baseIndent$INDENT_SINGLE// $GENERATED_WARNING_PREFIX") } 158 .let { start -> 159 if (start < 0) { 160 null 161 } else { 162 var endInclusive = sourceNoPrefix.indexOfFirst { 163 it.startsWith("$baseIndent$INDENT_SINGLE$GENERATED_END") 164 } 165 if (endInclusive == -1) { 166 // Legacy generated code doesn't have end markers 167 endInclusive = sourceNoPrefix.size - 2 168 } 169 IntRange( 170 range.start + start - fileInfo.generatedWarningNumPrecedingEmptyLines, 171 range.start + endInclusive) 172 } 173 } 174 175 /** Debug info */ 176 override fun toString(): String { 177 return buildString { 178 appendln("class $name $range") 179 nested.forEach { 180 appendln(it) 181 } 182 appendln("end $name") 183 } 184 } 185 } 186 187 /** 188 * Output of stage 2 of parsing a file 189 */ 190 sealed class CodeChunk { 191 /** General code */ 192 open class Code(val lines: List<String>): CodeChunk() {} 193 194 /** Copyright + package + imports + main javadoc */ 195 class FileHeader(lines: List<String>): Code(lines) 196 197 /** Code to be discarded and refreshed */ 198 open class GeneratedCode(lines: List<String>): Code(lines) { 199 lateinit var owner: DataClass 200 201 class Placeholder: GeneratedCode(emptyList()) 202 } 203 204 object ClosingBrace: Code(listOf("}")) 205 206 data class DataClass( 207 val ast: ClassOrInterfaceDeclaration, 208 val chunks: List<CodeChunk>, 209 val generatedCode: GeneratedCode?): CodeChunk() { 210 211 companion object { 212 fun parse(classBounds: ClassBounds): DataClass { 213 val initial = Code(lines = classBounds.fileInfo.sourceLines.subList( 214 fromIndex = classBounds.range.start, 215 toIndex = findLowerBound( 216 thisClass = classBounds, 217 nextNestedClass = classBounds.nestedDataClasses.getOrNull(0)))) 218 219 val chunks = mutableListOf<CodeChunk>(initial) 220 221 classBounds.nestedDataClasses.forEachSequentialPair { 222 nestedDataClass, nextNestedDataClass -> 223 chunks += DataClass.parse(nestedDataClass) 224 chunks += Code(lines = classBounds.fileInfo.sourceLines.subList( 225 fromIndex = nestedDataClass.range.endInclusive + 1, 226 toIndex = findLowerBound( 227 thisClass = classBounds, 228 nextNestedClass = nextNestedDataClass))) 229 } 230 231 var generatedCode = classBounds.generatedCodeRange?.let { rng -> 232 GeneratedCode(classBounds.fileInfo.sourceLines.subList( 233 rng.start, rng.endInclusive+1)) 234 } 235 if (generatedCode != null) { 236 chunks += generatedCode 237 chunks += ClosingBrace 238 } else if (classBounds.isDataclass) { 239 240 // Insert placeholder for generated code to be inserted for the 1st time 241 chunks.last = (chunks.last as Code) 242 .lines 243 .dropLastWhile { it.isBlank() } 244 .run { 245 if (last().dropWhile { it.isWhitespace() }.startsWith("}")) { 246 dropLast(1) 247 } else { 248 this 249 } 250 }.let { Code(it) } 251 generatedCode = GeneratedCode.Placeholder() 252 chunks += generatedCode 253 chunks += ClosingBrace 254 } else { 255 // Outer class may be not a @DataClass but contain ones 256 // so just skip generated code for them 257 } 258 259 return DataClass(classBounds.ast, chunks, generatedCode).also { 260 generatedCode?.owner = it 261 } 262 } 263 264 private fun findLowerBound(thisClass: ClassBounds, nextNestedClass: ClassBounds?): Int { 265 return nextNestedClass?.range?.start 266 ?: thisClass.generatedCodeRange?.start 267 ?: thisClass.range.endInclusive + 1 268 } 269 } 270 } 271 272 /** Debug info */ 273 fun summary(): String = when(this) { 274 is Code -> "${javaClass.simpleName}(${lines.size} lines): ${lines.getOrNull(0)?.take(70) ?: ""}..." 275 is DataClass -> "DataClass ${ast.nameAsString} nested:${ast.nestedTypes.map { it.nameAsString }}:\n" + 276 chunks.joinToString("\n") { it.summary() } + 277 "\n//end ${ast.nameAsString}" 278 } 279 } 280 281 private fun ClassOrInterfaceDeclaration.plusNested(): List<ClassOrInterfaceDeclaration> { 282 return mutableListOf<ClassOrInterfaceDeclaration>().apply { 283 add(this@plusNested) 284 childNodes.filterIsInstance<ClassOrInterfaceDeclaration>() 285 .flatMap { it.plusNested() } 286 .let { addAll(it) } 287 } 288 } 289 }