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 
18 package android.processor.staledataclass
19 
20 import com.android.codegen.BASE_BUILDER_CLASS
21 import com.android.codegen.CANONICAL_BUILDER_CLASS
22 import com.android.codegen.CODEGEN_NAME
23 import com.android.codegen.CODEGEN_VERSION
24 import com.sun.tools.javac.code.Symbol
25 import com.sun.tools.javac.code.Type
26 import java.io.File
27 import java.io.FileNotFoundException
28 import javax.annotation.processing.AbstractProcessor
29 import javax.annotation.processing.RoundEnvironment
30 import javax.annotation.processing.SupportedAnnotationTypes
31 import javax.lang.model.SourceVersion
32 import javax.lang.model.element.AnnotationMirror
33 import javax.lang.model.element.Element
34 import javax.lang.model.element.ElementKind
35 import javax.lang.model.element.TypeElement
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.toLowerCase() })
102             append(" ")
103             append(elem.annotationMirrors.joinToString(" ", transform = { annotationToString(it) }))
104             append(" ")
105             if (elem is Symbol) {
106                 if (elem.type is Type.MethodType) {
107                     append((elem.type as Type.MethodType).returnType)
108                 } else {
109                     append(elem.type)
110                 }
111                 append(" ")
112             }
113             append(elem)
114         }
115     }
116 
117     private fun annotationToString(ann: AnnotationMirror): String {
118         return if (ann.annotationType.toString().startsWith("com.android.internal.util.DataClass")) {
119             ann.toString()
120         } else {
121             ann.toString().substringBefore("(")
122         }
123     }
124 
125     private fun processSingleFile(elementAnnotatedWithGenerated: Element) {
126 
127         val classElement = elementAnnotatedWithGenerated.enclosingElement
128 
129         val inputSignatures = computeSignaturesForClass(classElement)
130                 .plus(computeSignaturesForClass(classElement.enclosedElements.find {
131                     it.kind == ElementKind.CLASS
132                             && !isGenerated(it)
133                             && it.simpleName.toString() == BASE_BUILDER_CLASS
134                 }))
135                 .plus(computeSignaturesForClass(classElement.enclosedElements.find {
136                     it.kind == ElementKind.CLASS
137                             && !isGenerated(it)
138                             && it.simpleName.toString() == CANONICAL_BUILDER_CLASS
139                 }))
140                 .plus(classElement
141                         .annotationMirrors
142                         .find { it.annotationType.toString() == DATACLASS_ANNOTATION_NAME }
143                         .toString())
144                 .toSet()
145 
146         val annotationParams = elementAnnotatedWithGenerated
147                 .annotationMirrors
148                 .find { ann -> isGeneratedAnnotation(ann) }!!
149                 .elementValues
150                 .map { (k, v) -> k.simpleName.toString() to v.value }
151                 .toMap()
152 
153         val lastGenerated = annotationParams["time"] as Long
154         val codegenVersion = annotationParams["codegenVersion"] as String
155         val codegenMajorVersion = codegenVersion.substringBefore(".")
156         val sourceRelative = File(annotationParams["sourceFile"] as String)
157 
158         val lastGenInputSignatures = (annotationParams["inputSignatures"] as String).lines().toSet()
159 
160         if (repoRoot == null) {
161             repoRoot = generateSequence(WORKING_DIR) { it.parentFile }
162                     .find { it.resolve(sourceRelative).isFile }
163                     ?.canonicalFile
164                     ?: throw FileNotFoundException(
165                             "Failed to detect repository root: " +
166                                     "no parent of $WORKING_DIR contains $sourceRelative")
167         }
168 
169         val source = repoRoot!!.resolve(sourceRelative)
170         val clazz = classElement.toString()
171 
172         if (inputSignatures != lastGenInputSignatures) {
173             error(buildString {
174                 append(sourceRelative).append(":\n")
175                 append("  Added:\n").append((inputSignatures-lastGenInputSignatures).joinToString("\n"))
176                 append("\n")
177                 append("  Removed:\n").append((lastGenInputSignatures-inputSignatures).joinToString("\n"))
178             })
179             stale += Stale(clazz, source, lastGenerated)
180         }
181 
182         if (codegenMajorVersion != CODEGEN_VERSION.substringBefore(".")) {
183             stale += Stale(clazz, source, lastGenerated)
184         }
185     }
186 
187     private fun computeSignaturesForClass(classElement: Element?): List<String> {
188         if (classElement == null) return emptyList()
189         val type = classElement as TypeElement
190         return classElement
191                 .enclosedElements
192                 .filterNot {
193                     it.kind == ElementKind.CLASS
194                             || it.kind == ElementKind.CONSTRUCTOR
195                             || it.kind == ElementKind.INTERFACE
196                             || it.kind == ElementKind.ENUM
197                             || it.kind == ElementKind.ANNOTATION_TYPE
198                             || it.kind == ElementKind.INSTANCE_INIT
199                             || it.kind == ElementKind.STATIC_INIT
200                             || isGenerated(it)
201                 }.map {
202                     elemToString(it)
203                 } + "class ${classElement.simpleName} extends ${type.superclass} implements [${type.interfaces.joinToString(", ")}]"
204     }
205 
206     private fun isGenerated(it: Element) =
207             it.annotationMirrors.any { "Generated" in it.annotationType.toString() }
208 
209     private fun error(msg: String) {
210         processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, msg)
211     }
212 
213     private fun isGeneratedAnnotation(ann: AnnotationMirror): Boolean {
214         return generatedAnnotation!!.qualifiedName.toString() == ann.annotationType.toString()
215     }
216 
217     data class Stale(val clazz: String, val file: File?, val lastGenerated: Long) {
218         val refreshCmd = if (file != null) {
219             "$CODEGEN_NAME $file"
220         } else {
221             var gotTopLevelCalssName = false
222             val filePath = clazz.split(".")
223                     .takeWhile { word ->
224                         if (!gotTopLevelCalssName && word[0].isUpperCase()) {
225                             gotTopLevelCalssName = true
226                             return@takeWhile true
227                         }
228                         !gotTopLevelCalssName
229                     }.joinToString("/")
230             "find \$ANDROID_BUILD_TOP -path */$filePath.java -exec $CODEGEN_NAME {} \\;"
231         }
232     }
233 
234     override fun getSupportedSourceVersion(): SourceVersion {
235         return SourceVersion.latest()
236     }
237 }