blob: 1efb4e2a589a9672b2f38acf46ecffd5adb14113 [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.tools.metalava.manifest.Manifest
import com.android.tools.metalava.manifest.emptyManifest
import com.android.tools.metalava.model.ANDROID_ANNOTATION_PREFIX
import com.android.tools.metalava.model.ANDROID_DEPRECATED_FOR_SDK
import com.android.tools.metalava.model.ANNOTATION_ATTR_VALUE
import com.android.tools.metalava.model.AnnotationAttributeValue
import com.android.tools.metalava.model.AnnotationItem
import com.android.tools.metalava.model.BaseItemVisitor
import com.android.tools.metalava.model.BaseTypeVisitor
import com.android.tools.metalava.model.ClassItem
import com.android.tools.metalava.model.ClassTypeItem
import com.android.tools.metalava.model.Codebase
import com.android.tools.metalava.model.ConstructorItem
import com.android.tools.metalava.model.FieldItem
import com.android.tools.metalava.model.Item
import com.android.tools.metalava.model.JAVA_LANG_DEPRECATED
import com.android.tools.metalava.model.MethodItem
import com.android.tools.metalava.model.PackageItem
import com.android.tools.metalava.model.PackageList
import com.android.tools.metalava.model.ParameterItem
import com.android.tools.metalava.model.PrimitiveTypeItem
import com.android.tools.metalava.model.PropertyItem
import com.android.tools.metalava.model.TypeItem
import com.android.tools.metalava.model.TypeParameterList
import com.android.tools.metalava.model.VisibilityLevel
import com.android.tools.metalava.model.findAnnotation
import com.android.tools.metalava.model.psi.PsiClassItem
import com.android.tools.metalava.model.psi.PsiItem.Companion.isKotlin
import com.android.tools.metalava.model.source.SourceParser
import com.android.tools.metalava.model.visitors.ApiVisitor
import com.android.tools.metalava.reporter.Issues
import com.android.tools.metalava.reporter.Reporter
import java.io.File
import java.util.Collections
import java.util.IdentityHashMap
import java.util.Locale
import java.util.function.Predicate
import org.jetbrains.kotlin.asJava.classes.KtLightClassForFacade
import org.jetbrains.uast.UClass
/**
* The [ApiAnalyzer] is responsible for walking over the various classes and members and compute
* visibility etc. of the APIs
*/
class ApiAnalyzer(
private val sourceParser: SourceParser,
/** The code to analyze */
private val codebase: Codebase,
private val reporter: Reporter,
private val config: Config = Config(),
) {
data class Config(
val manifest: Manifest = emptyManifest,
/** Packages to exclude/hide */
val hidePackages: List<String> = emptyList(),
/**
* Packages that we should skip generating even if not hidden; typically only used by tests
*/
val skipEmitPackages: List<String> = emptyList(),
/**
* External annotation files that contain non-inclusion annotations which will appear in the
* generated API.
*
* These will be merged into the codebase.
*/
val mergeQualifierAnnotations: List<File> = emptyList(),
/**
* External annotation files that contain annotations which affect inclusion of items in the
* API.
*
* These will be merged into the codebase.
*/
val mergeInclusionAnnotations: List<File> = emptyList(),
/** Packages to import (if empty, include all) */
val stubImportPackages: Set<String> = emptySet(),
/** The filter for all the show annotations. */
val allShowAnnotations: AnnotationFilter = AnnotationFilter.emptyFilter(),
/** Configuration for any [ApiPredicate] instances this needs to create. */
val apiPredicateConfig: ApiPredicate.Config = ApiPredicate.Config()
)
/** All packages in the API */
private val packages: PackageList = codebase.getPackages()
fun computeApi() {
if (codebase.trustedApi()) {
// The codebase is already an API; no consistency checks to be performed
return
}
skipEmitPackages()
// Suppress kotlin file facade classes with no public api
hideEmptyKotlinFileFacadeClasses()
// Propagate visibility down into individual elements -- if a class is hidden,
// then the methods and fields are hidden etc
propagateHiddenRemovedAndDocOnly()
}
fun addConstructors(filter: Predicate<Item>) {
// Let's say we have
// class GrandParent { public GrandParent(int) {} }
// class Parent { Parent(int) {} }
// class Child { public Child(int) {} }
//
// Here Parent's constructor is not public. For normal stub generation we'd end up with
// this:
// class GrandParent { public GrandParent(int) {} }
// class Parent { }
// class Child { public Child(int) {} }
//
// This doesn't compile - Parent can't have a default constructor since there isn't
// one for it to invoke on GrandParent.
//
// we can generate a fake constructor instead, such as
// Parent() { super(0); }
//
// But it's hard to do this lazily; what if we're generating the Child class first?
// Therefore, we'll instead walk over the hierarchy and insert these constructors into the
// Item hierarchy such that code generation can find them.
//
// We also need to handle the throws list, so we can't just unconditionally insert package
// private constructors
// Keep track of all the ClassItems that have been visited so classes are only visited once.
val visited = Collections.newSetFromMap(IdentityHashMap<ClassItem, Boolean>())
// Add constructors to the classes by walking up the super hierarchy and recursively add
// constructors; we'll do it recursively to make sure that the superclass has had its
// constructors initialized first (such that we can match the parameter lists and throws
// signatures), and we use the tag fields to avoid looking at all the internal classes more
// than once.
packages
.allClasses()
.filter { filter.test(it) }
.forEach { addConstructors(it, filter, visited) }
}
/**
* Handle computing constructor hierarchy.
*
* We'll be setting several attributes: [ClassItem.stubConstructor] : The default constructor to
* invoke in this class from subclasses. **NOTE**: This constructor may not be part of the
* [ClassItem.constructors] list, e.g. for package private default constructors we've inserted
* (because there were no public constructors or constructors not using hidden parameter types.)
*
* [ConstructorItem.superConstructor] : The default constructor to invoke.
*
* @param visited contains the [ClassItem]s that have already been visited; this method adds
* [cls] to it so [cls] will not be visited again.
*/
private fun addConstructors(
cls: ClassItem,
filter: Predicate<Item>,
visited: MutableSet<ClassItem>
) {
// What happens if we have
// package foo:
// public class A { public A(int) }
// package bar
// public class B extends A { public B(int) }
// If we just try inserting package private constructors here things will NOT work:
// package foo:
// public class A { public A(int); A() {} }
// package bar
// public class B extends A { public B(int); B() }
// because A <() is not accessible from B() -- it's outside the same package.
//
// So, we'll need to model the real constructors for all the scenarios where that works.
//
// The remaining challenge is that there will be some gaps: when we don't have a default
// constructor, subclass constructors will have to have an explicit super(args) call to pick
// the parent constructor to use. And which one? It generally doesn't matter; just pick one,
// but unfortunately, the super constructor can throw exceptions, and in that case the
// subclass constructor must also throw all those exceptions (you can't surround a super
// call with try/catch.)
//
// Luckily, this does not seem to be an actual problem with any of the source code that
// metalava currently processes. If it did become a problem then the solution would be to
// pick super constructors with a compatible set of throws.
if (cls in visited) {
return
}
// Don't add constructors to interfaces, enums, annotations, etc
if (!cls.isClass()) {
return
}
// Remember that we have visited this class so that it is not visited again. This does not
// strictly need to be done before visiting the super classes as there should not be cycles
// in the class hierarchy. However, if due to some invalid input there is then doing this
// here will prevent those cycles from causing a stack overflow.
visited.add(cls)
// First handle its super class hierarchy to make sure that we've already constructed super
// classes.
val superClass = cls.filteredSuperclass(filter)
superClass?.let { addConstructors(it, filter, visited) }
val superDefaultConstructor = superClass?.stubConstructor
if (superDefaultConstructor != null) {
cls.constructors().forEach { constructor ->
constructor.superConstructor = superDefaultConstructor
}
}
// Find default constructor, if one doesn't exist
val filteredConstructors = cls.filteredConstructors(filter).toList()
cls.stubConstructor =
if (filteredConstructors.isNotEmpty()) {
// Try to pick the constructor, select first by fewest throwables,
// then fewest parameters, then based on order in listFilter.test(cls)
filteredConstructors.reduce { first, second -> pickBest(first, second) }
} else if (
cls.constructors().isNotEmpty() ||
// For text based codebase, stub constructor needs to be generated even if
// cls.constructors() is empty, so that public default constructor is not
// created.
cls.codebase.preFiltered
) {
// No accessible constructors are available so a package private constructor is
// created. Technically, the stub now has a constructor that isn't available at
// runtime, but apps creating subclasses inside the android.* package is not
// supported.
cls.createDefaultConstructor().also {
it.mutableModifiers().setVisibilityLevel(VisibilityLevel.PACKAGE_PRIVATE)
it.hidden = false
it.superConstructor = superDefaultConstructor
}
} else {
null
}
}
// TODO: Annotation test: @ParameterName, if present, must be supplied on *all* the arguments!
// Warn about @DefaultValue("null"); they probably meant @DefaultNull
// Supplying default parameter in override is not allowed!
private fun pickBest(current: ConstructorItem, next: ConstructorItem): ConstructorItem {
val currentThrowsCount = current.throwsTypes().size
val nextThrowsCount = next.throwsTypes().size
return if (currentThrowsCount < nextThrowsCount) {
current
} else if (currentThrowsCount > nextThrowsCount) {
next
} else {
val currentParameterCount = current.parameters().size
val nextParameterCount = next.parameters().size
if (currentParameterCount <= nextParameterCount) {
current
} else next
}
}
fun generateInheritedStubs(filterEmit: Predicate<Item>, filterReference: Predicate<Item>) {
// When analyzing libraries we may discover some new classes during traversal; these aren't
// part of the API but may be super classes or interfaces; these will then be added into the
// package class lists, which could trigger a concurrent modification, so create a snapshot
// of the class list and iterate over it:
val allClasses = packages.allClasses().toList()
allClasses.forEach {
if (filterEmit.test(it)) {
generateInheritedStubs(it, filterEmit, filterReference)
}
}
}
private fun generateInheritedStubs(
cls: ClassItem,
filterEmit: Predicate<Item>,
filterReference: Predicate<Item>
) {
if (!cls.isClass()) return
if (cls.superClass() == null) return
val superClasses: Sequence<ClassItem> =
generateSequence(cls.superClass()) { it.superClass() }
val hiddenSuperClasses: Sequence<ClassItem> =
superClasses.filter { !filterReference.test(it) && !it.isJavaLangObject() }
if (hiddenSuperClasses.none()) { // not missing any implementation methods
return
}
addInheritedStubsFrom(cls, hiddenSuperClasses, superClasses, filterEmit, filterReference)
addInheritedInterfacesFrom(cls, hiddenSuperClasses, filterReference)
}
private fun addInheritedInterfacesFrom(
cls: ClassItem,
hiddenSuperClasses: Sequence<ClassItem>,
filterReference: Predicate<Item>
) {
var interfaceTypes: MutableList<TypeItem>? = null
var interfaceTypeClasses: MutableList<ClassItem>? = null
for (hiddenSuperClass in hiddenSuperClasses) {
for (hiddenInterface in hiddenSuperClass.interfaceTypes()) {
val hiddenInterfaceClass = hiddenInterface.asClass()
if (filterReference.test(hiddenInterfaceClass ?: continue)) {
if (interfaceTypes == null) {
interfaceTypes = cls.interfaceTypes().toMutableList()
interfaceTypeClasses =
interfaceTypes.mapNotNull { it.asClass() }.toMutableList()
if (cls.isInterface()) {
cls.superClass()?.let { interfaceTypeClasses.add(it) }
}
cls.setInterfaceTypes(interfaceTypes)
}
if (interfaceTypeClasses!!.any { it == hiddenInterfaceClass }) {
continue
}
interfaceTypeClasses.add(hiddenInterfaceClass)
if (hiddenInterfaceClass.hasTypeVariables()) {
val mapping = cls.mapTypeVariables(hiddenSuperClass)
if (mapping.isNotEmpty()) {
val mappedType: TypeItem = hiddenInterface.convertType(mapping, cls)
interfaceTypes.add(mappedType)
continue
}
}
interfaceTypes.add(hiddenInterface)
}
}
}
}
private fun addInheritedStubsFrom(
cls: ClassItem,
hiddenSuperClasses: Sequence<ClassItem>,
superClasses: Sequence<ClassItem>,
filterEmit: Predicate<Item>,
filterReference: Predicate<Item>
) {
// Also generate stubs for any methods we would have inherited from abstract parents
// All methods from super classes that (1) aren't overridden in this class already, and
// (2) are overriding some method that is in a public interface accessible from this class.
val interfaces: Set<TypeItem> = cls.allInterfaceTypes(filterReference).toSet()
// Note that we can't just call method.superMethods() to and see whether any of their
// containing classes are among our target APIs because it's possible that the super class
// doesn't actually implement the interface, but still provides a matching signature for the
// interface. Instead, we'll look through all of our interface methods and look for
// potential overrides.
val interfaceNames = mutableMapOf<String, MutableList<MethodItem>>()
for (interfaceType in interfaces) {
val interfaceClass = interfaceType.asClass() ?: continue
for (method in interfaceClass.methods()) {
val name = method.name()
val list =
interfaceNames[name]
?: run {
val list = ArrayList<MethodItem>()
interfaceNames[name] = list
list
}
list.add(method)
}
}
// Also add in any abstract methods from public super classes
val publicSuperClasses =
superClasses.filter { filterEmit.test(it) && !it.isJavaLangObject() }
for (superClass in publicSuperClasses) {
for (method in superClass.methods()) {
if (!method.modifiers.isAbstract() || !method.modifiers.isPublicOrProtected()) {
continue
}
val name = method.name()
val list =
interfaceNames[name]
?: run {
val list = ArrayList<MethodItem>()
interfaceNames[name] = list
list
}
list.add(method)
}
}
// Also add in any concrete public methods from hidden super classes
for (superClass in hiddenSuperClasses) {
// Determine if there is a non-hidden class between the superClass and this class.
// If non-hidden classes are found, don't include the methods for this hiddenSuperClass,
// as it will already have been included in a previous super class
var includeHiddenSuperClassMethods = true
var currentClass = cls.superClass()
while (currentClass != superClass && currentClass != null) {
if (!hiddenSuperClasses.contains(currentClass)) {
includeHiddenSuperClassMethods = false
break
}
currentClass = currentClass.superClass()
}
if (!includeHiddenSuperClassMethods) {
continue
}
for (method in superClass.methods()) {
if (method.modifiers.isAbstract() || !method.modifiers.isPublic()) {
continue
}
if (method.hasHiddenType(filterReference)) {
continue
}
val name = method.name()
val list =
interfaceNames[name]
?: run {
val list = ArrayList<MethodItem>()
interfaceNames[name] = list
list
}
list.add(method)
}
}
// Find all methods that are inherited from these classes into our class
// (making sure that we don't have duplicates, e.g. a method defined by one
// inherited class and then overridden by another closer one).
// map from method name to super methods overriding our interfaces
val map = HashMap<String, MutableList<MethodItem>>()
for (superClass in hiddenSuperClasses) {
for (method in superClass.methods()) {
val modifiers = method.modifiers
if (!modifiers.isPrivate() && !modifiers.isAbstract()) {
val name = method.name()
val candidates = interfaceNames[name] ?: continue
val parameterCount = method.parameters().size
for (superMethod in candidates) {
if (parameterCount != superMethod.parameters().count()) {
continue
}
if (method.matches(superMethod)) {
val list =
map[name]
?: run {
val newList = ArrayList<MethodItem>()
map[name] = newList
newList
}
list.add(method)
break
}
}
}
}
}
// Remove any methods that are overriding any of our existing methods
for (method in cls.methods()) {
val name = method.name()
val candidates = map[name] ?: continue
val iterator = candidates.listIterator()
while (iterator.hasNext()) {
val inheritedMethod = iterator.next()
if (method.matches(inheritedMethod)) {
iterator.remove()
}
}
}
// Next remove any overrides among the remaining super methods (e.g. one method from a
// hidden parent is
// overriding another method from a more distant hidden parent).
map.values.forEach { methods ->
if (methods.size >= 2) {
for (candidate in ArrayList(methods)) {
for (superMethod in candidate.allSuperMethods()) {
methods.remove(superMethod)
}
}
}
}
val existingMethodMap = HashMap<String, MutableList<MethodItem>>()
for (method in cls.methods()) {
val name = method.name()
val list =
existingMethodMap[name]
?: run {
val newList = ArrayList<MethodItem>()
existingMethodMap[name] = newList
newList
}
list.add(method)
}
// We're now left with concrete methods in hidden parents that are implementing methods in
// public
// interfaces that are listed in this class. Create stubs for them:
map.values.flatten().forEach {
val method = cls.createMethod(it)
/* Insert comment marker: This is useful for debugging purposes but doesn't
belong in the stub
method.documentation = "// Inlined stub from hidden parent class ${it.containingClass().qualifiedName()}\n" +
method.documentation
*/
method.inheritedMethod = true
method.inheritedFrom = it.containingClass()
val name = method.name()
val candidates = existingMethodMap[name]
if (candidates != null) {
val iterator = candidates.listIterator()
while (iterator.hasNext()) {
val inheritedMethod = iterator.next()
if (method.matches(inheritedMethod)) {
// If we already have an override of this method, do not add it to the
// methods list
return@forEach
}
}
}
cls.addMethod(method)
}
}
/** Apply package filters listed in [Options.skipEmitPackages] */
private fun skipEmitPackages() {
for (pkgName in config.skipEmitPackages) {
val pkg = codebase.findPackage(pkgName) ?: continue
pkg.emit = false
}
}
/** If a file facade class has no public members, don't add it to the api */
private fun hideEmptyKotlinFileFacadeClasses() {
codebase.getPackages().allClasses().forEach { cls ->
val psi = (cls as? PsiClassItem)?.psi()
if (
psi != null &&
isKotlin(psi) &&
psi is UClass &&
psi.javaPsi is KtLightClassForFacade &&
// a facade class needs to be emitted if it has any top-level fun/prop to emit
cls.members().none { member ->
// a member needs to be emitted if
// 1) it doesn't have a hide annotation and
// 2) it is either public or has a show annotation
!member.hasHideAnnotation() &&
(member.isPublic || member.hasShowAnnotation())
}
) {
cls.emit = false
}
}
}
/**
* Merge in external qualifier annotations (i.e. ones intended to be included in the API written
* from all configured sources).
*/
fun mergeExternalQualifierAnnotations() {
val mergeQualifierAnnotations = config.mergeQualifierAnnotations
if (mergeQualifierAnnotations.isNotEmpty()) {
AnnotationsMerger(sourceParser, codebase, reporter)
.mergeQualifierAnnotations(mergeQualifierAnnotations)
}
}
/** Merge in external show/hide annotations from all configured sources */
fun mergeExternalInclusionAnnotations() {
val mergeInclusionAnnotations = config.mergeInclusionAnnotations
if (mergeInclusionAnnotations.isNotEmpty()) {
AnnotationsMerger(sourceParser, codebase, reporter)
.mergeInclusionAnnotations(mergeInclusionAnnotations)
}
}
/**
* Propagate the hidden flag down into individual elements -- if a class is hidden, then the
* methods and fields are hidden etc
*/
private fun propagateHiddenRemovedAndDocOnly() {
packages.accept(
object : BaseItemVisitor(visitConstructorsAsMethods = true, nestInnerClasses = true) {
/**
* Mark [item] as deprecated if [Item.parent] is deprecated, and it is not a
* package.
*
* This must be called from the type specific `visit*()` methods after any other
* logic as it will depend on the value of [Item.removed] set in that method.
*/
private fun markAsDeprecatedIfNonPackageParentIsDeprecated(item: Item) {
val parent = item.parent() ?: return
if (parent !is PackageItem && parent.effectivelyDeprecated) {
item.effectivelyDeprecated = true
}
}
override fun visitPackage(pkg: PackageItem) {
when {
config.hidePackages.contains(pkg.qualifiedName()) -> pkg.hidden = true
else -> {
val showability = pkg.showability
when {
showability.show() -> pkg.hidden = false
showability.hide() -> pkg.hidden = true
}
}
}
val containingPackage = pkg.containingPackage()
if (containingPackage != null) {
if (containingPackage.hidden && !containingPackage.isDefault) {
pkg.hidden = true
}
if (containingPackage.docOnly) {
pkg.docOnly = true
}
}
}
override fun visitClass(cls: ClassItem) {
val containingClass = cls.containingClass()
val showability = cls.showability
if (showability.show()) {
cls.hidden = false
// Make containing package non-hidden if it contains a show-annotation
// class. Doclava does this in PackageInfo.isHidden().
cls.containingPackage().hidden = false
if (containingClass != null) {
ensureParentVisible(cls)
}
} else if (showability.hide()) {
cls.hidden = true
} else if (containingClass != null) {
if (containingClass.hidden) {
cls.hidden = true
} else if (
containingClass.originallyHidden &&
containingClass.hasShowSingleAnnotation()
) {
// See explanation in visitMethod
cls.hidden = true
}
if (containingClass.docOnly) {
cls.docOnly = true
}
if (containingClass.removed) {
cls.removed = true
}
} else {
val containingPackage = cls.containingPackage()
if (containingPackage.hidden && !containingPackage.isDefault) {
cls.hidden = true
} else if (containingPackage.originallyHidden) {
// Package was marked hidden; it's been unhidden by some other
// classes (marked with show annotations) but this class
// should continue to default.
cls.hidden = true
}
if (containingPackage.docOnly && !containingPackage.isDefault) {
cls.docOnly = true
}
if (containingPackage.removed && !showability.show()) {
cls.removed = true
}
}
markAsDeprecatedIfNonPackageParentIsDeprecated(cls)
}
override fun visitMethod(method: MethodItem) {
val showability = method.showability
if (showability.show()) {
method.hidden = false
ensureParentVisible(method)
} else if (showability.hide()) {
method.hidden = true
} else {
val containingClass = method.containingClass()
if (containingClass.hidden) {
method.hidden = true
} else if (
containingClass.originallyHidden &&
containingClass.hasShowSingleAnnotation()
) {
// This is a member in a class that was hidden but then unhidden;
// but it was unhidden by a non-recursive (single) show annotation, so
// don't inherit the show annotation into this item.
method.hidden = true
}
if (containingClass.docOnly) {
method.docOnly = true
}
if (containingClass.removed) {
method.removed = true
}
}
markAsDeprecatedIfNonPackageParentIsDeprecated(method)
}
override fun visitParameter(parameter: ParameterItem) {
markAsDeprecatedIfNonPackageParentIsDeprecated(parameter)
}
override fun visitField(field: FieldItem) {
val showability = field.showability
if (showability.show()) {
field.hidden = false
ensureParentVisible(field)
} else if (showability.hide()) {
field.hidden = true
} else {
val containingClass = field.containingClass()
if (
containingClass.originallyHidden &&
containingClass.hasShowSingleAnnotation()
) {
// See explanation in visitMethod
field.hidden = true
}
if (containingClass.docOnly) {
field.docOnly = true
}
if (containingClass.removed) {
field.removed = true
}
}
markAsDeprecatedIfNonPackageParentIsDeprecated(field)
}
override fun visitProperty(property: PropertyItem) {
markAsDeprecatedIfNonPackageParentIsDeprecated(property)
}
private fun ensureParentVisible(item: Item) {
val parent = item.parent() ?: return
if (!parent.hidden) {
return
}
item.modifiers.findAnnotation(AnnotationItem::isShowAnnotation)?.let {
violatingAnnotation ->
reporter.report(
Issues.SHOWING_MEMBER_IN_HIDDEN_CLASS,
item,
"Attempting to unhide ${item.describe()}, but surrounding ${parent.describe()} is " +
"hidden and should also be annotated with $violatingAnnotation"
)
}
}
}
)
}
private fun checkSystemPermissions(method: MethodItem) {
if (
method.isImplicitConstructor()
) { // Don't warn on non-source elements like implicit default constructors
return
}
val annotation = method.modifiers.findAnnotation(ANDROID_REQUIRES_PERMISSION)
var hasAnnotation = false
if (annotation != null) {
hasAnnotation = true
for (attribute in annotation.attributes) {
var values: List<AnnotationAttributeValue>? = null
var any = false
when (attribute.name) {
"value",
"allOf" -> {
values = attribute.leafValues()
}
"anyOf" -> {
any = true
values = attribute.leafValues()
}
}
values ?: continue
val system = ArrayList<String>()
val nonSystem = ArrayList<String>()
val missing = ArrayList<String>()
for (value in values) {
val perm = (value.value() ?: value.toSource()).toString()
val level = config.manifest.getPermissionLevel(perm)
if (level == null) {
if (any) {
missing.add(perm)
continue
}
reporter.report(
Issues.REQUIRES_PERMISSION,
method,
"Permission '$perm' is not defined by manifest ${config.manifest}."
)
continue
}
if (
level.contains("normal") ||
level.contains("dangerous") ||
level.contains("ephemeral")
) {
nonSystem.add(perm)
} else {
system.add(perm)
}
}
if (any && missing.size == values.size) {
reporter.report(
Issues.REQUIRES_PERMISSION,
method,
"None of the permissions ${missing.joinToString()} are defined by manifest " +
"${config.manifest}."
)
}
if (system.isEmpty() && nonSystem.isEmpty()) {
hasAnnotation = false
} else if (any && nonSystem.isNotEmpty() || !any && system.isEmpty()) {
reporter.report(
Issues.REQUIRES_PERMISSION,
method,
"Method '" +
method.name() +
"' must be protected with a system permission; it currently" +
" allows non-system callers holding " +
nonSystem.toString()
)
}
}
}
if (!hasAnnotation) {
reporter.report(
Issues.REQUIRES_PERMISSION,
method,
"Method '" + method.name() + "' must be protected with a system permission."
)
}
}
fun performChecks() {
if (codebase.trustedApi()) {
// The codebase is already an API; no consistency checks to be performed
return
}
val checkSystemApi =
!reporter.isSuppressed(Issues.REQUIRES_PERMISSION) &&
config.allShowAnnotations.matches(ANDROID_SYSTEM_API) &&
!config.manifest.isEmpty()
val checkHiddenShowAnnotations =
!reporter.isSuppressed(Issues.UNHIDDEN_SYSTEM_API) &&
config.allShowAnnotations.isNotEmpty()
packages.accept(
object : ApiVisitor() {
override fun visitParameter(parameter: ParameterItem) {
checkTypeReferencesHidden(parameter, parameter.type())
}
override fun visitItem(item: Item) {
if (
item.deprecated &&
!item.documentationContainsDeprecated() &&
// Don't warn about this in Kotlin; the Kotlin deprecation annotation
// includes deprecation
// messages (unlike java.lang.Deprecated which has no attributes).
// Instead, these
// are added to the documentation by the [DocAnalyzer].
!item.isKotlin() &&
// Don't warn about this on ParameterItem as there is no way to provide
// a deprecation message in Javadoc for parameters.
item !is ParameterItem &&
// @DeprecatedForSdk will show up as an alias for @Deprecated, but it's
// correct
// and expected to *not* combine this with @deprecated in the text;
// here,
// the text comes from an annotation attribute.
item.modifiers.isAnnotatedWith(JAVA_LANG_DEPRECATED)
) {
reporter.report(
Issues.DEPRECATION_MISMATCH,
item,
"${item.toString().capitalize()}: @Deprecated annotation (present) and @deprecated doc tag (not present) do not match"
)
// TODO: Check opposite (doc tag but no annotation)
} else {
val deprecatedForSdk =
item.modifiers.findAnnotation(ANDROID_DEPRECATED_FOR_SDK)
if (deprecatedForSdk != null) {
if (item.documentation.contains("@deprecated")) {
reporter.report(
Issues.DEPRECATION_MISMATCH,
item,
"${item.toString().capitalize()}: Documentation contains `@deprecated` which implies this API is fully deprecated, not just @DeprecatedForSdk"
)
} else {
val value = deprecatedForSdk.findAttribute(ANNOTATION_ATTR_VALUE)
val message = value?.value?.value()?.toString() ?: ""
item.appendDocumentation(message, "@deprecated")
}
}
}
if (
checkHiddenShowAnnotations &&
item.hasShowAnnotation() &&
!item.originallyHidden &&
!item.hasShowSingleAnnotation()
) {
val annotationName =
item.modifiers
.annotations()
// As item.hasShowAnnotation() is true there must be at least one
// annotation that matches the following predicate.
.first(AnnotationItem::isShowAnnotation)
// All show annotations must have a non-null string otherwise they
// would not have been matched.
.qualifiedName!!
.removePrefix(ANDROID_ANNOTATION_PREFIX)
reporter.report(
Issues.UNHIDDEN_SYSTEM_API,
item,
"@$annotationName APIs must also be marked @hide: ${item.describe()}"
)
}
}
override fun visitClass(cls: ClassItem) {
// Propagate @Deprecated flags down from classes into inner classes, if
// configured.
// Done here rather than in the analyzer which propagates visibility, since we
// want to do it
// after warning
val containingClass = cls.containingClass()
if (containingClass != null && containingClass.deprecated) {
cls.deprecated = true
}
if (checkSystemApi) {
// Look for Android @SystemApi exposed outside the normal SDK; we require
// that they're protected with a system permission.
// Also flag @SystemApi apis not annotated with @hide.
// This class is a system service if it's annotated with @SystemService,
// or if it's android.content.pm.PackageManager
if (
cls.modifiers.isAnnotatedWith("android.annotation.SystemService") ||
cls.qualifiedName() == "android.content.pm.PackageManager"
) {
// Check permissions on system services
for (method in cls.filteredMethods(filterEmit)) {
checkSystemPermissions(method)
}
}
}
}
override fun visitField(field: FieldItem) {
val containingClass = field.containingClass()
if (containingClass.deprecated) {
field.deprecated = true
}
checkTypeReferencesHidden(field, field.type())
}
override fun visitProperty(property: PropertyItem) {
val containingClass = property.containingClass()
if (containingClass.deprecated) {
property.deprecated = true
}
checkTypeReferencesHidden(property, property.type())
}
override fun visitMethod(method: MethodItem) {
if (!method.isConstructor()) {
checkTypeReferencesHidden(
method,
method.returnType()
) // returnType is nullable only for constructors
}
val containingClass = method.containingClass()
if (containingClass.deprecated) {
method.deprecated = true
}
// Make sure we don't annotate findViewById & getSystemService as @Nullable.
// See for example b/68914170.
val name = method.name()
if (
(name == "findViewById" || name == "getSystemService") &&
method.parameters().size == 1 &&
method.modifiers.isNullable()
) {
reporter.report(
Issues.EXPECTED_PLATFORM_TYPE,
method,
"$method should not be annotated @Nullable; it should be left unspecified to make it a platform type"
)
val annotation = method.modifiers.findAnnotation(AnnotationItem::isNullable)
annotation?.let { method.mutableModifiers().removeAnnotation(it) }
// Have to also clear the annotation out of the return type itself, if it's
// a type use annotation
val typeAnnotation =
method.returnType().modifiers.annotations().singleOrNull {
it.isNullnessAnnotation()
}
typeAnnotation?.let { method.returnType().modifiers.removeAnnotation(it) }
}
}
/** Check that the type doesn't refer to any hidden classes. */
private fun checkTypeReferencesHidden(item: Item, type: TypeItem) {
type.accept(
object : BaseTypeVisitor() {
override fun visitClassType(classType: ClassTypeItem) {
val cls = classType.asClass() ?: return
if (!filterReference.test(cls) && !cls.isFromClassPath()) {
reporter.report(
Issues.HIDDEN_TYPE_PARAMETER,
item,
"${item.toString().capitalize()} references hidden type $classType."
)
}
}
}
)
}
}
)
}
fun handleStripping() {
// TODO: Switch to visitor iteration
val stubImportPackages = config.stubImportPackages
handleStripping(stubImportPackages)
}
private fun handleStripping(stubImportPackages: Set<String>) {
val notStrippable = HashSet<ClassItem>(5000)
val filter = ApiPredicate(config = config.apiPredicateConfig.copy(ignoreShown = true))
// If a class is public or protected, not hidden, not imported and marked as included,
// then we can't strip it
val allTopLevelClasses = codebase.getPackages().allTopLevelClasses().toList()
allTopLevelClasses
.filter { it.isApiCandidate() && it.emit && !it.hidden() }
.forEach { cantStripThis(it, filter, notStrippable, stubImportPackages, it, "self") }
// complain about anything that looks includeable but is not supposed to
// be written, e.g. hidden things
for (cl in notStrippable) {
if (!cl.isHiddenOrRemoved()) {
val publiclyConstructable =
!cl.modifiers.isSealed() && cl.constructors().any { it.isApiCandidate() }
for (m in cl.methods()) {
if (!m.isApiCandidate()) {
if (publiclyConstructable && m.modifiers.isAbstract()) {
reporter.report(
Issues.HIDDEN_ABSTRACT_METHOD,
m,
"${m.name()} cannot be hidden and abstract when " +
"${cl.simpleName()} has a visible constructor, in case a " +
"third-party attempts to subclass it."
)
}
continue
}
if (m.isHiddenOrRemoved()) {
reporter.report(
Issues.UNAVAILABLE_SYMBOL,
m,
"Reference to unavailable method " + m.name()
)
} else if (m.deprecated) {
// don't bother reporting deprecated methods
// unless they are public
reporter.report(
Issues.DEPRECATED,
m,
"Method " + cl.qualifiedName() + "." + m.name() + " is deprecated"
)
}
val returnType = m.returnType()
if (
!m.deprecated && !cl.deprecated && returnType.asClass()?.deprecated == true
) {
reporter.report(
Issues.REFERENCES_DEPRECATED,
m,
"Return type of deprecated type $returnType in ${cl.qualifiedName()}.${m.name()}(): this method should also be deprecated"
)
}
val returnHiddenClasses = findHiddenClasses(returnType, stubImportPackages)
val returnClassName = (returnType as? ClassTypeItem)?.qualifiedName
for (hiddenClass in returnHiddenClasses) {
if (hiddenClass.isFromClassPath()) continue
if (hiddenClass.qualifiedName() == returnClassName) {
// Return type is hidden
reporter.report(
Issues.UNAVAILABLE_SYMBOL,
m,
"Method ${cl.qualifiedName()}.${m.name()} returns unavailable " +
"type ${hiddenClass.simpleName()}"
)
} else {
// Return type contains a generic parameter
reporter.report(
Issues.HIDDEN_TYPE_PARAMETER,
m,
"Method ${cl.qualifiedName()}.${m.name()} returns unavailable " +
"type ${hiddenClass.simpleName()} as a type parameter"
)
}
}
for (p in m.parameters()) {
val t = p.type()
if (t !is PrimitiveTypeItem) {
if (
!m.deprecated && !cl.deprecated && t.asClass()?.deprecated == true
) {
reporter.report(
Issues.REFERENCES_DEPRECATED,
m,
"Parameter of deprecated type $t in ${cl.qualifiedName()}.${m.name()}(): this method should also be deprecated"
)
}
val parameterHiddenClasses = findHiddenClasses(t, stubImportPackages)
val parameterClassName = (t as? ClassTypeItem)?.qualifiedName
for (hiddenClass in parameterHiddenClasses) {
if (hiddenClass.isFromClassPath()) continue
if (hiddenClass.qualifiedName() == parameterClassName) {
// Parameter type is hidden
reporter.report(
Issues.UNAVAILABLE_SYMBOL,
m,
"Parameter of unavailable type $t in ${cl.qualifiedName()}.${m.name()}()"
)
} else {
// Parameter type contains a generic parameter
reporter.report(
Issues.HIDDEN_TYPE_PARAMETER,
m,
"Parameter uses type parameter of unavailable type $t in ${cl.qualifiedName()}.${m.name()}()"
)
}
}
}
}
val t = m.returnType()
if (
t !is PrimitiveTypeItem &&
!m.deprecated &&
!cl.deprecated &&
t.asClass()?.deprecated == true
) {
reporter.report(
Issues.REFERENCES_DEPRECATED,
m,
"Returning deprecated type $t from ${cl.qualifiedName()}.${m.name()}(): this method should also be deprecated"
)
}
}
if (!cl.deprecated) {
val s = cl.superClass()
if (s?.deprecated == true) {
reporter.report(
Issues.EXTENDS_DEPRECATED,
cl,
"Extending deprecated super class $s from ${cl.qualifiedName()}: this class should also be deprecated"
)
}
for (t in cl.interfaceTypes()) {
if (t.asClass()?.deprecated == true) {
reporter.report(
Issues.EXTENDS_DEPRECATED,
cl,
"Implementing interface of deprecated type $t in ${cl.qualifiedName()}: this class should also be deprecated"
)
}
}
}
} else if (cl.deprecated) {
// not hidden, but deprecated
reporter.report(Issues.DEPRECATED, cl, "Class ${cl.qualifiedName()} is deprecated")
}
}
}
private fun cantStripThis(
cl: ClassItem,
filter: Predicate<Item>,
notStrippable: MutableSet<ClassItem>,
stubImportPackages: Set<String>?,
from: Item,
usage: String
) {
if (
stubImportPackages != null &&
stubImportPackages.contains(cl.containingPackage().qualifiedName())
) {
// if the package is imported then it does not need stubbing.
return
}
if (cl.isFromClassPath()) {
return
}
if (
(cl.isHiddenOrRemoved() || cl.isPackagePrivate && !cl.isApiCandidate()) &&
!cl.isTypeParameter
) {
reporter.report(
Issues.REFERENCES_HIDDEN,
from,
"Class ${cl.qualifiedName()} is ${if (cl.isHiddenOrRemoved()) "hidden" else "not public"} but was referenced ($usage) from public ${from.describe(
false
)}"
)
}
if (!notStrippable.add(cl)) {
// slight optimization: if it already contains cl, it already contains
// all of cl's parents
return
}
// cant strip any public fields or their generics
for (field in cl.fields()) {
if (!filter.test(field)) {
continue
}
cantStripThis(
field.type(),
field,
filter,
notStrippable,
stubImportPackages,
"in field type"
)
}
// cant strip any of the type's generics
cantStripThis(cl.typeParameterList(), filter, notStrippable, stubImportPackages, cl)
// cant strip any of the annotation elements
// cantStripThis(cl.annotationElements(), notStrippable);
// take care of methods
cantStripThis(cl.methods(), filter, notStrippable, stubImportPackages)
cantStripThis(cl.constructors(), filter, notStrippable, stubImportPackages)
// blow the outer class open if this is an inner class
val containingClass = cl.containingClass()
if (containingClass != null) {
cantStripThis(
containingClass,
filter,
notStrippable,
stubImportPackages,
cl,
"as containing class"
)
}
// blow open super class and interfaces
// TODO: Consider using val superClass = cl.filteredSuperclass(filter)
val superItems = cl.allInterfaces().toMutableSet()
cl.superClass()?.let { superClass -> superItems.add(superClass) }
for (superItem in superItems) {
if (superItem.isHiddenOrRemoved()) {
// cl is a public class declared as extending a hidden superclass.
// this is not a desired practice, but it's happened, so we deal
// with it by finding the first super class which passes checkLevel for purposes of
// generating the doc & stub information, and proceeding normally.
if (!superItem.isFromClassPath()) {
reporter.report(
Issues.HIDDEN_SUPERCLASS,
cl,
"Public class " +
cl.qualifiedName() +
" stripped of unavailable superclass " +
superItem.qualifiedName()
)
}
} else {
// doclava would also mark the package private super classes as unhidden, but that's
// not
// right (this was just done for its stub handling)
// cantStripThis(superClass, filter, notStrippable, stubImportPackages, cl, "as
// super class")
if (superItem.isPrivate && !superItem.isFromClassPath()) {
reporter.report(
Issues.PRIVATE_SUPERCLASS,
cl,
"Public class " +
cl.qualifiedName() +
" extends private class " +
superItem.qualifiedName()
)
}
}
}
}
private fun cantStripThis(
methods: List<MethodItem>,
filter: Predicate<Item>,
notStrippable: MutableSet<ClassItem>,
stubImportPackages: Set<String>?
) {
// for each method, blow open the parameters, throws and return types. also blow open their
// generics
for (method in methods) {
if (!filter.test(method)) {
continue
}
cantStripThis(
method.typeParameterList(),
filter,
notStrippable,
stubImportPackages,
method
)
for (parameter in method.parameters()) {
cantStripThis(
parameter.type(),
parameter,
filter,
notStrippable,
stubImportPackages,
"in parameter type"
)
}
for (thrown in method.throwsTypes()) {
cantStripThis(
thrown,
filter,
notStrippable,
stubImportPackages,
method,
"as exception"
)
}
cantStripThis(
method.returnType(),
method,
filter,
notStrippable,
stubImportPackages,
"in return type"
)
}
}
private fun cantStripThis(
typeParameterList: TypeParameterList,
filter: Predicate<Item>,
notStrippable: MutableSet<ClassItem>,
stubImportPackages: Set<String>?,
context: Item
) {
for (typeParameter in typeParameterList.typeParameters()) {
for (bound in typeParameter.typeBounds()) {
cantStripThis(
bound,
context,
filter,
notStrippable,
stubImportPackages,
"as type parameter"
)
}
}
}
private fun cantStripThis(
type: TypeItem,
context: Item,
filter: Predicate<Item>,
notStrippable: MutableSet<ClassItem>,
stubImportPackages: Set<String>?,
usage: String,
) {
type.accept(
object : BaseTypeVisitor() {
override fun visitClassType(classType: ClassTypeItem) {
val asClass = classType.asClass() ?: return
cantStripThis(
asClass,
filter,
notStrippable,
stubImportPackages,
context,
usage
)
}
}
)
}
/**
* Find references to hidden classes.
*
* This finds hidden classes that are used by public parts of the API in order to ensure the API
* is self-consistent and does not reference classes that are not included in the stubs. Any
* such references cause an error to be reported.
*
* A reference to an imported class is not treated as an error, even though imported classes are
* hidden from the stub generation. That is because imported classes are, by definition,
* excluded from the set of classes for which stubs are required.
*
* @param ti the type information to examine for references to hidden classes.
* @param stubImportPackages the possibly null set of imported package names.
* @return all references to hidden classes referenced by the type
*/
private fun findHiddenClasses(ti: TypeItem, stubImportPackages: Set<String>?): Set<ClassItem> {
val hiddenClasses = mutableSetOf<ClassItem>()
ti.accept(
object : BaseTypeVisitor() {
override fun visitClassType(classType: ClassTypeItem) {
val asClass = classType.asClass() ?: return
if (
stubImportPackages != null &&
stubImportPackages.contains(asClass.containingPackage().qualifiedName())
) {
return
}
if (asClass.isHiddenOrRemoved()) {
hiddenClasses.add(asClass)
}
}
}
)
return hiddenClasses
}
}
private fun String.capitalize(): String {
return this.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.getDefault())
} else {
it.toString()
}
}
}
/** Returns true if this item is public or protected and so a candidate for inclusion in an API. */
private fun Item.isApiCandidate(): Boolean {
return !isHiddenOrRemoved() && (modifiers.isPublic() || modifiers.isProtected())
}
/**
* Whether documentation for the [Item] has the `@deprecated` tag -- for inherited methods, this
* also looks at any inherited documentation.
*/
private fun Item.documentationContainsDeprecated(): Boolean {
if (documentation.contains("@deprecated")) return true
if (this is MethodItem && (documentation == "" || documentation.contains("@inheritDoc"))) {
return superMethods().any { it.documentationContainsDeprecated() }
}
return false
}