• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }