blob: 122ebc659a6179666a942c73c81cd1a32497b3a0 [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.doclava1.ApiPredicate
import com.android.tools.metalava.doclava1.Errors
import com.android.tools.metalava.model.AnnotationAttributeValue
import com.android.tools.metalava.model.ClassItem
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.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.TypeItem
import com.android.tools.metalava.model.VisibilityLevel
import com.android.tools.metalava.model.psi.EXPAND_DOCUMENTATION
import com.android.tools.metalava.model.visitors.ApiVisitor
import com.android.tools.metalava.model.visitors.ItemVisitor
import java.util.ArrayList
import java.util.HashMap
import java.util.HashSet
import java.util.function.Predicate
/**
* The [ApiAnalyzer] is responsible for walking over the various
* classes and members and compute visibility etc of the APIs
*/
class ApiAnalyzer(
/** The code to analyze */
private val codebase: Codebase
) {
/** 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
}
// Apply options for packages that should be hidden
hidePackages()
skipEmitPackages()
// Propagate visibility down into individual elements -- if a class is hidden,
// then the methods and fields are hidden etc
propagateHiddenRemovedAndDocOnly(false)
}
fun addConstructors(filter: Predicate<Item>) {
// Let's say I 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 I'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.
//
// I can generate a fake constructor instead, such as
// Parent() { super(0); }
//
// But it's hard to do this lazily; what if I'm 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
//
// To do this right I really need to process super constructors before the classes
// depending on them.
// Mark all classes that are the super class of some other class:
val allClasses = packages.allClasses().filter { filter.test(it) }
codebase.clearTags()
allClasses.forEach { cls ->
cls.superClass()?.tag = true
}
val leafClasses = allClasses.filter { !it.tag }.toList()
// Now walk through all the leaf classes, and walk 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.
codebase.clearTags()
leafClasses
// Filter classes by filter here to not waste time in hidden packages
.filter { filter.test(it) }
.forEach { addConstructors(it, filter) }
}
/**
* 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.)
*
* If we can find a public constructor we'll put that here instead.
*
* [ConstructorItem.superConstructor] The default constructor to invoke. If set,
* use this rather than the [ClassItem.stubConstructor].
*
* [Item.tag] : mark for avoiding repeated iteration of internal item nodes
*
*
*/
private fun addConstructors(cls: ClassItem, filter: Predicate<Item>) {
// What happens if we have
// package foo:
// public class A { public A(int) }
// package bar
// public class B extends A { public B(int) }
// If I 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, I'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 I 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 constructors (you can't surround a super call
// with try/catch.) Luckily, the source code already needs to do this to
// compile, so we can just use the same constructor as the super call.
// But there are two cases we have to deal with:
// (1) the constructor doesn't call a super constructor; it calls another
// constructor on this class.
// (2) the super constructor it *does* call isn't available.
//
// For (1), this means that our stub code generator should be prepared to
// handle both super- and this- dispatches; we'll handle this by pointing
// it to the constructor to use, and it checks to see if the containing class
// for the constructor is the same to decide whether to emit "this" or "super".
if (cls.tag || !cls.isClass()) { // Don't add constructors to interfaces, enums, annotations, etc
return
}
// 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) }
cls.tag = true
if (superClass != null) {
val superDefaultConstructor = superClass.stubConstructor
if (superDefaultConstructor != null) {
val constructors = cls.constructors()
for (constructor in constructors) {
val superConstructor = constructor.superConstructor
if (superConstructor == null ||
(superConstructor.containingClass() != superClass &&
superConstructor.containingClass() != cls)
) {
constructor.superConstructor = superDefaultConstructor
}
}
}
}
// Find default constructor, if one doesn't exist
val allConstructors = cls.constructors()
if (allConstructors.isNotEmpty()) {
// Try and use a publicly accessible constructor first.
val constructors = cls.filteredConstructors(filter).toList()
if (!constructors.isEmpty()) {
// Try to pick the constructor, select first by fewest throwables, then fewest parameters,
// then based on order in listFilter.test(cls)
cls.stubConstructor = constructors.reduce { first, second -> pickBest(first, second) }
return
}
// No accessible constructors are available so one will have to be created, either a private constructor to
// prevent instances of the class from being created, or a package private constructor for use by subclasses
// in the package to use. Subclasses outside the package would need a protected or public constructor which
// would already be part of the API so should have dropped out above.
//
// The visibility levels on the constructors from the source can give a clue as to what is required. e.g.
// if all constructors are private then it is ok for the generated constructor to be private, otherwise it
// should be package private.
val allPrivate = allConstructors.asSequence()
.map { it.isPrivate }
.reduce { v1, v2 -> v1 and v2 }
val visibilityLevel = if (allPrivate) VisibilityLevel.PRIVATE else VisibilityLevel.PACKAGE_PRIVATE
// No constructors, yet somebody extends this (or private constructor): we have to invent one, such that
// subclasses can dispatch to it in the stub files etc
cls.stubConstructor = cls.createDefaultConstructor().also {
it.mutableModifiers().setVisibilityLevel(visibilityLevel)
it.hidden = false
it.superConstructor = superClass?.stubConstructor
}
}
}
// 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.asSequence().map { it.asClass() }.filterNotNull().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).asSequence().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) {
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)
}
}
}
}
// 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()
// The documentation may use relative references to classes in import statements
// in the original class, so expand the documentation to be fully qualified.
@Suppress("ConstantConditionIf")
if (!EXPAND_DOCUMENTATION) {
method.documentation = it.fullyQualifiedDocumentation()
}
cls.addMethod(method)
}
}
/** Hide packages explicitly listed in [Options.hidePackages] */
private fun hidePackages() {
for (pkgName in options.hidePackages) {
val pkg = codebase.findPackage(pkgName) ?: continue
pkg.hidden = true
pkg.included = false // because included has already been initialized
}
}
/** Apply emit filters listed in [Options.skipEmitPackages] */
private fun skipEmitPackages() {
for (pkgName in options.skipEmitPackages) {
val pkg = codebase.findPackage(pkgName) ?: continue
pkg.emit = false
}
}
/**
* Merge in external qualifier annotations (i.e. ones intended to be included in the API written
* from all configured sources.
*/
fun mergeExternalQualifierAnnotations() {
if (!options.mergeQualifierAnnotations.isEmpty()) {
AnnotationsMerger(codebase).mergeQualifierAnnotations(options.mergeQualifierAnnotations)
}
}
/** Merge in external show/hide annotations from all configured sources */
fun mergeExternalInclusionAnnotations() {
if (!options.mergeInclusionAnnotations.isEmpty()) {
AnnotationsMerger(codebase).mergeInclusionAnnotations(options.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(includingFields: Boolean) {
packages.accept(object : ItemVisitor(visitConstructorsAsMethods = true, nestInnerClasses = true) {
override fun visitPackage(pkg: PackageItem) {
when {
options.hidePackages.contains(pkg.qualifiedName()) -> pkg.hidden = true
pkg.modifiers.hasShowAnnotation() -> pkg.hidden = false
pkg.modifiers.hasHideAnnotations() -> 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()
if (cls.modifiers.hasShowAnnotation()) {
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 (cls.containingClass() != null) {
ensureParentVisible(cls)
}
} else if (cls.modifiers.hasHideAnnotations()) {
cls.hidden = true
} else if (containingClass != null) {
if (containingClass.hidden) {
cls.hidden = true
} else if (containingClass.originallyHidden && containingClass.modifiers.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 && !cls.modifiers.hasShowAnnotation()) {
cls.removed = true
}
}
}
override fun visitMethod(method: MethodItem) {
if (method.modifiers.hasShowAnnotation()) {
method.hidden = false
ensureParentVisible(method)
} else if (method.modifiers.hasHideAnnotations()) {
method.hidden = true
} else {
val containingClass = method.containingClass()
if (containingClass.hidden) {
method.hidden = true
} else if (containingClass.originallyHidden && containingClass.modifiers.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
}
}
}
override fun visitField(field: FieldItem) {
if (field.modifiers.hasShowAnnotation()) {
field.hidden = false
ensureParentVisible(field)
} else if (field.modifiers.hasHideAnnotations()) {
field.hidden = true
} else {
val containingClass = field.containingClass()
/* We don't always propagate field visibility down to the fields
because we sometimes move fields around, and in that
case we don't want to carry forward the "hidden" attribute
from the field that wasn't marked on the field but its
container interface.
*/
if (includingFields && containingClass.hidden) {
field.hidden = true
} else if (containingClass.originallyHidden && containingClass.modifiers.hasShowSingleAnnotation()) {
// See explanation in visitMethod
field.hidden = true
}
if (containingClass.docOnly) {
field.docOnly = true
}
if (containingClass.removed) {
field.removed = true
}
}
}
private fun ensureParentVisible(item: Item) {
val parent = item.parent() ?: return
if (parent.hidden && item.modifiers.hasShowSingleAnnotation()) {
val annotation = item.modifiers.annotations().find {
options.showSingleAnnotations.contains(it.qualifiedName())
} ?: options.showSingleAnnotations.first()
reporter.report(
Errors.SHOWING_MEMBER_IN_HIDDEN_CLASS, item,
"Attempting to unhide ${item.describe()}, but surrounding ${parent.describe()} is " +
"hidden and should also be annotated with $annotation"
)
}
}
})
}
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 = codebase.getPermissionLevel(perm)
if (level == null) {
if (any) {
missing.add(perm)
continue
}
reporter.report(
Errors.REQUIRES_PERMISSION, method,
"Permission '$perm' is not defined by manifest ${codebase.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(
Errors.REQUIRES_PERMISSION, method,
"None of the permissions ${missing.joinToString()} are defined by manifest " +
"${codebase.manifest}."
)
}
if (system.isEmpty() && nonSystem.isEmpty()) {
hasAnnotation = false
} else if (any && !nonSystem.isEmpty() || !any && system.isEmpty()) {
reporter.report(
Errors.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(
Errors.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(Errors.REQUIRES_PERMISSION) &&
options.showAnnotations.contains(ANDROID_SYSTEM_API) && options.manifest != null
val checkHiddenShowAnnotations = !reporter.isSuppressed(Errors.UNHIDDEN_SYSTEM_API) &&
options.showAnnotations.isNotEmpty()
packages.accept(object : ApiVisitor() {
override fun visitParameter(parameter: ParameterItem) {
checkTypeReferencesHidden(parameter, parameter.type())
}
override fun visitItem(item: Item) {
if (item.deprecated && !item.documentation.contains("@deprecated") &&
// 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()
) {
reporter.report(
Errors.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)
}
if (checkHiddenShowAnnotations &&
item.hasShowAnnotation() &&
!item.documentation.contains("@hide") &&
!item.modifiers.hasShowSingleAnnotation()
) {
val annotationName = (item.modifiers.annotations().firstOrNull {
options.showAnnotations.contains(it.qualifiedName())
}?.qualifiedName() ?: options.showAnnotations.first()).removePrefix(ANDROID_ANNOTATION_PREFIX)
reporter.report(
Errors.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 && compatibility.propagateDeprecatedInnerClasses) {
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 && compatibility.propagateDeprecatedMembers) {
field.deprecated = true
}
checkTypeReferencesHidden(field, field.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 && compatibility.propagateDeprecatedMembers) {
method.deprecated = true
}
// Make sure we don't annotate findViewById & getSystemService as @Nullable.
// See for example 68914170.
val name = method.name()
if ((name == "findViewById" || name == "getSystemService") && method.parameters().size == 1 &&
method.modifiers.isNullable()
) {
reporter.report(
Errors.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.annotations().find { it.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
method.returnType()?.scrubAnnotations()
}
}
}
private fun checkTypeReferencesHidden(item: Item, type: TypeItem) {
if (type.primitive) {
return
}
val cls = type.asClass()
// Don't flag type parameters like T
if (cls?.isTypeParameter == true) {
return
}
// class may be null for things like array types and ellipsis types,
// but iterating through the type argument classes below will find and
// check the component class
if (cls != null && !filterReference.test(cls) && !cls.isFromClassPath()) {
reporter.report(
Errors.HIDDEN_TYPE_PARAMETER, item,
"${item.toString().capitalize()} references hidden type $type."
)
}
type.typeArgumentClasses()
.filter { it != cls }
.forEach { checkTypeReferencesHidden(item, it) }
}
private fun checkTypeReferencesHidden(item: Item, cls: ClassItem) {
if (!filterReference.test(cls)) {
if (!cls.isFromClassPath()) {
reporter.report(
Errors.HIDDEN_TYPE_PARAMETER, item,
"${item.toString().capitalize()} references hidden type $cls."
)
}
} else {
cls.typeArgumentClasses()
.filter { it != cls }
.forEach { checkTypeReferencesHidden(item, it) }
}
}
})
}
fun handleStripping() {
// TODO: Switch to visitor iteration
// val stubPackages = options.stubPackages
val stubImportPackages = options.stubImportPackages
handleStripping(stubImportPackages)
}
private fun handleStripping(stubImportPackages: Set<String>) {
val notStrippable = HashSet<ClassItem>(5000)
val filter = ApiPredicate(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.checkLevel() && 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()) {
for (m in cl.methods()) {
if (!m.checkLevel()) {
continue
}
if (m.isHiddenOrRemoved()) {
reporter.report(
Errors.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(
Errors.DEPRECATED, m, "Method " + cl.qualifiedName() + "." +
m.name() + " is deprecated"
)
}
val returnType = m.returnType()
if (!m.deprecated && !cl.deprecated && returnType != null && returnType.asClass()?.deprecated == true) {
reporter.report(
Errors.REFERENCES_DEPRECATED, m,
"Return type of deprecated type $returnType in ${cl.qualifiedName()}.${m.name()}(): this method should also be deprecated"
)
}
var hiddenClass = findHiddenClasses(returnType, stubImportPackages)
if (hiddenClass != null && !hiddenClass.isFromClassPath()) {
if (hiddenClass.qualifiedName() == returnType?.asClass()?.qualifiedName()) {
// Return type is hidden
reporter.report(
Errors.UNAVAILABLE_SYMBOL, m,
"Method ${cl.qualifiedName()}.${m.name()} returns unavailable " +
"type ${hiddenClass.simpleName()}"
)
} else {
// Return type contains a generic parameter
reporter.report(
Errors.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.primitive) {
if (!m.deprecated && !cl.deprecated && t.asClass()?.deprecated == true) {
reporter.report(
Errors.REFERENCES_DEPRECATED, m,
"Parameter of deprecated type $t in ${cl.qualifiedName()}.${m.name()}(): this method should also be deprecated"
)
}
hiddenClass = findHiddenClasses(t, stubImportPackages)
if (hiddenClass != null && !hiddenClass.isFromClassPath()) {
if (hiddenClass.qualifiedName() == t.asClass()?.qualifiedName()) {
// Parameter type is hidden
reporter.report(
Errors.UNAVAILABLE_SYMBOL, m,
"Parameter of unavailable type $t in ${cl.qualifiedName()}.${m.name()}()"
)
} else {
// Parameter type contains a generic parameter
reporter.report(
Errors.HIDDEN_TYPE_PARAMETER, m,
"Parameter uses type parameter of unavailable type $t in ${cl.qualifiedName()}.${m.name()}()"
)
}
}
}
}
val t = m.returnType()
if (t != null && !t.primitive && !m.deprecated && !cl.deprecated && t.asClass()?.deprecated == true) {
reporter.report(
Errors.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(
Errors.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(
Errors.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(Errors.DEPRECATED, cl, "Class ${cl.qualifiedName()} is deprecated")
} else if (reporter.isSuppressed(Errors.REFERENCES_HIDDEN, cl)) {
// If we're not reporting hidden references, bring the type back
// Bring this class back
cl.hidden = false
cl.removed = false
cl.notStrippable = true
}
}
}
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.checkLevel()) && !cl.isTypeParameter) {
reporter.report(
Errors.REFERENCES_HIDDEN, from,
"Class ${cl.qualifiedName()} is ${if (cl.isHiddenOrRemoved()) "hidden" else "not public"} but was referenced ($usage) from public ${from.describe(
false
)}"
)
cl.notStrippable = true
}
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
}
val fieldType = field.type()
if (!fieldType.primitive) {
val typeClass = fieldType.asClass()
if (typeClass != null) {
cantStripThis(typeClass, filter, notStrippable, stubImportPackages, field, "as field type")
}
for (cls in fieldType.typeArgumentClasses()) {
if (cls == typeClass) {
continue
}
cantStripThis(cls, filter, notStrippable, stubImportPackages, field, "as field type argument class")
}
}
}
// cant strip any of the type's generics
for (cls in cl.typeArgumentClasses()) {
cantStripThis(cls, filter, notStrippable, stubImportPackages, cl, "as type argument")
}
// 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 superClass = cl.superClass()
if (superClass != null) {
if (superClass.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 (!superClass.isFromClassPath()) {
reporter.report(
Errors.HIDDEN_SUPERCLASS, cl, "Public class " + cl.qualifiedName() +
" stripped of unavailable superclass " + superClass.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 (superClass.isPrivate && !superClass.isFromClassPath()) {
reporter.report(
Errors.PRIVATE_SUPERCLASS, cl, "Public class " +
cl.qualifiedName() + " extends private class " + superClass.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
}
for (typeParameterClass in method.typeArgumentClasses()) {
cantStripThis(
typeParameterClass,
filter,
notStrippable,
stubImportPackages,
method,
"as type parameter"
)
}
for (parameter in method.parameters()) {
for (parameterTypeClass in parameter.type().typeArgumentClasses()) {
cantStripThis(
parameterTypeClass,
filter,
notStrippable,
stubImportPackages,
parameter,
"as parameter type"
)
for (tcl in parameter.type().typeArgumentClasses()) {
if (tcl == parameterTypeClass) {
continue
}
if (tcl.isHiddenOrRemoved()) {
reporter.report(
Errors.UNAVAILABLE_SYMBOL, method,
"Parameter of hidden type ${tcl.fullName()} " +
"in ${method.containingClass().qualifiedName()}.${method.name()}()"
)
} else {
cantStripThis(
tcl,
filter,
notStrippable,
stubImportPackages,
parameter,
"as type parameter"
)
}
}
}
}
for (thrown in method.throwsTypes()) {
cantStripThis(thrown, filter, notStrippable, stubImportPackages, method, "as exception")
}
val returnType = method.returnType()
if (returnType != null && !returnType.primitive) {
val returnTypeClass = returnType.asClass()
if (returnTypeClass != null) {
cantStripThis(returnTypeClass, filter, notStrippable, stubImportPackages, method, "as return type")
for (tyItem in returnType.typeArgumentClasses()) {
if (tyItem == returnTypeClass) {
continue
}
cantStripThis(
tyItem,
filter,
notStrippable,
stubImportPackages,
method,
"as return type parameter"
)
}
}
}
}
}
/**
* 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 a reference to a hidden class or null if there are none
*/
private fun findHiddenClasses(ti: TypeItem?, stubImportPackages: Set<String>?): ClassItem? {
ti ?: return null
val ci = ti.asClass() ?: return null
return findHiddenClasses(ci, stubImportPackages)
}
private fun findHiddenClasses(ci: ClassItem, stubImportPackages: Set<String>?): ClassItem? {
if (stubImportPackages != null && stubImportPackages.contains(ci.containingPackage().qualifiedName())) {
return null
}
if (ci.isHiddenOrRemoved()) return ci
for (tii in ci.toType().typeArgumentClasses()) {
// Avoid infinite recursion in the case of Foo<T extends Foo>
if (tii != ci) {
val hiddenClass = findHiddenClasses(tii, stubImportPackages)
if (hiddenClass != null) return hiddenClass
}
}
return null
}
}