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