• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 package com.android.tools.metalava.apilevels
17 
18 import com.android.tools.metalava.SdkIdentifier
19 import org.xml.sax.Attributes
20 import org.xml.sax.helpers.DefaultHandler
21 import javax.xml.parsers.SAXParserFactory
22 
23 /**
24  * A filter of classes, fields and methods that are allowed in and extension SDK, and for each item,
25  * what extension SDK it first appeared in. Also, a mapping between SDK name and numerical ID.
26  *
27  * Internally, the filers are represented as a tree, where each node in the tree matches a part of a
28  * package, class or member name. For example, given the patterns
29  *
30  *   com.example.Foo             -> [A]
31  *   com.example.Foo#someMethod  -> [B]
32  *   com.example.Bar             -> [A, C]
33  *
34  * (anything prefixed with com.example.Foo is allowed and part of the A extension, except for
35  * com.example.Foo#someMethod which is part of B; anything prefixed with com.example.Bar is part
36  * of both A and C), the internal tree looks like
37  *
38  *   root -> null
39  *     com -> null
40  *       example -> null
41  *         Foo -> [A]
42  *           someMethod -> [B]
43  *         Bar -> [A, C]
44  */
45 class ApiToExtensionsMap private constructor(
46     private val sdkIdentifiers: Set<SdkIdentifier>,
47     private val root: Node,
48 ) {
isEmptynull49     fun isEmpty(): Boolean = root.children.isEmpty() && root.extensions.isEmpty()
50 
51     fun getExtensions(clazz: ApiClass): List<String> = getExtensions(clazz.name.toDotNotation())
52 
53     fun getExtensions(clazz: ApiClass, member: ApiElement): List<String> =
54         getExtensions(clazz.name.toDotNotation() + "#" + member.name.toDotNotation())
55 
56     fun getExtensions(what: String): List<String> {
57         // Special case: getExtensionVersion is not part of an extension
58         val sdkExtensions = "android.os.ext.SdkExtensions"
59         if (what == sdkExtensions || what == "$sdkExtensions#getExtensionVersion") {
60             return listOf()
61         }
62 
63         val parts = what.split(REGEX_DELIMITERS)
64 
65         var lastSeenExtensions = root.extensions
66         var node = root.children.findNode(parts[0]) ?: return lastSeenExtensions
67         if (node.extensions.isNotEmpty()) {
68             lastSeenExtensions = node.extensions
69         }
70 
71         for (part in parts.stream().skip(1)) {
72             node = node.children.findNode(part) ?: break
73             if (node.extensions.isNotEmpty()) {
74                 lastSeenExtensions = node.extensions
75             }
76         }
77         return lastSeenExtensions
78     }
79 
getSdkIdentifiersnull80     fun getSdkIdentifiers(): Set<SdkIdentifier> = sdkIdentifiers.toSet()
81 
82     /**
83      * Construct a `sdks` attribute value
84      *
85      * `sdks` is an XML attribute on class, method and fields in the XML generated by ARG_GENERATE_API_LEVELS.
86      * It expresses in what SDKs an API exist, and in which version of each SDK it was first introduced;
87      * `sdks` replaces the `since` attribute.
88      *
89      * The format of `sdks` is
90      *
91      * sdks="ext:version[,ext:version[,...]]
92      *
93      * where <ext> is the numerical ID of the SDK, and <version> is the version in which the API was introduced.
94      *
95      * The returned string is guaranteed to be one of
96      *
97      * - list of (extensions,finalized_version) pairs + ANDROID_SDK:finalized_dessert
98      * - list of (extensions,finalized_version) pairs
99      * - ANDROID_SDK:finalized_dessert
100      * - ANDROID_SDK:next_dessert_int (for symbols not finalized anywhere)
101      *
102      * See go/mainline-sdk-api-versions-xml for more information.
103      *
104      * @param androidSince Android dessert version in which this symbol was finalized, or notFinalizedValue
105      *                     if this symbol has not been finalized in an Android dessert
106      * @param notFinalizedValue value used together with the Android SDK ID to indicate that this symbol
107      *                          has not been finalized at all
108      * @param extensions names of the SDK extensions in which this symbol has been finalized (may be non-empty
109      *                   even if extensionsSince is ApiElement.NEVER)
110      * @param extensionsSince the version of the SDK extensions in which this API was initially introduced
111      *                        (same value for all SDK extensions), or ApiElement.NEVER if this symbol
112      *                        has not been finalized in any SDK extension (regardless of the extensions argument)
113      * @return an `sdks` value suitable for including verbatim in XML
114      */
115     fun calculateSdksAttr(
116         androidSince: Int,
117         notFinalizedValue: Int,
118         extensions: List<String>,
119         extensionsSince: Int
120     ): String {
121         // Special case: symbol not finalized anywhere -> "ANDROID_SDK:next_dessert_int"
122         if (androidSince == notFinalizedValue && extensionsSince == ApiElement.NEVER) {
123             return "$ANDROID_PLATFORM_SDK_ID:$notFinalizedValue"
124         }
125 
126         val versions = mutableSetOf<String>()
127         // Only include SDK extensions if the symbol has been finalized in at least one
128         if (extensionsSince != ApiElement.NEVER) {
129             for (ext in extensions) {
130                 val ident = sdkIdentifiers.find {
131                     it.shortname == ext
132                 } ?: throw IllegalStateException("unknown extension SDK \"$ext\"")
133                 assert(ident.id != ANDROID_PLATFORM_SDK_ID) // invariant
134                 versions.add("${ident.id}:$extensionsSince")
135             }
136         }
137 
138         // Only include the Android SDK in `sdks` if
139         // - the symbol has been finalized in an Android dessert, and
140         // - the symbol has been finalized in at least one SDK extension
141         if (androidSince != notFinalizedValue && versions.isNotEmpty()) {
142             versions.add("$ANDROID_PLATFORM_SDK_ID:$androidSince")
143         }
144         return versions.joinToString(",")
145     }
146 
147     companion object {
148         // Hard-coded ID for the Android platform SDK. Used identically as the extension SDK IDs
149         // to express when an API first appeared in an SDK.
150         const val ANDROID_PLATFORM_SDK_ID = 0
151 
152         private val REGEX_DELIMITERS = Regex("[.#$]")
153 
154         /*
155          * Create an ApiToExtensionsMap from a list of text based rules.
156          *
157          * The input is XML:
158          *
159          *     <?xml version="1.0" encoding="utf-8"?>
160          *     <sdk-extensions-info version="1">
161          *         <sdk name="<name>" shortname="<short-name>" id="<int>" reference="<constant>" />
162          *         <symbol jar="<jar>" pattern="<pattern>" sdks="<sdks>" />
163          *     </sdk-extensions-info>
164          *
165          * The <sdk> and <symbol> tags may be repeated.
166          *
167          * - <name> is a long name for the SDK, e.g. "R Extensions".
168          *
169          * - <short-name> is a short name for the SDK, e.g. "R-ext".
170          *
171          * - <id> is the numerical identifier for the SDK, e.g. 30. It is an error to use the
172          *   Android SDK ID (0).
173          *
174          * - <jar> is the jar file symbol belongs to, named after the jar file in
175          *   prebuilts/sdk/extensions/<int>/public, e.g. "framework-sdkextensions".
176          *
177          * - <constant> is a Java symbol that can be passed to `SdkExtensions.getExtensionVersion`
178          *   to look up the version of the corresponding SDK, e.g.
179          *   "android/os/Build$VERSION_CODES$R"
180          *
181          * - <pattern> is either '*', which matches everything, or a 'com.foo.Bar$Inner#member'
182          *   string (or prefix thereof terminated before . or $), which matches anything with that
183          *   prefix. Note that arguments and return values of methods are omitted (and there is no
184          *   way to distinguish overloaded methods).
185          *
186          * - <sdks> is a comma separated list of SDKs in which the symbol defined by <jar> and
187          *   <pattern> appears; the list items are <name> attributes of SDKs defined in the XML.
188          *
189          * It is an error to specify the same <jar> and <pattern> pair twice.
190          *
191          * A more specific <symbol> rule has higher precedence than a less specific rule.
192          *
193          * @param jar jar file to limit lookups to: ignore symbols not present in this jar file
194          * @param xml XML as described above
195          * @throws IllegalArgumentException if the XML is malformed
196          */
fromXmlnull197         fun fromXml(filterByJar: String, xml: String): ApiToExtensionsMap {
198             val root = Node("<root>")
199             val sdkIdentifiers = mutableSetOf<SdkIdentifier>()
200             val allSeenExtensions = mutableSetOf<String>()
201 
202             val parser = SAXParserFactory.newDefaultInstance().newSAXParser()
203             try {
204                 parser.parse(
205                     xml.byteInputStream(),
206                     object : DefaultHandler() {
207                         override fun startElement(uri: String, localName: String, qualifiedName: String, attributes: Attributes) {
208                             when (qualifiedName) {
209                                 "sdk" -> {
210                                     val id = attributes.getIntOrThrow(qualifiedName, "id")
211                                     val shortname = attributes.getStringOrThrow(qualifiedName, "shortname")
212                                     val name = attributes.getStringOrThrow(qualifiedName, "name")
213                                     val reference = attributes.getStringOrThrow(qualifiedName, "reference")
214                                     sdkIdentifiers.add(SdkIdentifier(id, shortname, name, reference))
215                                 }
216                                 "symbol" -> {
217                                     val jar = attributes.getStringOrThrow(qualifiedName, "jar")
218                                     if (jar != filterByJar) {
219                                         return
220                                     }
221                                     val sdks = attributes.getStringOrThrow(qualifiedName, "sdks").split(',')
222                                     if (sdks != sdks.distinct()) {
223                                         throw IllegalArgumentException("symbol lists the same SDK multiple times: '$sdks'")
224                                     }
225                                     allSeenExtensions.addAll(sdks)
226                                     val pattern = attributes.getStringOrThrow(qualifiedName, "pattern")
227                                     if (pattern == "*") {
228                                         root.extensions = sdks
229                                         return
230                                     }
231                                     // add each part of the pattern as separate nodes, e.g. if pattern is
232                                     // com.example.Foo, add nodes, "com" -> "example" -> "Foo"
233                                     val parts = pattern.split(REGEX_DELIMITERS)
234                                     var node = root.children.addNode(parts[0])
235                                     for (name in parts.stream().skip(1)) {
236                                         node = node.children.addNode(name)
237                                     }
238                                     if (node.extensions.isNotEmpty()) {
239                                         throw IllegalArgumentException("duplicate pattern: $pattern")
240                                     }
241                                     node.extensions = sdks
242                                 }
243                             }
244                         }
245                     }
246                 )
247             } catch (e: Throwable) {
248                 throw IllegalArgumentException("failed to parse xml", e)
249             }
250 
251             // verify: the predefined Android platform SDK ID is not reused as an extension SDK ID
252             if (sdkIdentifiers.any { it.id == ANDROID_PLATFORM_SDK_ID }) {
253                 throw IllegalArgumentException("bad SDK definition: the ID $ANDROID_PLATFORM_SDK_ID is reserved for the Android platform SDK")
254             }
255 
256             // verify: all rules refer to declared SDKs
257             val allSdkNames = sdkIdentifiers.map { it.shortname }.toList()
258             for (ext in allSeenExtensions) {
259                 if (!allSdkNames.contains(ext)) {
260                     throw IllegalArgumentException("bad SDK definitions: undefined SDK $ext")
261                 }
262             }
263 
264             // verify: no duplicate SDK IDs
265             if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.id }.size) {
266                 throw IllegalArgumentException("bad SDK definitions: duplicate SDK IDs")
267             }
268 
269             // verify: no duplicate SDK names
270             if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.shortname }.size) {
271                 throw IllegalArgumentException("bad SDK definitions: duplicate SDK short names")
272             }
273 
274             // verify: no duplicate SDK names
275             if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.name }.size) {
276                 throw IllegalArgumentException("bad SDK definitions: duplicate SDK names")
277             }
278 
279             // verify: no duplicate SDK references
280             if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.reference }.size) {
281                 throw IllegalArgumentException("bad SDK definitions: duplicate SDK references")
282             }
283 
284             return ApiToExtensionsMap(sdkIdentifiers, root)
285         }
286     }
287 }
288 
MutableSetnull289 private fun MutableSet<Node>.addNode(name: String): Node {
290     findNode(name)?.let {
291         return it
292     }
293     val node = Node(name)
294     add(node)
295     return node
296 }
297 
Attributesnull298 private fun Attributes.getStringOrThrow(tag: String, attr: String): String = getValue(attr) ?: throw IllegalArgumentException("<$tag>: missing attribute: $attr")
299 
300 private fun Attributes.getIntOrThrow(tag: String, attr: String): Int = getStringOrThrow(tag, attr).toIntOrNull() ?: throw IllegalArgumentException("<$tag>: attribute $attr: not an integer")
301 
302 private fun Set<Node>.findNode(breadcrumb: String): Node? = find { it.breadcrumb == breadcrumb }
303 
Stringnull304 private fun String.toDotNotation(): String = split('(')[0].replace('/', '.')
305 
306 private class Node(val breadcrumb: String) {
307     var extensions: List<String> = emptyList()
308     val children: MutableSet<Node> = mutableSetOf()
309 }
310