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