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