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