1 /* <lambda>null2 * Copyright (C) 2023 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.ANNOTATION_ATTR_VALUE 20 import com.android.tools.metalava.model.AnnotationArrayAttributeValue 21 import com.android.tools.metalava.model.AnnotationAttribute 22 import com.android.tools.metalava.model.AnnotationItem 23 import com.android.tools.metalava.model.AnnotationSingleAttributeValue 24 import com.android.tools.metalava.model.DefaultAnnotationAttribute 25 import java.util.TreeMap 26 27 interface AnnotationFilter { 28 // tells whether an annotation is included by the filter 29 fun matches(annotation: AnnotationItem): Boolean 30 // tells whether an annotation is included by this filter 31 fun matches(annotationSource: String): Boolean 32 33 // Returns a sorted set of fully qualified annotation names that may be included by this filter. 34 // Note that this filter might incorporate parameters but this function strips them. 35 fun getIncludedAnnotationNames(): Set<String> 36 // Returns true if [getIncludedAnnotationNames] includes the given qualified name 37 fun matchesAnnotationName(qualifiedName: String): Boolean 38 39 // Returns true if nothing is matched by this filter 40 fun isEmpty(): Boolean 41 // Returns true if some annotation is matched by this filter 42 fun isNotEmpty(): Boolean 43 44 companion object { 45 private val empty = AnnotationFilterBuilder().build() 46 47 fun emptyFilter(): AnnotationFilter = empty 48 49 /** 50 * Create an [AnnotationFilter] from a list of [filterExpressions] each of which is an 51 * annotation filter expression that can include or exclude an annotation based on its 52 * qualified name and/or attribute values. 53 */ 54 fun create(filterExpressions: List<String>): AnnotationFilter { 55 val builder = AnnotationFilterBuilder() 56 filterExpressions.forEach(builder::add) 57 return builder.build() 58 } 59 } 60 } 61 62 /** Builder for [AnnotationFilter]s. */ 63 class AnnotationFilterBuilder { 64 private val inclusionExpressions = mutableListOf<AnnotationFilterEntry>() 65 66 // Adds the given option as a fully qualified annotation name to match with this filter 67 // Can be "androidx.annotation.RestrictTo" 68 // Can be "androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP)" 69 // Note that the order of calls to this method could affect the return from 70 // {@link #firstQualifiedName} . addnull71 fun add(option: String) { 72 val (matchResult, pattern) = 73 if (option.startsWith("!")) { 74 Pair(false, option.substring(1)) 75 } else { 76 Pair(true, option) 77 } 78 inclusionExpressions.add(AnnotationFilterEntry.fromOption(pattern, matchResult)) 79 } 80 81 /** Build the [AnnotationFilter]. */ buildnull82 fun build(): AnnotationFilter { 83 // Sort the expressions by match result, so that those expressions that exclude come before 84 // those which include. 85 val map = 86 inclusionExpressions 87 .sortedBy { it.matchResult } 88 .groupByTo(TreeMap()) { it.qualifiedName } 89 90 // Verify that the filter is consistent. 91 for ((fqn, patterns) in map.entries) { 92 val (includes, excludes) = patterns.partition { it.matchResult } 93 if (excludes.isNotEmpty()) { 94 for (exclude in excludes) { 95 if (exclude.attributes.isEmpty()) { 96 throw IllegalStateException( 97 "Exclude pattern '$exclude' is invalid as it does not specify attributes" 98 ) 99 } 100 } 101 102 if (includes.isEmpty()) { 103 throw IllegalStateException( 104 "Patterns for '$fqn' contains ${excludes.size} excludes but no includes" 105 ) 106 } 107 } 108 } 109 return ImmutableAnnotationFilter(map) 110 } 111 } 112 113 // Immutable implementation of AnnotationFilter 114 private class ImmutableAnnotationFilter( 115 private val qualifiedNameToEntries: Map<String, List<AnnotationFilterEntry>> 116 ) : AnnotationFilter { 117 matchesnull118 override fun matches(annotationSource: String): Boolean { 119 val wrapper = AnnotationFilterEntry.fromSource(annotationSource) 120 return matches(wrapper) 121 } 122 matchesnull123 override fun matches(annotation: AnnotationItem): Boolean { 124 val qualifiedName = annotation.qualifiedName 125 // If the annotation name is not in the map of annotation names that can be matched then 126 // this can never match so return immediately rather than generating the source 127 // representation of the annotation. 128 if (qualifiedName !in qualifiedNameToEntries) { 129 return false 130 } 131 val wrapper = AnnotationFilterEntry.fromAnnotationItem(annotation) 132 return matches(wrapper) 133 } 134 matchesnull135 private fun matches(annotation: AnnotationFilterEntry): Boolean { 136 val entries = qualifiedNameToEntries[annotation.qualifiedName] ?: return false 137 return entries.firstOrNull { entry -> annotationsMatch(entry, annotation) }?.matchResult 138 ?: false 139 } 140 getIncludedAnnotationNamesnull141 override fun getIncludedAnnotationNames(): Set<String> = qualifiedNameToEntries.keys 142 143 override fun matchesAnnotationName(qualifiedName: String): Boolean { 144 return qualifiedNameToEntries.contains(qualifiedName) 145 } 146 isEmptynull147 override fun isEmpty(): Boolean { 148 return qualifiedNameToEntries.isEmpty() 149 } 150 isNotEmptynull151 override fun isNotEmpty(): Boolean { 152 return !isEmpty() 153 } 154 annotationsMatchnull155 private fun annotationsMatch( 156 filter: AnnotationFilterEntry, 157 existingAnnotation: AnnotationFilterEntry 158 ): Boolean { 159 if (filter.attributes.count() > existingAnnotation.attributes.count()) { 160 return false 161 } 162 for (attribute in filter.attributes) { 163 val existingValue = existingAnnotation.findAttribute(attribute.name)?.value 164 val existingValueSource = existingValue?.toSource() 165 val attributeValueSource = attribute.value.toSource() 166 if (attribute.name == "value") { 167 // Special-case where varargs value annotation attribute can be specified with 168 // either @Foo(BAR) or @Foo({BAR}) and they are equivalent. 169 when { 170 attribute.value is AnnotationSingleAttributeValue && 171 existingValue is AnnotationArrayAttributeValue -> { 172 if (existingValueSource != "{$attributeValueSource}") return false 173 } 174 attribute.value is AnnotationArrayAttributeValue && 175 existingValue is AnnotationSingleAttributeValue -> { 176 if ("{$existingValueSource}" != attributeValueSource) return false 177 } 178 else -> { 179 if (existingValueSource != attributeValueSource) return false 180 } 181 } 182 } else { 183 if (existingValueSource != attributeValueSource) { 184 return false 185 } 186 } 187 } 188 return true 189 } 190 } 191 192 // An AnnotationFilterEntry filters for annotations having a certain qualifiedName and 193 // possibly certain attributes. 194 // An AnnotationFilterEntry doesn't necessarily have a Codebase like an AnnotationItem does 195 private class AnnotationFilterEntry( 196 val qualifiedName: String, 197 val attributes: List<AnnotationAttribute>, 198 /** The result that will be returned from [AnnotationFilter.matches] when this entry matches. */ 199 val matchResult: Boolean, 200 ) { findAttributenull201 fun findAttribute(name: String?): AnnotationAttribute? { 202 val actualName = name ?: ANNOTATION_ATTR_VALUE 203 return attributes.firstOrNull { it.name == actualName } 204 } 205 toStringnull206 override fun toString(): String { 207 return buildString { 208 if (!matchResult) { 209 append("!") 210 } 211 append(qualifiedName) 212 if (attributes.isNotEmpty()) { 213 append("(") 214 attributes.joinTo(this) 215 append(")") 216 } 217 } 218 } 219 220 companion object { fromSourcenull221 fun fromSource(source: String): AnnotationFilterEntry { 222 val text = source.replace("@", "") 223 return fromOption(text) 224 } 225 fromOptionnull226 fun fromOption(text: String, matchResult: Boolean = true): AnnotationFilterEntry { 227 val index = text.indexOf("(") 228 229 val qualifiedName = 230 if (index == -1) { 231 text 232 } else { 233 text.substring(0, index) 234 } 235 236 val attributes: List<AnnotationAttribute> = 237 if (index == -1) { 238 emptyList() 239 } else { 240 DefaultAnnotationAttribute.createList( 241 text.substring(index + 1, text.lastIndexOf(')')) 242 ) 243 } 244 return AnnotationFilterEntry(qualifiedName, attributes, matchResult) 245 } 246 fromAnnotationItemnull247 fun fromAnnotationItem(annotationItem: AnnotationItem): AnnotationFilterEntry { 248 // Have to call toSource to resolve attribute values into fully qualified class names. 249 // For example: resolving RestrictTo(LIBRARY_GROUP) into 250 // RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) 251 // In addition, toSource (with the default argument showDefaultAttrs=true) retrieves 252 // default attributes from the definition of the annotation. For example, 253 // @SystemApi actually is converted into @android.annotation.SystemApi(\ 254 // client=android.annotation.SystemApi.Client.PRIVILEGED_APPS,\ 255 // process=android.annotation.SystemApi.Process.ALL) 256 return fromSource(annotationItem.toSource()) 257 } 258 } 259 } 260