1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tools.metalava
18 
19 import com.android.tools.metalava.model.AnnotationItem
20 import com.android.tools.metalava.model.ClassItem
21 import com.android.tools.metalava.model.Item
22 import com.android.tools.metalava.model.MemberItem
23 import com.android.tools.metalava.model.MethodItem
24 import com.android.tools.metalava.model.PackageItem
25 import com.android.tools.metalava.model.TypeParameterItem
26 import java.util.function.Predicate
27 
28 /**
29  * Predicate that decides if the given member should be considered part of an API surface area. To
30  * make the most accurate decision, it searches for signals on the member, all containing classes,
31  * and all containing packages.
32  */
33 class ApiPredicate(
34     /**
35      * Set if the value of [MemberItem.removed] should be ignored. That is, this predicate will
36      * assume that all encountered members match the "removed" requirement.
37      *
38      * This is typically useful when generating "removed.txt", when it's okay to reference both
39      * current and removed APIs.
40      */
41     private val ignoreRemoved: Boolean = false,
42 
43     /**
44      * Set what the value of [MemberItem.removed] must be equal to in order for a member to match.
45      *
46      * This is typically useful when generating "removed.txt", when you only want to match members
47      * that have actually been removed.
48      */
49     private val matchRemoved: Boolean = false,
50 
51     /** Whether we should include doc-only items */
52     private val includeDocOnly: Boolean = false,
53 
54     /** Whether to include "for stub purposes" APIs. See [AnnotationItem.isShowForStubPurposes] */
55     private val includeApisForStubPurposes: Boolean = true,
56 
57     /** Configuration that may be provided by command line options. */
58     private val config: Config = @Suppress("DEPRECATION") options.apiPredicateConfig,
59 ) : Predicate<Item> {
60 
61     /**
62      * Contains configuration for [ApiPredicate] that can, or at least could, come from command line
63      * options.
64      */
65     data class Config(
66         /**
67          * Set if the value of [MemberItem.hasShowAnnotation] should be ignored. That is, this
68          * predicate will assume that all encountered members match the "shown" requirement.
69          *
70          * This is typically useful when generating "current.txt", when no
71          * [Options.allShowAnnotations] have been defined.
72          */
73         val ignoreShown: Boolean = true,
74 
75         /** Whether we allow matching items loaded from jar files instead of sources */
76         val allowClassesFromClasspath: Boolean = true,
77 
78         /**
79          * Whether overriding methods essential for compiling the stubs should be considered as APIs
80          * or not.
81          */
82         val addAdditionalOverrides: Boolean = false,
83     )
84 
testnull85     override fun test(member: Item): Boolean {
86         // non-class, i.e., (literally) member declaration w/o emit flag, e.g., due to `expect`
87         // Some [ClassItem], e.g., JvmInline, java.lang.* classes, may not set the emit flag.
88         if (member !is ClassItem && !member.emit) {
89             return false
90         }
91 
92         // Type Parameter references (e.g. T) aren't actual types, skip all visibility checks
93         if (member is TypeParameterItem) {
94             return true
95         }
96 
97         if (!config.allowClassesFromClasspath && member.isFromClassPath()) {
98             return false
99         }
100 
101         val visibleForAdditionalOverridePurpose =
102             if (config.addAdditionalOverrides) {
103                 member is MethodItem &&
104                     !member.isConstructor() &&
105                     member.isRequiredOverridingMethodForTextStub()
106             } else {
107                 false
108             }
109 
110         var visible =
111             member.isPublic ||
112                 member.isProtected ||
113                 (member.isInternal &&
114                     member.hasShowAnnotation()) // TODO: Should this use checkLevel instead?
115         var hidden = member.hidden && !visibleForAdditionalOverridePurpose
116         if (!visible || hidden) {
117             return false
118         }
119         if (!includeApisForStubPurposes && includeOnlyForStubPurposes(member)) {
120             return false
121         }
122 
123         // If a class item's parent class is an api-only annotation marked class,
124         // the item should be marked visible as well, in order to provide
125         // information about the correct class hierarchy that was concealed for
126         // less restricted APIs.
127         // Only the class definition is marked visible, and class attributes are
128         // not affected.
129         if (
130             member is ClassItem &&
131                 member.superClass()?.let {
132                     it.hasShowAnnotation() && !includeOnlyForStubPurposes(it)
133                 } == true
134         ) {
135             return member.removed == matchRemoved
136         }
137 
138         var hasShowAnnotation = config.ignoreShown || member.hasShowAnnotation()
139         var docOnly = member.docOnly
140         var removed = member.removed
141 
142         var clazz: ClassItem? =
143             when (member) {
144                 is MemberItem -> member.containingClass()
145                 is ClassItem -> member
146                 else -> null
147             }
148 
149         if (clazz != null) {
150             var pkg: PackageItem? = clazz.containingPackage()
151             while (pkg != null) {
152                 hidden = hidden or pkg.hidden
153                 docOnly = docOnly or pkg.docOnly
154                 removed = removed or pkg.removed
155                 pkg = pkg.containingPackage()
156             }
157         }
158         while (clazz != null) {
159             visible =
160                 visible and
161                     (clazz.isPublic ||
162                         clazz.isProtected ||
163                         (clazz.isInternal && clazz.hasShowAnnotation()))
164             hasShowAnnotation =
165                 hasShowAnnotation or (config.ignoreShown || clazz.hasShowAnnotation())
166             hidden = hidden or clazz.hidden
167             docOnly = docOnly or clazz.docOnly
168             removed = removed or clazz.removed
169             clazz = clazz.containingClass()
170         }
171 
172         if (ignoreRemoved) {
173             removed = matchRemoved
174         }
175 
176         if (docOnly && includeDocOnly) {
177             docOnly = false
178         }
179 
180         return visible && hasShowAnnotation && !hidden && !docOnly && removed == matchRemoved
181     }
182 
183     /**
184      * Returns true, if an item should be included only for "stub" purposes; that is, the item does
185      * have at least one [AnnotationItem.isShowAnnotation] annotation and all those annotations are
186      * also an [AnnotationItem.isShowForStubPurposes] annotation.
187      */
includeOnlyForStubPurposesnull188     private fun includeOnlyForStubPurposes(item: Item): Boolean {
189         if (!item.codebase.annotationManager.hasAnyStubPurposesAnnotations()) {
190             return false
191         }
192 
193         return includeOnlyForStubPurposesRecursive(item)
194     }
195 
includeOnlyForStubPurposesRecursivenull196     private fun includeOnlyForStubPurposesRecursive(item: Item): Boolean {
197         // Get the item's API membership. If it belongs to an API surface then return `true` if the
198         // API surface to which it belongs is the base API, and false otherwise.
199         val membership = item.apiMembership()
200         if (membership != ApiMembership.NONE_OR_UNANNOTATED) {
201             return membership == ApiMembership.BASE
202         }
203 
204         // If this item has neither --show-annotation nor --show-for-stub-purposes-annotation,
205         // Then defer to the "parent" item (i.e. the containing class or package).
206         return item.parent()?.let { includeOnlyForStubPurposesRecursive(it) } ?: false
207     }
208 
209     /**
210      * Indicates which API, if any, an annotated item belongs to.
211      *
212      * This does not take into account unannotated items which are part of an API; they will be
213      * treated as being in no API, i.e. have a membership of [NONE_OR_UNANNOTATED].
214      */
215     private enum class ApiMembership {
216         /**
217          * An item is not part of any API, at least not one which is defined through an annotation.
218          * It could be part of the unannotated API, i.e. `--show-unannotated`.
219          */
220         NONE_OR_UNANNOTATED,
221 
222         /**
223          * An item is part of the base API, i.e. the API which the [CURRENT] API extends.
224          *
225          * Items in this API will be output to stub files (which must include the whole API surface)
226          * but not signature files (which only include a delta on the base API surface).
227          */
228         BASE,
229 
230         /**
231          * An item is part of the current API, i.e. the API being generated by this invocation of
232          * metalava.
233          *
234          * Items in this API will be output to stub and signature files.
235          */
236         CURRENT
237     }
238 
239     /** Get the API to which this [Item] belongs, according to the annotations. */
Itemnull240     private fun Item.apiMembership(): ApiMembership {
241         // If the item has a "show" annotation, then return whether it *only* has a "for stubs"
242         // show annotation or not.
243         //
244         // Note, If the item does not have a show annotation, then it can't have a "for stubs" one,
245         // because the later must be a subset of the former, which we don't detect in *this*
246         // run (unfortunately it's hard to do so due to how things work), but when metalava
247         // is executed for the parent API, we'd detect it as
248         // [Issues.SHOWING_MEMBER_IN_HIDDEN_CLASS].
249         val showability = this.showability
250         if (showability.show()) {
251             if (showability.showForStubsOnly()) {
252                 return ApiMembership.BASE
253             } else {
254                 return ApiMembership.CURRENT
255             }
256         }
257 
258         // Unlike classes or fields, methods implicitly inherits visibility annotations, and for
259         // some visibility calculation we need to take it into account.
260         //
261         // See ShowAnnotationTest.`Methods inherit showAnnotations but fields and classes don't`.
262         var membership = ApiMembership.NONE_OR_UNANNOTATED
263         if (this is MethodItem) {
264             // Find the maximum API membership inherited from an overridden method.
265             for (superMethod in superMethods()) {
266                 val superMethodMembership = superMethod.apiMembership()
267                 membership = maxOf(membership, superMethodMembership)
268                 // Break out if membership == CURRENT as that is the maximum allowable
269                 // [ApiMembership] so there is no point in checking any other methods.
270                 if (membership == ApiMembership.CURRENT) {
271                     break
272                 }
273             }
274         }
275         return membership
276     }
277 }
278