• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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.model.visitors
18 
19 import com.android.tools.metalava.model.AnnotationItem
20 import com.android.tools.metalava.model.ClassContentItem
21 import com.android.tools.metalava.model.ClassItem
22 import com.android.tools.metalava.model.ClassOrigin
23 import com.android.tools.metalava.model.FilterPredicate
24 import com.android.tools.metalava.model.MemberItem
25 import com.android.tools.metalava.model.MethodItem
26 import com.android.tools.metalava.model.SelectableItem
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,
59 ) : FilterPredicate {
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(item: SelectableItem): 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 (item !is ClassItem && !item.emit) {
89             return false
90         }
91 
92         if (
93             !config.allowClassesFromClasspath &&
94                 item is ClassContentItem &&
95                 // This disallows classes from the source path not just the class path, contrary to
96                 // what might be expected from the config property name.
97                 item.origin != ClassOrigin.COMMAND_LINE
98         ) {
99             return false
100         }
101 
102         val visibleForAdditionalOverridePurpose =
103             if (config.addAdditionalOverrides) {
104                 item is MethodItem && item.isRequiredOverridingMethodForTextStub()
105             } else {
106                 false
107             }
108 
109         val itemSelectors = item.variantSelectors
110 
111         // If the item or any of its containing classes are inaccessible then ignore it.
112         if (!itemSelectors.accessible) return false
113 
114         var hidden = itemSelectors.hidden && !visibleForAdditionalOverridePurpose
115         if (hidden) return false
116 
117         if (!includeApisForStubPurposes && includeOnlyForStubPurposes(item)) {
118             return false
119         }
120 
121         // If a class item's parent class is an api-only annotation marked class,
122         // the item should be marked visible as well, in order to provide
123         // information about the correct class hierarchy that was concealed for
124         // less restricted APIs.
125         // Only the class definition is marked visible, and class attributes are
126         // not affected.
127         if (
128             item is ClassItem &&
129                 item.superClass()?.let {
130                     it.hasShowAnnotation() && !includeOnlyForStubPurposes(it)
131                 } == true
132         ) {
133             return itemSelectors.removed == matchRemoved
134         }
135 
136         // If docOnly items are not included and this item is docOnly then ignore it.
137         if (!includeDocOnly && itemSelectors.docOnly) return false
138 
139         // If removed status is not ignored and this item's status does not match what is required
140         // then ignore this item.
141         if (!ignoreRemoved && itemSelectors.removed != matchRemoved) return false
142 
143         val closestClass: ClassItem? =
144             when (item) {
145                 is MemberItem -> item.containingClass()
146                 is ClassItem -> item
147                 else -> null
148             }
149 
150         if (!config.ignoreShown) {
151             var hasShowAnnotation = item.hasShowAnnotation()
152             var showClass = closestClass
153             while (showClass != null && !hasShowAnnotation) {
154                 hasShowAnnotation = showClass.hasShowAnnotation()
155                 showClass = showClass.containingClass()
156             }
157             if (!hasShowAnnotation) return false
158         }
159 
160         var hiddenClass = closestClass
161         while (hiddenClass != null) {
162             if (hiddenClass.hidden) return false
163             hiddenClass = hiddenClass.containingClass()
164         }
165 
166         return true
167     }
168 
169     /**
170      * Returns true, if an item should be included only for "stub" purposes; that is, the item does
171      * have at least one [AnnotationItem.isShowAnnotation] annotation and all those annotations are
172      * also an [AnnotationItem.isShowForStubPurposes] annotation.
173      */
includeOnlyForStubPurposesnull174     private fun includeOnlyForStubPurposes(item: SelectableItem): Boolean {
175         if (!item.codebase.annotationManager.hasAnyStubPurposesAnnotations()) {
176             return false
177         }
178 
179         return includeOnlyForStubPurposesRecursive(item)
180     }
181 
includeOnlyForStubPurposesRecursivenull182     private fun includeOnlyForStubPurposesRecursive(item: SelectableItem): Boolean {
183         // Get the item's API membership. If it belongs to an API surface then return `true` if the
184         // API surface to which it belongs is the base API, and false otherwise.
185         val membership = item.apiMembership()
186         if (membership != ApiMembership.NONE_OR_UNANNOTATED) {
187             return membership == ApiMembership.BASE
188         }
189 
190         // If this item has neither --show-annotation nor --show-for-stub-purposes-annotation,
191         // Then defer to the "parent" item (i.e. the containing class or package).
192         return item.parent()?.let { includeOnlyForStubPurposesRecursive(it) } ?: false
193     }
194 
195     /**
196      * Indicates which API, if any, an annotated item belongs to.
197      *
198      * This does not take into account unannotated items which are part of an API; they will be
199      * treated as being in no API, i.e. have a membership of [NONE_OR_UNANNOTATED].
200      */
201     private enum class ApiMembership {
202         /**
203          * An item is not part of any API, at least not one which is defined through an annotation.
204          * It could be part of the unannotated API, i.e. `--show-unannotated`.
205          */
206         NONE_OR_UNANNOTATED,
207 
208         /**
209          * An item is part of the base API, i.e. the API which the [CURRENT] API extends.
210          *
211          * Items in this API will be output to stub files (which must include the whole API surface)
212          * but not signature files (which only include a delta on the base API surface).
213          */
214         BASE,
215 
216         /**
217          * An item is part of the current API, i.e. the API being generated by this invocation of
218          * metalava.
219          *
220          * Items in this API will be output to stub and signature files.
221          */
222         CURRENT
223     }
224 
225     /** Get the API to which this [SelectableItem] belongs, according to the annotations. */
SelectableItemnull226     private fun SelectableItem.apiMembership(): ApiMembership {
227         // If the item has a "show" annotation, then return whether it *only* has a "for stubs"
228         // show annotation or not.
229         //
230         // Note, If the item does not have a show annotation, then it can't have a "for stubs" one,
231         // because the later must be a subset of the former, which we don't detect in *this*
232         // run (unfortunately it's hard to do so due to how things work), but when metalava
233         // is executed for the parent API, we'd detect it as
234         // [Issues.SHOWING_MEMBER_IN_HIDDEN_CLASS].
235         val showability = this.showability
236         if (showability.show()) {
237             if (showability.showForStubsOnly()) {
238                 return ApiMembership.BASE
239             } else {
240                 return ApiMembership.CURRENT
241             }
242         }
243 
244         // Unlike classes or fields, methods implicitly inherits visibility annotations, and for
245         // some visibility calculation we need to take it into account.
246         //
247         // See ShowAnnotationTest.`Methods inherit showAnnotations but fields and classes don't`.
248         var membership = ApiMembership.NONE_OR_UNANNOTATED
249         if (this is MethodItem) {
250             // Find the maximum API membership inherited from an overridden method.
251             for (superMethod in superMethods()) {
252                 val superMethodMembership = superMethod.apiMembership()
253                 membership = maxOf(membership, superMethodMembership)
254                 // Break out if membership == CURRENT as that is the maximum allowable
255                 // [ApiMembership] so there is no point in checking any other methods.
256                 if (membership == ApiMembership.CURRENT) {
257                     break
258                 }
259             }
260         }
261         return membership
262     }
263 }
264