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