blob: 1cef5b0c8dfbe395efaec8a8650911a3da6b42ac [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE")
package android.processor.staledataclass
import com.android.codegen.BASE_BUILDER_CLASS
import com.android.codegen.CANONICAL_BUILDER_CLASS
import com.android.codegen.CODEGEN_NAME
import com.android.codegen.CODEGEN_VERSION
import java.io.File
import java.io.FileNotFoundException
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.RoundEnvironment
import javax.annotation.processing.SupportedAnnotationTypes
import javax.lang.model.SourceVersion
import javax.lang.model.element.AnnotationMirror
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.TypeElement
import javax.lang.model.type.ExecutableType
import javax.tools.Diagnostic
private const val STALE_FILE_THRESHOLD_MS = 1000
private val WORKING_DIR = File(".").absoluteFile
private const val DATACLASS_ANNOTATION_NAME = "com.android.internal.util.DataClass"
private const val GENERATED_ANNOTATION_NAME = "com.android.internal.util.DataClass.Generated"
private const val GENERATED_MEMBER_ANNOTATION_NAME
= "com.android.internal.util.DataClass.Generated.Member"
@SupportedAnnotationTypes(DATACLASS_ANNOTATION_NAME, GENERATED_ANNOTATION_NAME)
class StaleDataclassProcessor: AbstractProcessor() {
private var dataClassAnnotation: TypeElement? = null
private var generatedAnnotation: TypeElement? = null
private var repoRoot: File? = null
private val stale = mutableListOf<Stale>()
/**
* This is the main entry point in the processor, called by the compiler.
*/
override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
if (generatedAnnotation == null) {
generatedAnnotation = annotations.find {
it.qualifiedName.toString() == GENERATED_ANNOTATION_NAME
}
}
if (dataClassAnnotation == null) {
dataClassAnnotation = annotations.find {
it.qualifiedName.toString() == DATACLASS_ANNOTATION_NAME
} ?: return true
}
val generatedAnnotatedElements = if (generatedAnnotation != null) {
roundEnv.getElementsAnnotatedWith(generatedAnnotation)
} else {
emptySet()
}
generatedAnnotatedElements.forEach {
processSingleFile(it)
}
val dataClassesWithoutGeneratedPart =
roundEnv.getElementsAnnotatedWith(dataClassAnnotation) -
generatedAnnotatedElements.map { it.enclosingElement }
dataClassesWithoutGeneratedPart.forEach { dataClass ->
stale += Stale(dataClass.toString(), file = null, lastGenerated = 0L)
}
if (!stale.isEmpty()) {
error("Stale generated dataclass(es) detected. " +
"Run the following command(s) to update them:" +
stale.joinToString("") { "\n" + it.refreshCmd })
}
return true
}
private fun elemToString(elem: Element): String {
return buildString {
append(elem.modifiers.joinToString(" ") { it.name.lowercase() })
append(" ")
append(elem.annotationMirrors.joinToString(" ", transform = { annotationToString(it) }))
append(" ")
val type = elem.asType()
if (type is ExecutableType) {
append(type.returnType)
} else {
append(type)
}
append(" ")
append(elem)
}
}
private fun annotationToString(ann: AnnotationMirror): String {
return if (ann.annotationType.toString().startsWith("com.android.internal.util.DataClass")) {
ann.toString()
} else {
ann.toString().substringBefore("(")
}
}
private fun processSingleFile(elementAnnotatedWithGenerated: Element) {
val classElement = elementAnnotatedWithGenerated.enclosingElement
val inputSignatures = computeSignaturesForClass(classElement)
.plus(computeSignaturesForClass(classElement.enclosedElements.find {
it.kind == ElementKind.CLASS
&& !isGenerated(it)
&& it.simpleName.toString() == BASE_BUILDER_CLASS
}))
.plus(computeSignaturesForClass(classElement.enclosedElements.find {
it.kind == ElementKind.CLASS
&& !isGenerated(it)
&& it.simpleName.toString() == CANONICAL_BUILDER_CLASS
}))
.plus(classElement
.annotationMirrors
.find { it.annotationType.toString() == DATACLASS_ANNOTATION_NAME }
.toString())
.toSet()
val annotationParams = elementAnnotatedWithGenerated
.annotationMirrors
.find { ann -> isGeneratedAnnotation(ann) }!!
.elementValues
.map { (k, v) -> k.simpleName.toString() to v.value }
.toMap()
val lastGenerated = annotationParams["time"] as Long
val codegenVersion = annotationParams["codegenVersion"] as String
val codegenMajorVersion = codegenVersion.substringBefore(".")
val sourceRelative = File(annotationParams["sourceFile"] as String)
val lastGenInputSignatures = (annotationParams["inputSignatures"] as String).lines().toSet()
if (repoRoot == null) {
repoRoot = generateSequence(WORKING_DIR) { it.parentFile }
.find { it.resolve(sourceRelative).isFile }
?.canonicalFile
?: throw FileNotFoundException(
"Failed to detect repository root: " +
"no parent of $WORKING_DIR contains $sourceRelative")
}
val source = repoRoot!!.resolve(sourceRelative)
val clazz = classElement.toString()
if (inputSignatures != lastGenInputSignatures) {
error(buildString {
append(sourceRelative).append(":\n")
append(" Added:\n").append((inputSignatures-lastGenInputSignatures).joinToString("\n"))
append("\n")
append(" Removed:\n").append((lastGenInputSignatures-inputSignatures).joinToString("\n"))
})
stale += Stale(clazz, source, lastGenerated)
}
if (codegenMajorVersion != CODEGEN_VERSION.substringBefore(".")) {
stale += Stale(clazz, source, lastGenerated)
}
}
private fun computeSignaturesForClass(classElement: Element?): List<String> {
if (classElement == null) return emptyList()
val type = classElement as TypeElement
return classElement
.enclosedElements
.filterNot {
it.kind == ElementKind.CLASS
|| it.kind == ElementKind.CONSTRUCTOR
|| it.kind == ElementKind.INTERFACE
|| it.kind == ElementKind.ENUM
|| it.kind == ElementKind.ANNOTATION_TYPE
|| it.kind == ElementKind.INSTANCE_INIT
|| it.kind == ElementKind.STATIC_INIT
|| isGenerated(it)
}.map {
elemToString(it)
} + "class ${classElement.simpleName} extends ${type.superclass} implements [${type.interfaces.joinToString(", ")}]"
}
private fun isGenerated(it: Element) =
it.annotationMirrors.any { "Generated" in it.annotationType.toString() }
private fun error(msg: String) {
processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, msg)
}
private fun isGeneratedAnnotation(ann: AnnotationMirror): Boolean {
return generatedAnnotation!!.qualifiedName.toString() == ann.annotationType.toString()
}
data class Stale(val clazz: String, val file: File?, val lastGenerated: Long) {
val refreshCmd = if (file != null) {
"$CODEGEN_NAME $file"
} else {
var gotTopLevelCalssName = false
val filePath = clazz.split(".")
.takeWhile { word ->
if (!gotTopLevelCalssName && word[0].isUpperCase()) {
gotTopLevelCalssName = true
return@takeWhile true
}
!gotTopLevelCalssName
}.joinToString("/")
"find \$ANDROID_BUILD_TOP -path */$filePath.java -exec $CODEGEN_NAME {} \\;"
}
}
override fun getSupportedSourceVersion(): SourceVersion {
return SourceVersion.latest()
}
}