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 }