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