1 /*
2  * 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 @file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE")
18 
19 package android.processor.staledataclass
20 
21 import com.android.codegen.BASE_BUILDER_CLASS
22 import com.android.codegen.CANONICAL_BUILDER_CLASS
23 import com.android.codegen.CODEGEN_NAME
24 import com.android.codegen.CODEGEN_VERSION
25 import java.io.File
26 import java.io.FileNotFoundException
27 import javax.annotation.processing.AbstractProcessor
28 import javax.annotation.processing.RoundEnvironment
29 import javax.annotation.processing.SupportedAnnotationTypes
30 import javax.lang.model.SourceVersion
31 import javax.lang.model.element.AnnotationMirror
32 import javax.lang.model.element.Element
33 import javax.lang.model.element.ElementKind
34 import javax.lang.model.element.TypeElement
35 import javax.lang.model.type.ExecutableType
36 import javax.tools.Diagnostic
37 
38 private const val STALE_FILE_THRESHOLD_MS = 1000
39 private val WORKING_DIR = File(".").absoluteFile
40 
41 private const val DATACLASS_ANNOTATION_NAME = "com.android.internal.util.DataClass"
42 private const val GENERATED_ANNOTATION_NAME = "com.android.internal.util.DataClass.Generated"
43 private const val GENERATED_MEMBER_ANNOTATION_NAME
44         = "com.android.internal.util.DataClass.Generated.Member"
45 
46 
47 @SupportedAnnotationTypes(DATACLASS_ANNOTATION_NAME, GENERATED_ANNOTATION_NAME)
48 class StaleDataclassProcessor: AbstractProcessor() {
49 
50     private var dataClassAnnotation: TypeElement? = null
51     private var generatedAnnotation: TypeElement? = null
52     private var repoRoot: File? = null
53 
54     private val stale = mutableListOf<Stale>()
55 
56     /**
57      * This is the main entry point in the processor, called by the compiler.
58      */
59     override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
60 
61         if (generatedAnnotation == null) {
62             generatedAnnotation = annotations.find {
63                 it.qualifiedName.toString() == GENERATED_ANNOTATION_NAME
64             }
65         }
66         if (dataClassAnnotation == null) {
67             dataClassAnnotation = annotations.find {
68                 it.qualifiedName.toString() == DATACLASS_ANNOTATION_NAME
69             } ?: return true
70         }
71 
72         val generatedAnnotatedElements = if (generatedAnnotation != null) {
73             roundEnv.getElementsAnnotatedWith(generatedAnnotation)
74         } else {
75             emptySet()
76         }
77         generatedAnnotatedElements.forEach {
78             processSingleFile(it)
79         }
80 
81 
82         val dataClassesWithoutGeneratedPart =
83                 roundEnv.getElementsAnnotatedWith(dataClassAnnotation) -
84                         generatedAnnotatedElements.map { it.enclosingElement }
85 
86         dataClassesWithoutGeneratedPart.forEach { dataClass ->
87             stale += Stale(dataClass.toString(), file = null, lastGenerated = 0L)
88         }
89 
90 
91         if (!stale.isEmpty()) {
92             error("Stale generated dataclass(es) detected. " +
93                     "Run the following command(s) to update them:" +
94                     stale.joinToString("") { "\n" + it.refreshCmd })
95         }
96         return true
97     }
98 
99     private fun elemToString(elem: Element): String {
100         return buildString {
101             append(elem.modifiers.joinToString(" ") { it.name.lowercase() })
102             append(" ")
103             append(elem.annotationMirrors.joinToString(" ", transform = { annotationToString(it) }))
104             append(" ")
105             val type = elem.asType()
106             if (type is ExecutableType) {
107                 append(type.returnType)
108             } else {
109                 append(type)
110             }
111             append(" ")
112             append(elem)
113         }
114     }
115 
116     private fun annotationToString(ann: AnnotationMirror): String {
117         return if (ann.annotationType.toString().startsWith("com.android.internal.util.DataClass")) {
118             ann.toString()
119         } else {
120             ann.toString().substringBefore("(")
121         }
122     }
123 
124     private fun processSingleFile(elementAnnotatedWithGenerated: Element) {
125 
126         val classElement = elementAnnotatedWithGenerated.enclosingElement
127 
128         val inputSignatures = computeSignaturesForClass(classElement)
129                 .plus(computeSignaturesForClass(classElement.enclosedElements.find {
130                     it.kind == ElementKind.CLASS
131                             && !isGenerated(it)
132                             && it.simpleName.toString() == BASE_BUILDER_CLASS
133                 }))
134                 .plus(computeSignaturesForClass(classElement.enclosedElements.find {
135                     it.kind == ElementKind.CLASS
136                             && !isGenerated(it)
137                             && it.simpleName.toString() == CANONICAL_BUILDER_CLASS
138                 }))
139                 .plus(classElement
140                         .annotationMirrors
141                         .find { it.annotationType.toString() == DATACLASS_ANNOTATION_NAME }
142                         .toString())
143                 .toSet()
144 
145         val annotationParams = elementAnnotatedWithGenerated
146                 .annotationMirrors
147                 .find { ann -> isGeneratedAnnotation(ann) }!!
148                 .elementValues
149                 .map { (k, v) -> k.simpleName.toString() to v.value }
150                 .toMap()
151 
152         val lastGenerated = annotationParams["time"] as Long
153         val codegenVersion = annotationParams["codegenVersion"] as String
154         val codegenMajorVersion = codegenVersion.substringBefore(".")
155         val sourceRelative = File(annotationParams["sourceFile"] as String)
156 
157         val lastGenInputSignatures = (annotationParams["inputSignatures"] as String).lines().toSet()
158 
159         if (repoRoot == null) {
160             repoRoot = generateSequence(WORKING_DIR) { it.parentFile }
161                     .find { it.resolve(sourceRelative).isFile }
162                     ?.canonicalFile
163                     ?: throw FileNotFoundException(
164                             "Failed to detect repository root: " +
165                                     "no parent of $WORKING_DIR contains $sourceRelative")
166         }
167 
168         val source = repoRoot!!.resolve(sourceRelative)
169         val clazz = classElement.toString()
170 
171         if (inputSignatures != lastGenInputSignatures) {
172             error(buildString {
173                 append(sourceRelative).append(":\n")
174                 append("  Added:\n").append((inputSignatures-lastGenInputSignatures).joinToString("\n"))
175                 append("\n")
176                 append("  Removed:\n").append((lastGenInputSignatures-inputSignatures).joinToString("\n"))
177             })
178             stale += Stale(clazz, source, lastGenerated)
179         }
180 
181         if (codegenMajorVersion != CODEGEN_VERSION.substringBefore(".")) {
182             stale += Stale(clazz, source, lastGenerated)
183         }
184     }
185 
186     private fun computeSignaturesForClass(classElement: Element?): List<String> {
187         if (classElement == null) return emptyList()
188         val type = classElement as TypeElement
189         return classElement
190                 .enclosedElements
191                 .filterNot {
192                     it.kind == ElementKind.CLASS
193                             || it.kind == ElementKind.CONSTRUCTOR
194                             || it.kind == ElementKind.INTERFACE
195                             || it.kind == ElementKind.ENUM
196                             || it.kind == ElementKind.ANNOTATION_TYPE
197                             || it.kind == ElementKind.INSTANCE_INIT
198                             || it.kind == ElementKind.STATIC_INIT
199                             || isGenerated(it)
200                 }.map {
201                     elemToString(it)
202                 } + "class ${classElement.simpleName} extends ${type.superclass} implements [${type.interfaces.joinToString(", ")}]"
203     }
204 
205     private fun isGenerated(it: Element) =
206             it.annotationMirrors.any { "Generated" in it.annotationType.toString() }
207 
208     private fun error(msg: String) {
209         processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, msg)
210     }
211 
212     private fun isGeneratedAnnotation(ann: AnnotationMirror): Boolean {
213         return generatedAnnotation!!.qualifiedName.toString() == ann.annotationType.toString()
214     }
215 
216     data class Stale(val clazz: String, val file: File?, val lastGenerated: Long) {
217         val refreshCmd = if (file != null) {
218             "$CODEGEN_NAME $file"
219         } else {
220             var gotTopLevelCalssName = false
221             val filePath = clazz.split(".")
222                     .takeWhile { word ->
223                         if (!gotTopLevelCalssName && word[0].isUpperCase()) {
224                             gotTopLevelCalssName = true
225                             return@takeWhile true
226                         }
227                         !gotTopLevelCalssName
228                     }.joinToString("/")
229             "find \$ANDROID_BUILD_TOP -path */$filePath.java -exec $CODEGEN_NAME {} \\;"
230         }
231     }
232 
233     override fun getSupportedSourceVersion(): SourceVersion {
234         return SourceVersion.latest()
235     }
236 }
237