blob: a3d9647618df70e0404f6a9f0a301a466e153271 [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.model
import com.android.SdkConstants
import com.android.SdkConstants.ATTR_VALUE
import com.android.SdkConstants.INT_DEF_ANNOTATION
import com.android.SdkConstants.LONG_DEF_ANNOTATION
import com.android.SdkConstants.STRING_DEF_ANNOTATION
import com.android.tools.lint.annotations.Extractor.ANDROID_INT_DEF
import com.android.tools.lint.annotations.Extractor.ANDROID_LONG_DEF
import com.android.tools.lint.annotations.Extractor.ANDROID_STRING_DEF
import com.android.tools.metalava.ANDROIDX_ANNOTATION_PREFIX
import com.android.tools.metalava.ANDROID_SUPPORT_ANNOTATION_PREFIX
import com.android.tools.metalava.JAVA_LANG_PREFIX
import com.android.tools.metalava.Options
import com.android.tools.metalava.RECENTLY_NONNULL
import com.android.tools.metalava.RECENTLY_NULLABLE
import com.android.tools.metalava.options
import java.util.function.Predicate
fun isNullableAnnotation(qualifiedName: String): Boolean {
return qualifiedName.endsWith("Nullable")
}
fun isNonNullAnnotation(qualifiedName: String): Boolean {
return qualifiedName.endsWith("NonNull") ||
qualifiedName.endsWith("NotNull") ||
qualifiedName.endsWith("Nonnull")
}
interface AnnotationItem {
val codebase: Codebase
/** Fully qualified name of the annotation */
fun qualifiedName(): String?
/** Generates source code for this annotation (using fully qualified names) */
fun toSource(): String
/** Whether this annotation is significant and should be included in signature files */
fun isSignificantInSignatures(): Boolean {
return includeInSignatures(qualifiedName() ?: return false)
}
/** Whether this annotation is significant and should be included in stub files etc */
fun isSignificantInStubs(): Boolean {
return includeInStubs(qualifiedName() ?: return false)
}
/**
* Whether this annotation has class retention. Only class retention annotations are
* inserted into the stubs, the rest are extracted into the separate external annotations file.
*/
fun hasClassRetention(): Boolean {
return hasClassRetention(qualifiedName())
}
/** Attributes of the annotation (may be empty) */
fun attributes(): List<AnnotationAttribute>
/** True if this annotation represents @Nullable or @NonNull (or some synonymous annotation) */
fun isNullnessAnnotation(): Boolean {
return isNullable() || isNonNull()
}
/** True if this annotation represents @Nullable (or some synonymous annotation) */
fun isNullable(): Boolean {
return isNullableAnnotation(qualifiedName() ?: return false)
}
/** True if this annotation represents @NonNull (or some synonymous annotation) */
fun isNonNull(): Boolean {
return isNonNullAnnotation(qualifiedName() ?: return false)
}
/** True if this annotation represents @IntDef, @LongDef or @StringDef */
fun isTypeDefAnnotation(): Boolean {
val name = qualifiedName() ?: return false
return (INT_DEF_ANNOTATION.isEquals(name) ||
STRING_DEF_ANNOTATION.isEquals(name) ||
LONG_DEF_ANNOTATION.isEquals(name) ||
ANDROID_INT_DEF == name ||
ANDROID_STRING_DEF == name ||
ANDROID_LONG_DEF == name)
}
/**
* True if this annotation represents a @ParameterName annotation (or some synonymous annotation).
* The parameter name should be the default attribute or "value".
*/
fun isParameterName(): Boolean {
return qualifiedName()?.endsWith(".ParameterName") ?: return false
}
/**
* True if this annotation represents a @DefaultValue annotation (or some synonymous annotation).
* The default value should be the default attribute or "value".
*/
fun isDefaultValue(): Boolean {
return qualifiedName()?.endsWith(".DefaultValue") ?: return false
}
/** Returns the given named attribute if specified */
fun findAttribute(name: String?): AnnotationAttribute? {
val actualName = name ?: ATTR_VALUE
return attributes().firstOrNull { it.name == actualName }
}
/** Find the class declaration for the given annotation */
fun resolve(): ClassItem? {
return codebase.findClass(qualifiedName() ?: return null)
}
companion object {
/** Whether the given annotation name is "significant", e.g. should be included in signature files */
fun includeInSignatures(qualifiedName: String?): Boolean {
qualifiedName ?: return false
if (qualifiedName.startsWith(ANDROID_SUPPORT_ANNOTATION_PREFIX) ||
qualifiedName.startsWith(ANDROIDX_ANNOTATION_PREFIX)
) {
// Don't include typedefs in the stub files.
if (qualifiedName.endsWith("IntDef") || qualifiedName.endsWith("StringDef")) {
return false
}
return true
}
return false
}
/** Whether the given annotation name is "significant", e.g. should be included in signature files */
fun includeInStubs(qualifiedName: String?): Boolean {
qualifiedName ?: return false
if (includeInSignatures(qualifiedName)) {
return true
}
// These are the significant annotations that should be included in the stubs.
// This is a hardcoded list here to minimize risk in the P release branch;
// in master the check is more general (we keep only runtime retention annotations
// that match the API filter, plus the retention one)
return when (qualifiedName) {
"android.view.ViewDebug.ExportedProperty",
"android.widget.RemoteViews.RemoteView",
"android.view.ViewDebug.CapturedViewProperty",
"java.lang.FunctionalInterface",
"java.lang.SafeVarargs",
"java.lang.annotation.Documented",
"java.lang.annotation.Inherited",
"java.lang.annotation.Repeatable",
"java.lang.annotation.Retention",
"java.lang.annotation.Target" -> true
else -> false
}
}
/** The simple name of an annotation, which is the annotation name (not qualified name) prefixed by @ */
fun simpleName(item: AnnotationItem): String {
val qualifiedName = item.qualifiedName() ?: return ""
return "@${qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1)}"
}
/**
* Maps an annotation name to the name to be used in signatures/stubs/external annotation files.
* Annotations that should not be exported are mapped to null.
*/
fun mapName(codebase: Codebase, qualifiedName: String?, filter: Predicate<Item>? = null): String? {
qualifiedName ?: return null
when (qualifiedName) {
// Resource annotations
"android.support.annotation.AnimRes",
"android.annotation.AnimRes" -> return "androidx.annotation.AnimRes"
"android.support.annotation.AnimatorRes",
"android.annotation.AnimatorRes" -> return "androidx.annotation.AnimatorRes"
"android.support.annotation.AnyRes",
"android.annotation.AnyRes" -> return "androidx.annotation.AnyRes"
"android.support.annotation.ArrayRes",
"android.annotation.ArrayRes" -> return "androidx.annotation.ArrayRes"
"android.support.annotation.AttrRes",
"android.annotation.AttrRes" -> return "androidx.annotation.AttrRes"
"android.support.annotation.BoolRes",
"android.annotation.BoolRes" -> return "androidx.annotation.BoolRes"
"android.support.annotation.ColorRes",
"android.annotation.ColorRes" -> return "androidx.annotation.ColorRes"
"android.support.annotation.DimenRes",
"android.annotation.DimenRes" -> return "androidx.annotation.DimenRes"
"android.support.annotation.DrawableRes",
"android.annotation.DrawableRes" -> return "androidx.annotation.DrawableRes"
"android.support.annotation.FontRes",
"android.annotation.FontRes" -> return "androidx.annotation.FontRes"
"android.support.annotation.FractionRes",
"android.annotation.FractionRes" -> return "androidx.annotation.FractionRes"
"android.support.annotation.IdRes",
"android.annotation.IdRes" -> return "androidx.annotation.IdRes"
"android.support.annotation.IntegerRes",
"android.annotation.IntegerRes" -> return "androidx.annotation.IntegerRes"
"android.support.annotation.InterpolatorRes",
"android.annotation.InterpolatorRes" -> return "androidx.annotation.InterpolatorRes"
"android.support.annotation.LayoutRes",
"android.annotation.LayoutRes" -> return "androidx.annotation.LayoutRes"
"android.support.annotation.MenuRes",
"android.annotation.MenuRes" -> return "androidx.annotation.MenuRes"
"android.support.annotation.PluralsRes",
"android.annotation.PluralsRes" -> return "androidx.annotation.PluralsRes"
"android.support.annotation.RawRes",
"android.annotation.RawRes" -> return "androidx.annotation.RawRes"
"android.support.annotation.StringRes",
"android.annotation.StringRes" -> return "androidx.annotation.StringRes"
"android.support.annotation.StyleRes",
"android.annotation.StyleRes" -> return "androidx.annotation.StyleRes"
"android.support.annotation.StyleableRes",
"android.annotation.StyleableRes" -> return "androidx.annotation.StyleableRes"
"android.support.annotation.TransitionRes",
"android.annotation.TransitionRes" -> return "androidx.annotation.TransitionRes"
"android.support.annotation.XmlRes",
"android.annotation.XmlRes" -> return "androidx.annotation.XmlRes"
// Threading
"android.support.annotation.AnyThread",
"android.annotation.AnyThread" -> return "androidx.annotation.AnyThread"
"android.support.annotation.BinderThread",
"android.annotation.BinderThread" -> return "androidx.annotation.BinderThread"
"android.support.annotation.MainThread",
"android.annotation.MainThread" -> return "androidx.annotation.MainThread"
"android.support.annotation.UiThread",
"android.annotation.UiThread" -> return "androidx.annotation.UiThread"
"android.support.annotation.WorkerThread",
"android.annotation.WorkerThread" -> return "androidx.annotation.WorkerThread"
// Colors
"android.support.annotation.ColorInt",
"android.annotation.ColorInt" -> return "androidx.annotation.ColorInt"
"android.support.annotation.ColorLong",
"android.annotation.ColorLong" -> return "androidx.annotation.ColorLong"
"android.support.annotation.HalfFloat",
"android.annotation.HalfFloat" -> return "androidx.annotation.HalfFloat"
// Ranges and sizes
"android.support.annotation.FloatRange",
"android.annotation.FloatRange" -> return "androidx.annotation.FloatRange"
"android.support.annotation.IntRange",
"android.annotation.IntRange" -> return "androidx.annotation.IntRange"
"android.support.annotation.Size",
"android.annotation.Size" -> return "androidx.annotation.Size"
"android.support.annotation.Px",
"android.annotation.Px" -> return "androidx.annotation.Px"
"android.support.annotation.Dimension",
"android.annotation.Dimension" -> return "androidx.annotation.Dimension"
// Null
"android.support.annotation.NonNull",
"android.annotation.NonNull" -> return "androidx.annotation.NonNull"
"android.support.annotation.Nullable",
"android.annotation.Nullable" -> return "androidx.annotation.Nullable"
"libcore.util.NonNull" -> return "androidx.annotation.NonNull"
"libcore.util.Nullable" -> return "androidx.annotation.Nullable"
"org.jetbrains.annotations.NotNull" -> return "androidx.annotation.NonNull"
"org.jetbrains.annotations.Nullable" -> return "androidx.annotation.Nullable"
// Typedefs
"android.support.annotation.IntDef",
"android.annotation.IntDef" -> return "androidx.annotation.IntDef"
"android.support.annotation.StringDef",
"android.annotation.StringDef" -> return "androidx.annotation.StringDef"
"android.support.annotation.LongDef",
"android.annotation.LongDef" -> return "androidx.annotation.LongDef"
// Misc
"android.support.annotation.CallSuper",
"android.annotation.CallSuper" -> return "androidx.annotation.CallSuper"
"android.support.annotation.CheckResult",
"android.annotation.CheckResult" -> return "androidx.annotation.CheckResult"
"android.support.annotation.RequiresPermission",
"android.annotation.RequiresPermission" -> return "androidx.annotation.RequiresPermission"
"android.annotation.RequiresPermission.Read" -> return "androidx.annotation.RequiresPermission.Read"
"android.annotation.RequiresPermission.Write" -> return "androidx.annotation.RequiresPermission.Write"
// These aren't support annotations, but could/should be:
"android.annotation.CurrentTimeMillisLong",
"android.annotation.DurationMillisLong",
"android.annotation.ElapsedRealtimeLong",
"android.annotation.UserIdInt",
"android.annotation.BytesLong",
// These aren't support annotations
"android.annotation.AppIdInt",
"android.annotation.SuppressAutoDoc",
"android.annotation.SystemApi",
"android.annotation.TestApi",
"android.annotation.CallbackExecutor",
"android.annotation.Condemned",
"android.annotation.Widget" -> {
// Remove, unless (a) public or (b) specifically included in --showAnnotations
return if (options.showAnnotations.contains(qualifiedName)) {
qualifiedName
} else if (filter != null) {
val cls = codebase.findClass(qualifiedName)
if (cls != null && filter.test(cls)) {
qualifiedName
} else {
null
}
} else {
qualifiedName
}
}
// Included for analysis, but should not be exported:
"android.annotation.BroadcastBehavior",
"android.annotation.SdkConstant",
"android.annotation.RequiresFeature",
"android.annotation.SystemService" -> return qualifiedName
// Should not be mapped to a different package name:
"android.annotation.TargetApi",
"android.annotation.SuppressLint" -> return qualifiedName
// We only change recently/newly nullable annotation if the codebase supports it
RECENTLY_NULLABLE -> return if (codebase.supportsStagedNullability) qualifiedName else "androidx.annotation.Nullable"
RECENTLY_NONNULL -> return if (codebase.supportsStagedNullability) qualifiedName else "androidx.annotation.NonNull"
else -> {
// Some new annotations added to the platform: assume they are support annotations?
return when {
// Special Kotlin annotations recognized by the compiler: map to supported package name
qualifiedName.endsWith(".ParameterName") || qualifiedName.endsWith(".DefaultValue") ->
"kotlin.annotations.jvm.internal${qualifiedName.substring(qualifiedName.lastIndexOf('.'))}"
// Other third party nullness annotations?
isNullableAnnotation(qualifiedName) -> "androidx.annotation.Nullable"
isNonNullAnnotation(qualifiedName) -> "androidx.annotation.NonNull"
// Support library annotations are all included, as is the built-in stuff like @Retention
qualifiedName.startsWith(ANDROIDX_ANNOTATION_PREFIX) -> return qualifiedName
qualifiedName.startsWith(JAVA_LANG_PREFIX) -> return qualifiedName
// Unknown Android platform annotations
qualifiedName.startsWith("android.annotation.") -> {
// Remove, unless specifically included in --showAnnotations
return if (options.showAnnotations.contains(qualifiedName)) {
qualifiedName
} else {
null
}
}
qualifiedName.startsWith(ANDROID_SUPPORT_ANNOTATION_PREFIX) -> {
return mapName(
codebase,
ANDROIDX_ANNOTATION_PREFIX + qualifiedName.substring(ANDROID_SUPPORT_ANNOTATION_PREFIX.length),
filter
)
}
else -> {
// Remove, unless (a) public or (b) specifically included in --showAnnotations
return if (options.showAnnotations.contains(qualifiedName)) {
qualifiedName
} else if (filter != null) {
val cls = codebase.findClass(qualifiedName)
if (cls != null && filter.test(cls)) {
qualifiedName
} else {
null
}
} else {
qualifiedName
}
}
}
}
}
}
/**
* Given a "full" annotation name, shortens it by removing redundant package names.
* This is intended to be used by the [Options.omitCommonPackages] flag
* to reduce clutter in signature files.
*
* For example, this method will convert `@androidx.annotation.Nullable` to just
* `@Nullable`, and `@androidx.annotation.IntRange(from=20)` to `IntRange(from=20)`.
*/
fun shortenAnnotation(source: String): String {
return when {
source.startsWith("android.annotation.", 1) -> {
"@" + source.substring("@android.annotation.".length)
}
source.startsWith(ANDROID_SUPPORT_ANNOTATION_PREFIX, 1) -> {
"@" + source.substring("@android.support.annotation.".length)
}
source.startsWith(ANDROIDX_ANNOTATION_PREFIX, 1) -> {
"@" + source.substring("@androidx.annotation.".length)
}
else -> source
}
}
/**
* Reverses the [shortenAnnotation] method. Intended for use when reading in signature files
* that contain shortened type references.
*/
fun unshortenAnnotation(source: String): String {
return when {
// These 3 annotations are in the android.annotation. package, not android.support.annotation
source.startsWith("@SystemService") ||
source.startsWith("@TargetApi") ||
source.startsWith("@SuppressLint") ->
"@android.annotation." + source.substring(1)
else -> {
"@androidx.annotation." + source.substring(1)
}
}
}
fun hasClassRetention(qualifiedName: String?): Boolean {
// For now, we treat everything except the recently nullable annotations
// as source retention; this works around the bug that we don't want to
// reference (from the .class files) annotations that aren't part of the SDK
// except for those that we include with the stubs
qualifiedName ?: return false
return when (qualifiedName) {
// Hardcoded list for now; in master, this is generalized
"android.view.ViewDebug.ExportedProperty",
"android.widget.RemoteViews.RemoteView",
"android.view.ViewDebug.CapturedViewProperty",
"androidx.annotation.RecentlyNullable",
"androidx.annotation.RecentlyNonNull" -> return true
else -> qualifiedName.startsWith("java.") ||
qualifiedName.startsWith("javax.") ||
qualifiedName.startsWith("kotlin.") ||
qualifiedName.startsWith("kotlinx.")
}
}
}
}
/** An attribute of an annotation, such as "value" */
interface AnnotationAttribute {
/** The name of the annotation */
val name: String
/** The annotation value */
val value: AnnotationAttributeValue
/**
* Return all leaf values; this flattens the complication of handling
* {@code @SuppressLint("warning")} and {@code @SuppressLint({"warning1","warning2"})
*/
fun leafValues(): List<AnnotationAttributeValue> {
val result = mutableListOf<AnnotationAttributeValue>()
AnnotationAttributeValue.addValues(value, result)
return result
}
}
/** An annotation value */
interface AnnotationAttributeValue {
/** Generates source code for this annotation value */
fun toSource(): String
/** The value of the annotation */
fun value(): Any?
/** If the annotation declaration references a field (or class etc), return the resolved class */
fun resolve(): Item?
companion object {
fun addValues(value: AnnotationAttributeValue, into: MutableList<AnnotationAttributeValue>) {
if (value is AnnotationArrayAttributeValue) {
for (v in value.values) {
addValues(v, into)
}
} else if (value is AnnotationSingleAttributeValue) {
into.add(value)
}
}
}
}
/** An annotation value (for a single item, not an array) */
interface AnnotationSingleAttributeValue : AnnotationAttributeValue {
/** The annotation value, expressed as source code */
val valueSource: String
/** The annotation value */
val value: Any?
override fun value() = value
}
/** An annotation value for an array of items */
interface AnnotationArrayAttributeValue : AnnotationAttributeValue {
/** The annotation values */
val values: List<AnnotationAttributeValue>
override fun resolve(): Item? {
error("resolve() should not be called on an array value")
}
override fun value() = values.mapNotNull { it.value() }.toTypedArray()
}
class DefaultAnnotationAttribute(
override val name: String,
override val value: DefaultAnnotationValue
) : AnnotationAttribute {
companion object {
fun create(name: String, value: String): DefaultAnnotationAttribute {
return DefaultAnnotationAttribute(name, DefaultAnnotationValue.create(value))
}
fun createList(source: String): List<AnnotationAttribute> {
val list = mutableListOf<AnnotationAttribute>() // TODO: default size = 2
var begin = 0
var index = 0
val length = source.length
while (index < length) {
val c = source[index]
if (c == '{') {
index = findEnd(source, index + 1, length, '}')
} else if (c == '"') {
index = findEnd(source, index + 1, length, '"')
} else if (c == ',') {
addAttribute(list, source, begin, index)
index++
begin = index
continue
} else if (c == ' ' && index == begin) {
begin++
}
index++
}
if (begin < length) {
addAttribute(list, source, begin, length)
}
return list
}
private fun findEnd(source: String, from: Int, to: Int, sentinel: Char): Int {
var i = from
while (i < to) {
val c = source[i]
if (c == '\\') {
i++
} else if (c == sentinel) {
return i
}
i++
}
return to
}
private fun addAttribute(list: MutableList<AnnotationAttribute>, source: String, from: Int, to: Int) {
var split = source.indexOf('=', from)
if (split >= to) {
split = -1
}
val name: String
val value: String
val valueBegin: Int
val valueEnd: Int
if (split == -1) {
valueBegin = split + 1
valueEnd = to
name = "value"
} else {
name = source.substring(from, split).trim()
valueBegin = split + 1
valueEnd = to
}
value = source.substring(valueBegin, valueEnd).trim()
list.add(DefaultAnnotationAttribute.create(name, value))
}
}
override fun toString(): String {
return "DefaultAnnotationAttribute(name='$name', value=$value)"
}
}
abstract class DefaultAnnotationValue : AnnotationAttributeValue {
companion object {
fun create(value: String): DefaultAnnotationValue {
return if (value.startsWith("{")) { // Array
DefaultAnnotationArrayAttributeValue(value)
} else {
DefaultAnnotationSingleAttributeValue(value)
}
}
}
override fun toString(): String = toSource()
}
class DefaultAnnotationSingleAttributeValue(override val valueSource: String) : DefaultAnnotationValue(),
AnnotationSingleAttributeValue {
@Suppress("IMPLICIT_CAST_TO_ANY")
override val value = when {
valueSource == SdkConstants.VALUE_TRUE -> true
valueSource == SdkConstants.VALUE_FALSE -> false
valueSource.startsWith("\"") -> valueSource.removeSurrounding("\"")
valueSource.startsWith('\'') -> valueSource.removeSurrounding("'")[0]
else -> try {
if (valueSource.contains(".")) {
valueSource.toDouble()
} else {
valueSource.toLong()
}
} catch (e: NumberFormatException) {
valueSource
}
}
override fun resolve(): Item? = null
override fun toSource() = valueSource
}
class DefaultAnnotationArrayAttributeValue(val value: String) : DefaultAnnotationValue(),
AnnotationArrayAttributeValue {
init {
assert(value.startsWith("{") && value.endsWith("}")) { value }
}
override val values = value.substring(1, value.length - 1).split(",").map {
DefaultAnnotationValue.create(it.trim())
}.toList()
override fun toSource() = value
}