blob: d53af19fafdb4d3da477148919fc24f553aea0be [file] [log] [blame]
/*
* Copyright (C) 2017 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.
*/
package com.android.tools.metalava
import com.android.SdkConstants
import com.android.tools.lint.annotations.Extractor
import com.android.tools.lint.client.api.AnnotationLookup
import com.android.tools.metalava.model.AnnotationItem
import com.android.tools.metalava.model.AnnotationTarget
import com.android.tools.metalava.model.ClassItem
import com.android.tools.metalava.model.Codebase
import com.android.tools.metalava.model.FieldItem
import com.android.tools.metalava.model.Item
import com.android.tools.metalava.model.MemberItem
import com.android.tools.metalava.model.MethodItem
import com.android.tools.metalava.model.PackageItem
import com.android.tools.metalava.model.ParameterItem
import com.android.tools.metalava.model.psi.CodePrinter
import com.android.tools.metalava.model.psi.PsiAnnotationItem
import com.android.tools.metalava.model.psi.PsiClassItem
import com.android.tools.metalava.model.psi.PsiMethodItem
import com.android.tools.metalava.model.psi.UAnnotationItem
import com.android.tools.metalava.model.visitors.ApiVisitor
import com.google.common.xml.XmlEscapers
import com.intellij.psi.JavaRecursiveElementVisitor
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiField
import com.intellij.psi.PsiModifier
import com.intellij.psi.PsiNameValuePair
import com.intellij.psi.PsiReferenceExpression
import com.intellij.psi.PsiReturnStatement
import org.jetbrains.uast.UAnnotation
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.UReferenceExpression
import org.jetbrains.uast.USimpleNameReferenceExpression
import org.jetbrains.uast.UastEmptyExpression
import org.jetbrains.uast.java.JavaUAnnotation
import org.jetbrains.uast.java.expressions.JavaUAnnotationCallExpression
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.PrintWriter
import java.io.StringWriter
import java.util.jar.JarEntry
import java.util.jar.JarOutputStream
import kotlin.text.Charsets.UTF_8
// Like the tools/base Extractor class, but limited to our own (mapped) AnnotationItems,
// and only those with source retention (and in particular right now that just means the
// typedef annotations.)
class ExtractAnnotations(
private val codebase: Codebase,
private val outputFile: File
) : ApiVisitor() {
// Used linked hash map for order such that we always emit parameters after their surrounding method etc
private val packageToAnnotationPairs = LinkedHashMap<PackageItem, MutableList<Pair<Item, AnnotationHolder>>>()
private val annotationLookup = AnnotationLookup()
private data class AnnotationHolder(
val annotationClass: ClassItem?,
val annotationItem: AnnotationItem,
val uAnnotation: UAnnotation?
)
private val fieldNamePrinter = CodePrinter(
codebase = codebase,
filterReference = filterReference,
inlineFieldValues = false,
skipUnknown = true
)
private val fieldValuePrinter = CodePrinter(
codebase = codebase,
filterReference = filterReference,
inlineFieldValues = true,
skipUnknown = true
)
private val classToAnnotationHolder = mutableMapOf<String, AnnotationHolder>()
fun extractAnnotations() {
codebase.accept(this)
// Write external annotations
FileOutputStream(outputFile).use { fileOutputStream ->
JarOutputStream(BufferedOutputStream(fileOutputStream)).use { zos ->
val sortedPackages =
packageToAnnotationPairs.keys.asSequence().sortedBy { it.qualifiedName() }.toList()
for (pkg in sortedPackages) {
// Note: Using / rather than File.separator: jar lib requires it
val name = pkg.qualifiedName().replace('.', '/') + "/annotations.xml"
val outEntry = JarEntry(name)
outEntry.time = 0
zos.putNextEntry(outEntry)
val pairs = packageToAnnotationPairs[pkg] ?: continue
// Ensure stable output
if (pairs.size > 1) {
pairs.sortBy { it.first.getExternalAnnotationSignature() }
}
StringPrintWriter.create().use { writer ->
writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>")
var open = false
var prev: Item? = null
for ((item, annotation) in pairs) {
if (item != prev) {
if (open) {
writer.print(" </item>")
writer.println()
}
writer.print(" <item name=\"")
writer.print(item.getExternalAnnotationSignature())
writer.println("\">")
open = true
}
prev = item
writeAnnotation(writer, item, annotation)
}
if (open) {
writer.print(" </item>")
writer.println()
}
writer.println("</root>\n")
writer.close()
val bytes = writer.contents.toByteArray(UTF_8)
zos.write(bytes)
zos.closeEntry()
}
}
}
}
}
private fun addItem(item: Item, annotation: AnnotationHolder) {
val pkg = when (item) {
is MemberItem -> item.containingClass().containingPackage()
is ParameterItem -> item.containingMethod().containingClass().containingPackage()
else -> return
}
val list = packageToAnnotationPairs[pkg] ?: run {
val new =
mutableListOf<Pair<Item, AnnotationHolder>>()
packageToAnnotationPairs[pkg] = new
new
}
list.add(Pair(item, annotation))
}
override fun visitField(field: FieldItem) {
checkItem(field)
}
override fun visitMethod(method: MethodItem) {
checkItem(method)
}
override fun visitParameter(parameter: ParameterItem) {
checkItem(parameter)
}
/** For a given item, extract the relevant annotations for that item */
private fun checkItem(item: Item) {
for (annotation in item.modifiers.annotations()) {
val qualifiedName = annotation.qualifiedName() ?: continue
if (qualifiedName.startsWith(JAVA_LANG_PREFIX) ||
qualifiedName.startsWith(ANDROIDX_ANNOTATION_PREFIX) ||
qualifiedName.startsWith(ANDROID_ANNOTATION_PREFIX) ||
qualifiedName.startsWith(ANDROID_SUPPORT_ANNOTATION_PREFIX)
) {
if (annotation.isTypeDefAnnotation()) {
// Imported typedef
addItem(item, AnnotationHolder(null, annotation, null))
} else if (annotation.targets().contains(AnnotationTarget.EXTERNAL_ANNOTATIONS_FILE) &&
!options.includeSourceRetentionAnnotations
) {
addItem(item, AnnotationHolder(null, annotation, null))
}
continue
}
val typeDefClass = annotation.resolve() ?: continue
val className = typeDefClass.qualifiedName()
if (typeDefClass.isAnnotationType()) {
val cached = classToAnnotationHolder[className]
if (cached != null) {
addItem(item, cached)
continue
}
val typeDefAnnotation = typeDefClass.modifiers.annotations().firstOrNull {
it.isTypeDefAnnotation()
}
if (typeDefAnnotation != null) {
// Make sure it has the right retention
if (!hasSourceRetention(typeDefClass)) {
reporter.report(
Issues.ANNOTATION_EXTRACTION, typeDefClass,
"This typedef annotation class should have @Retention(RetentionPolicy.SOURCE)"
)
}
if (filterEmit.test(typeDefClass)) {
reporter.report(
Issues.ANNOTATION_EXTRACTION, typeDefClass,
"This typedef annotation class should be marked @hide or should not be marked public"
)
}
val result =
if (typeDefAnnotation is PsiAnnotationItem && typeDefClass is PsiClassItem) {
AnnotationHolder(
typeDefClass, typeDefAnnotation,
annotationLookup.findRealAnnotation(
typeDefAnnotation.psiAnnotation,
typeDefClass.psiClass,
null
)
)
} else if (typeDefAnnotation is UAnnotationItem && typeDefClass is PsiClassItem) {
AnnotationHolder(
typeDefClass, typeDefAnnotation, typeDefAnnotation.uAnnotation
)
} else {
continue
}
classToAnnotationHolder[className] = result
addItem(item, result)
if (item is PsiMethodItem && result.uAnnotation != null &&
!reporter.isSuppressed(Issues.RETURNING_UNEXPECTED_CONSTANT)
) {
verifyReturnedConstants(item, result.uAnnotation, result, className)
}
continue
}
}
}
}
/**
* Given a method whose return value is annotated with a typedef, runs checks on the typedef
* and flags any returned constants not in the list.
*/
private fun verifyReturnedConstants(
item: PsiMethodItem,
uAnnotation: UAnnotation,
result: AnnotationHolder,
className: String
) {
val method = item.psiMethod
if (method.body != null) {
method.body?.accept(object : JavaRecursiveElementVisitor() {
private var constants: List<String>? = null
override fun visitReturnStatement(statement: PsiReturnStatement) {
val value = statement.returnValue
if (value is PsiReferenceExpression) {
val resolved = value.resolve() as? PsiField ?: return
val modifiers = resolved.modifierList ?: return
if (modifiers.hasModifierProperty(PsiModifier.STATIC) &&
modifiers.hasModifierProperty(PsiModifier.FINAL)
) {
if (resolved.type.arrayDimensions > 0) {
return
}
val name = resolved.name
// Make sure this is one of the allowed annotations
val names = constants ?: run {
constants = computeValidConstantNames(uAnnotation)
constants!!
}
if (names.isNotEmpty() && !names.contains(name)) {
val expected = names.joinToString { it }
reporter.report(
Issues.RETURNING_UNEXPECTED_CONSTANT, value as PsiElement,
"Returning unexpected constant $name; is @${result.annotationClass?.simpleName()
?: className} missing this constant? Expected one of $expected"
)
}
}
}
}
})
}
}
private fun computeValidConstantNames(annotation: UAnnotation): List<String> {
val constants = annotation.findAttributeValue(SdkConstants.ATTR_VALUE) ?: return emptyList()
if (constants is UCallExpression) {
return constants.valueArguments.mapNotNull { (it as? USimpleNameReferenceExpression)?.identifier }.toList()
}
return emptyList()
}
private fun hasSourceRetention(annotationClass: ClassItem): Boolean {
if (annotationClass is PsiClassItem) {
return hasSourceRetention(annotationClass.psiClass)
}
return false
}
private fun hasSourceRetention(cls: PsiClass): Boolean {
val modifierList = cls.modifierList
if (modifierList != null) {
for (psiAnnotation in modifierList.annotations) {
val annotation = JavaUAnnotation.wrap(psiAnnotation)
if (hasSourceRetention(annotation)) {
return true
}
}
}
return false
}
private fun hasSourceRetention(annotation: UAnnotation): Boolean {
val qualifiedName = annotation.qualifiedName
if ("java.lang.annotation.Retention" == qualifiedName || "kotlin.annotation.Retention" == qualifiedName) {
val attributes = annotation.attributeValues
if (attributes.size != 1) {
reporter.report(
Issues.ANNOTATION_EXTRACTION, annotation.sourcePsi,
"Expected exactly one parameter passed to @Retention"
)
return false
}
val value = attributes[0].expression
if (value is UReferenceExpression) {
try {
val element = value.resolve()
if (element is PsiField) {
val field = element as PsiField?
if ("SOURCE" == field!!.name) {
return true
}
}
} catch (t: Throwable) {
val s = value.asSourceString()
return s.contains("SOURCE")
}
}
}
return false
}
/**
* A writer which stores all its contents into a string and has the ability to mark a certain
* freeze point and then reset back to it
*/
private class StringPrintWriter constructor(private val stringWriter: StringWriter) :
PrintWriter(stringWriter) {
private var mark: Int = 0
val contents: String get() = stringWriter.toString()
fun mark() {
flush()
mark = stringWriter.buffer.length
}
fun reset() {
stringWriter.buffer.setLength(mark)
}
override fun toString(): String {
return contents
}
companion object {
fun create(): StringPrintWriter {
return StringPrintWriter(StringWriter(1000))
}
}
}
private fun escapeXml(unescaped: String): String {
return XmlEscapers.xmlAttributeEscaper().escape(unescaped)
}
private fun Item.getExternalAnnotationSignature(): String? {
when (this) {
is PackageItem -> {
return escapeXml(qualifiedName())
}
is ClassItem -> {
return escapeXml(qualifiedName())
}
is MethodItem -> {
val sb = StringBuilder(100)
sb.append(escapeXml(containingClass().qualifiedName()))
sb.append(' ')
if (isConstructor()) {
sb.append(escapeXml(containingClass().simpleName()))
} else if (returnType() != null) {
sb.append(escapeXml(returnType()!!.toTypeString()))
sb.append(' ')
sb.append(escapeXml(name()))
}
sb.append('(')
// The signature must match *exactly* the formatting used by IDEA,
// since it looks up external annotations in a map by this key.
// Therefore, it is vital that the parameter list uses exactly one
// space after each comma between parameters, and *no* spaces between
// generics variables, e.g. foo(Map<A,B>, int)
var i = 0
val parameterList = parameters()
val n = parameterList.size
while (i < n) {
if (i > 0) {
sb.append(',').append(' ')
}
val type = parameterList[i].type().toTypeString()
.replace(" ", "")
.replace("?extends", "? extends ")
.replace("?super", "? super ")
sb.append(escapeXml(type))
i++
}
sb.append(')')
return sb.toString()
}
is FieldItem -> {
return escapeXml(containingClass().qualifiedName()) + " " + name()
}
is ParameterItem -> {
return containingMethod().getExternalAnnotationSignature() + " " + this.parameterIndex
}
}
return null
}
private fun writeAnnotation(
writer: StringPrintWriter,
item: Item,
annotationHolder: AnnotationHolder
) {
val annotationItem = annotationHolder.annotationItem
val uAnnotation = annotationHolder.uAnnotation
?: when (annotationItem) {
is UAnnotationItem -> annotationItem.uAnnotation
is PsiAnnotationItem ->
// Imported annotation
JavaUAnnotation.wrap(annotationItem.psiAnnotation)
else -> return
}
val qualifiedName = annotationItem.qualifiedName()
writer.mark()
writer.print(" <annotation name=\"")
writer.print(qualifiedName)
var attributes = uAnnotation.attributeValues
if (attributes.isEmpty()) {
writer.print("\"/>")
writer.println()
return
}
writer.print("\">")
writer.println()
// noinspection PointlessBooleanExpression,ConstantConditions
if (sortAnnotations) {
// Ensure that the value attribute is written first
attributes = attributes.sortedWith(
compareBy(
{ (it.name ?: SdkConstants.ATTR_VALUE) != SdkConstants.ATTR_VALUE },
{ it.name }
)
)
}
if (attributes.size == 1 && Extractor.REQUIRES_PERMISSION.isPrefix(qualifiedName, true)) {
val expression = attributes[0].expression
if (expression is UAnnotation) {
// The external annotations format does not allow for nested/complex annotations.
// However, these special annotations (@RequiresPermission.Read,
// @RequiresPermission.Write, etc) are known to only be simple containers with a
// single permission child, so instead we "inline" the content:
// @Read(@RequiresPermission(allOf={P1,P2},conditional=true)
// =>
// @RequiresPermission.Read(allOf({P1,P2},conditional=true)
// That's setting attributes that don't actually exist on the container permission,
// but we'll counteract that on the read-annotations side.
val annotation = expression as UAnnotation
attributes = annotation.attributeValues
} else if (expression is JavaUAnnotationCallExpression) {
val annotation = expression.uAnnotation
attributes = annotation.attributeValues
} else if (expression is UastEmptyExpression && attributes[0].sourcePsi is PsiNameValuePair) {
val memberValue = (attributes[0].sourcePsi as PsiNameValuePair).value
if (memberValue is PsiAnnotation) {
val annotation = JavaUAnnotation.wrap(memberValue)
attributes = annotation.attributeValues
}
}
}
val inlineConstants = isInlinedConstant(annotationItem)
var empty = true
for (pair in attributes) {
val expression = pair.expression
val value = attributeString(expression, inlineConstants) ?: continue
empty = false
var name = pair.name
if (name == null) {
name = SdkConstants.ATTR_VALUE // default name
}
// Platform typedef annotations now declare a prefix attribute for
// documentation generation purposes; this should not be part of the
// extracted metadata.
if (("prefix" == name || "suffix" == name) && annotationItem.isTypeDefAnnotation()) {
reporter.report(
Issues.SUPERFLUOUS_PREFIX, item,
"Superfluous $name attribute on typedef"
)
continue
}
writer.print(" <val name=\"")
writer.print(name)
writer.print("\" val=\"")
writer.print(escapeXml(value))
writer.println("\" />")
}
if (empty && attributes.isNotEmpty()) {
// All items were filtered out: don't write the annotation at all
writer.reset()
return
}
writer.println(" </annotation>")
}
private fun attributeString(
value: UExpression?,
inlineConstants: Boolean
): String? {
val printer =
if (inlineConstants) {
fieldValuePrinter
} else {
fieldNamePrinter
}
return printer.toSourceString(value)
}
private fun isInlinedConstant(annotationItem: AnnotationItem): Boolean {
return annotationItem.isTypeDefAnnotation()
}
/** Whether to sort annotation attributes (otherwise their declaration order is used) */
private val sortAnnotations: Boolean = true
}