• 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 /**
19  * A filter of classes, fields and methods that are allowed in and extension SDK, and for each item,
20  * what extension SDK it first appeared in. Also, a mapping between SDK name and numerical ID.
21  *
22  * Internally, the filters are represented as a tree, where each node in the tree matches a part of
23  * a package, class or member name. For example, given the patterns
24  *
25  * ```
26  * com.example.Foo -> [A] com.example.Foo#someMethod -> [B] com.example.Bar -> [A, C]
27  * ```
28  *
29  * (anything prefixed with com.example.Foo is allowed and part of the A extension, except for
30  * com.example.Foo#someMethod which is part of B; anything prefixed with com.example.Bar is part of
31  * both A and C), the internal tree looks like
32  *
33  * ```
34  * root -> null com -> null example -> null Foo -> [A] someMethod -> [B] Bar -> [A, C]
35  * ```
36  */
37 class ApiToExtensionsMap
38 internal constructor(
39     private val availableSdkExtensions: AvailableSdkExtensions,
40     internal val root: Node,
41 ) {
isEmptynull42     fun isEmpty(): Boolean = root.children.isEmpty() && root.extensions.isEmpty()
43 
44     fun getExtensions(clazz: ApiClass): List<String> = getExtensions(clazz.name.toDotNotation())
45 
46     fun getExtensions(clazz: ApiClass, member: ApiElement): List<String> =
47         getExtensions(clazz.name.toDotNotation() + "#" + member.name.toDotNotation())
48 
49     fun getExtensions(what: String): List<String> {
50         // Special case: getExtensionVersion is not part of an extension
51         val sdkExtensions = "android.os.ext.SdkExtensions"
52         if (what == sdkExtensions || what == "$sdkExtensions#getExtensionVersion") {
53             return listOf()
54         }
55 
56         val parts = what.splitIntoBreadcrumbs()
57 
58         var lastSeenExtensions = root.extensions
59         var node = root.children.findNode(parts[0]) ?: return lastSeenExtensions
60         if (node.extensions.isNotEmpty()) {
61             lastSeenExtensions = node.extensions
62         }
63 
64         for (part in parts.stream().skip(1)) {
65             node = node.children.findNode(part) ?: break
66             if (node.extensions.isNotEmpty()) {
67                 lastSeenExtensions = node.extensions
68             }
69         }
70         return lastSeenExtensions
71     }
72 
73     /**
74      * Construct a `sdks` attribute value
75      *
76      * `sdks` is an XML attribute on class, method and fields in the XML generated by
77      * ARG_GENERATE_API_LEVELS. It expresses in what SDKs an API exist, and in which version of each
78      * SDK it was first introduced; `sdks` replaces the `since` attribute.
79      *
80      * The format of `sdks` is
81      *
82      * ```
83      * sdks="ext:version[,ext:version[,...]]
84      * ```
85      *
86      * where <ext> is the numerical ID of the SDK, and <version> is the version in which the API was
87      * introduced.
88      *
89      * The returned string is guaranteed to be one of
90      * - list of (extensions,finalized_version) pairs + ANDROID_SDK:finalized_dessert
91      * - list of (extensions,finalized_version) pairs
92      * - ANDROID_SDK:finalized_dessert
93      * - ANDROID_SDK:next_dessert_int (for symbols not finalized anywhere)
94      *
95      * See go/mainline-sdk-api-versions-xml for more information.
96      *
97      * @param androidSince Android dessert version in which this symbol was finalized, or
98      *   notFinalizedValue if this symbol has not been finalized in an Android dessert
99      * @param notFinalizedValue value used together with the Android SDK ID to indicate that this
100      *   symbol has not been finalized at all
101      * @param shortExtensionNames short names of the SDK extensions in which this symbol has been
102      *   finalized; may be non-empty even if extensionsSince is `null`.
103      * @param extensionsSince the version of the SDK extensions in which this API was initially
104      *   introduced (same value for all SDK extensions), or `null` if this symbol has not been
105      *   finalized in any SDK extension (regardless of the [shortExtensionNames] argument)
106      * @return an `sdks` value suitable for including verbatim in XML
107      */
calculateSdksAttrnull108     fun calculateSdksAttr(
109         androidSince: ApiVersion,
110         notFinalizedValue: ApiVersion,
111         shortExtensionNames: List<String>,
112         extensionsSince: ExtVersion?,
113     ): String {
114         // Special case: symbol not finalized anywhere -> "ANDROID_SDK:next_dessert_int"
115         if (androidSince == notFinalizedValue && extensionsSince == null) {
116             return "$ANDROID_PLATFORM_SDK_ID:$notFinalizedValue"
117         }
118 
119         val versions = mutableSetOf<String>()
120         // Only include SDK extensions if the symbol has been finalized in at least one extension.
121         if (extensionsSince != null) {
122             for (shortExtensionName in shortExtensionNames) {
123                 val sdkExtension = availableSdkExtensions.retrieveSdkExtension(shortExtensionName)
124                 // Only add the extension version in which a symbol was added for those SDK
125                 // extensions that supersede the Android SDK version.
126                 if (sdkExtension.supersedesAndroidSdkVersion(androidSince)) {
127                     versions.add("${sdkExtension.id}:$extensionsSince")
128                 }
129             }
130         }
131 
132         // Only include the Android SDK in `sdks` if
133         // - the symbol has been finalized in an Android dessert, and
134         // - the symbol has been finalized in at least one SDK extension
135         if (androidSince != notFinalizedValue && versions.isNotEmpty()) {
136             versions.add("$ANDROID_PLATFORM_SDK_ID:$androidSince")
137         }
138         return versions.joinToString(",")
139     }
140 
141     companion object {
142         // Hard-coded ID for the Android platform SDK. Used identically as the extension SDK IDs
143         // to express when an API first appeared in an SDK.
144         const val ANDROID_PLATFORM_SDK_ID = 0
145     }
146 }
147 
<lambda>null148 internal fun Set<Node>.findNode(breadcrumb: String): Node? = find { it.breadcrumb == breadcrumb }
149 
Stringnull150 private fun String.toDotNotation(): String = split('(')[0].replace('/', '.')
151 
152 internal class Node(val breadcrumb: String) {
153     var extensions: List<String> = emptyList()
154     val children: MutableSet<Node> = mutableSetOf()
155 }
156 
157 /**
158  * Regular expression used to split an internal symbol name into separate breadcrumbs, i.e. values
159  * that will be used in [Node.breadcrumb].
160  */
161 private val REGEX_DELIMITERS = Regex("[.#$]")
162 
163 /**
164  * Split the string into breadcrumbs, i.e. values that will be used in [Node.breadcrumb].
165  *
166  * e.g. if this is com.example.Foo$Inner#method(I)I then this will split it into:
167  * * "com"
168  * * "example"
169  * * "Foo"
170  * * "Inner"
171  * * "method(I)I"
172  */
splitIntoBreadcrumbsnull173 internal fun String.splitIntoBreadcrumbs() = split(REGEX_DELIMITERS)
174