blob: b514048bd6c95acf4a3d1e32e713633013e5a3bf [file] [log] [blame]
Mårten Kongstad90087242024-04-16 09:55:56 +02001/*
2 * Copyright (C) 2024 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@file:JvmName("Main")
17
18package com.android.checkflaggedapis
19
Mårten Kongstad387ff6c2024-04-16 12:42:14 +020020import android.aconfig.Aconfig
Mårten Kongstad20de4052024-04-16 11:33:56 +020021import com.android.tools.metalava.model.BaseItemVisitor
Mårten Kongstad18ff19a2024-04-26 05:48:57 +020022import com.android.tools.metalava.model.ClassItem
Mårten Kongstad20de4052024-04-16 11:33:56 +020023import com.android.tools.metalava.model.FieldItem
Mårten Kongstad18ff19a2024-04-26 05:48:57 +020024import com.android.tools.metalava.model.Item
Mårten Kongstad40da9702024-04-27 01:42:51 +020025import com.android.tools.metalava.model.MethodItem
Mårten Kongstad20de4052024-04-16 11:33:56 +020026import com.android.tools.metalava.model.text.ApiFile
Mårten Kongstadacfeb112024-04-16 10:30:26 +020027import com.github.ajalt.clikt.core.CliktCommand
28import com.github.ajalt.clikt.core.ProgramResult
Mårten Kongstad20de4052024-04-16 11:33:56 +020029import com.github.ajalt.clikt.parameters.options.help
30import com.github.ajalt.clikt.parameters.options.option
31import com.github.ajalt.clikt.parameters.options.required
32import com.github.ajalt.clikt.parameters.types.path
33import java.io.InputStream
Mårten Kongstadb673d3b2024-04-16 18:34:20 +020034import javax.xml.parsers.DocumentBuilderFactory
35import org.w3c.dom.Node
Mårten Kongstadacfeb112024-04-16 10:30:26 +020036
Mårten Kongstade0179972024-04-16 11:16:44 +020037/**
38 * Class representing the fully qualified name of a class, method or field.
39 *
40 * This tool reads a multitude of input formats all of which represents the fully qualified path to
41 * a Java symbol slightly differently. To keep things consistent, all parsed APIs are converted to
42 * Symbols.
43 *
Mårten Kongstadece054c2024-05-02 09:45:11 +020044 * Symbols are encoded using the format similar to the one described in section 4.3.2 of the JVM
45 * spec [1], that is, "package.class.inner-class.method(int, int[], android.util.Clazz)" is
46 * represented as
Mårten Kongstade0179972024-04-16 11:16:44 +020047 * <pre>
Mårten Kongstadece054c2024-05-02 09:45:11 +020048 * package.class.inner-class.method(II[Landroid/util/Clazz;)
49 * <pre>
50 *
51 * Where possible, the format has been simplified (to make translation of the
52 * various input formats easier): for instance, only / is used as delimiter (#
53 * and $ are never used).
54 *
55 * 1. https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3.2
Mårten Kongstade0179972024-04-16 11:16:44 +020056 */
Mårten Kongstada1fe3712024-05-06 13:46:21 +020057internal sealed class Symbol {
Mårten Kongstade0179972024-04-16 11:16:44 +020058 companion object {
Mårten Kongstadece054c2024-05-02 09:45:11 +020059 private val FORBIDDEN_CHARS = listOf('#', '$', '.')
Mårten Kongstade0179972024-04-16 11:16:44 +020060
Mårten Kongstadc3f05a62024-05-06 21:42:15 +020061 fun createClass(clazz: String, superclass: String?, interfaces: Set<String>): Symbol {
62 return ClassSymbol(
63 toInternalFormat(clazz),
64 superclass?.let { toInternalFormat(it) },
65 interfaces.map { toInternalFormat(it) }.toSet())
Mårten Kongstada1fe3712024-05-06 13:46:21 +020066 }
67
68 fun createField(clazz: String, field: String): Symbol {
69 require(!field.contains("(") && !field.contains(")"))
70 return MemberSymbol(toInternalFormat(clazz), toInternalFormat(field))
71 }
72
73 fun createMethod(clazz: String, method: String): Symbol {
74 return MemberSymbol(toInternalFormat(clazz), toInternalFormat(method))
75 }
76
77 protected fun toInternalFormat(name: String): String {
78 var internalName = name
Mårten Kongstade0179972024-04-16 11:16:44 +020079 for (ch in FORBIDDEN_CHARS) {
Mårten Kongstada1fe3712024-05-06 13:46:21 +020080 internalName = internalName.replace(ch, '/')
Mårten Kongstade0179972024-04-16 11:16:44 +020081 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +020082 return internalName
Mårten Kongstade0179972024-04-16 11:16:44 +020083 }
84 }
85
Mårten Kongstada1fe3712024-05-06 13:46:21 +020086 abstract fun toPrettyString(): String
87}
Mårten Kongstade0179972024-04-16 11:16:44 +020088
Mårten Kongstadc3f05a62024-05-06 21:42:15 +020089internal data class ClassSymbol(
90 val clazz: String,
91 val superclass: String?,
92 val interfaces: Set<String>
93) : Symbol() {
Mårten Kongstada1fe3712024-05-06 13:46:21 +020094 override fun toPrettyString(): String = "$clazz"
95}
96
97internal data class MemberSymbol(val clazz: String, val member: String) : Symbol() {
98 override fun toPrettyString(): String = "$clazz/$member"
Mårten Kongstade0179972024-04-16 11:16:44 +020099}
100
Mårten Kongstaddc3fc2e2024-04-16 11:23:22 +0200101/**
102 * Class representing the fully qualified name of an aconfig flag.
103 *
104 * This includes both the flag's package and name, separated by a dot, e.g.:
105 * <pre>
106 * com.android.aconfig.test.disabled_ro
107 * <pre>
108 */
109@JvmInline
110internal value class Flag(val name: String) {
111 override fun toString(): String = name.toString()
112}
113
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200114internal sealed class ApiError {
115 abstract val symbol: Symbol
116 abstract val flag: Flag
117}
118
119internal data class EnabledFlaggedApiNotPresentError(
120 override val symbol: Symbol,
121 override val flag: Flag
122) : ApiError() {
123 override fun toString(): String {
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200124 return "error: enabled @FlaggedApi not present in built artifact: symbol=${symbol.toPrettyString()} flag=$flag"
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200125 }
126}
127
128internal data class DisabledFlaggedApiIsPresentError(
129 override val symbol: Symbol,
130 override val flag: Flag
131) : ApiError() {
132 override fun toString(): String {
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200133 return "error: disabled @FlaggedApi is present in built artifact: symbol=${symbol.toPrettyString()} flag=$flag"
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200134 }
135}
136
137internal data class UnknownFlagError(override val symbol: Symbol, override val flag: Flag) :
138 ApiError() {
139 override fun toString(): String {
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200140 return "error: unknown flag: symbol=${symbol.toPrettyString()} flag=$flag"
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200141 }
142}
143
Mårten Kongstad20de4052024-04-16 11:33:56 +0200144class CheckCommand :
145 CliktCommand(
146 help =
147 """
148Check that all flagged APIs are used in the correct way.
149
150This tool reads the API signature file and checks that all flagged APIs are used in the correct way.
151
152The tool will exit with a non-zero exit code if any flagged APIs are found to be used in the incorrect way.
153""") {
154 private val apiSignaturePath by
155 option("--api-signature")
156 .help(
157 """
158 Path to API signature file.
159 Usually named *current.txt.
160 Tip: `m frameworks-base-api-current.txt` will generate a file that includes all platform and mainline APIs.
161 """)
162 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
163 .required()
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200164 private val flagValuesPath by
165 option("--flag-values")
166 .help(
167 """
168 Path to aconfig parsed_flags binary proto file.
169 Tip: `m all_aconfig_declarations` will generate a file that includes all information about all flags.
170 """)
171 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
172 .required()
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200173 private val apiVersionsPath by
174 option("--api-versions")
175 .help(
176 """
177 Path to API versions XML file.
178 Usually named xml-versions.xml.
179 Tip: `m sdk dist` will generate a file that includes all platform and mainline APIs.
180 """)
181 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
182 .required()
Mårten Kongstad20de4052024-04-16 11:33:56 +0200183
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200184 override fun run() {
Mårten Kongstad20de4052024-04-16 11:33:56 +0200185 val flaggedSymbols =
186 apiSignaturePath.toFile().inputStream().use {
187 parseApiSignature(apiSignaturePath.toString(), it)
188 }
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200189 val flags = flagValuesPath.toFile().inputStream().use { parseFlagValues(it) }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200190 val exportedSymbols = apiVersionsPath.toFile().inputStream().use { parseApiVersions(it) }
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200191 val errors = findErrors(flaggedSymbols, flags, exportedSymbols)
192 for (e in errors) {
193 println(e)
194 }
195 throw ProgramResult(errors.size)
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200196 }
197}
198
Mårten Kongstad20de4052024-04-16 11:33:56 +0200199internal fun parseApiSignature(path: String, input: InputStream): Set<Pair<Symbol, Flag>> {
Mårten Kongstad20de4052024-04-16 11:33:56 +0200200 val output = mutableSetOf<Pair<Symbol, Flag>>()
201 val visitor =
202 object : BaseItemVisitor() {
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200203 override fun visitClass(cls: ClassItem) {
204 getFlagOrNull(cls)?.let { flag ->
Mårten Kongstad7c3571f2024-05-06 14:53:54 +0200205 val symbol =
206 Symbol.createClass(
207 cls.baselineElementId(),
Mårten Kongstadc3f05a62024-05-06 21:42:15 +0200208 cls.superClass()?.baselineElementId(),
Mårten Kongstad04d8b462024-05-06 16:26:40 +0200209 cls.allInterfaces()
210 .map { it.baselineElementId() }
211 .filter { it != cls.baselineElementId() }
212 .toSet())
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200213 output.add(Pair(symbol, flag))
Mårten Kongstad20de4052024-04-16 11:33:56 +0200214 }
215 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200216
217 override fun visitField(field: FieldItem) {
218 getFlagOrNull(field)?.let { flag ->
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200219 val symbol =
220 Symbol.createField(field.containingClass().baselineElementId(), field.name())
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200221 output.add(Pair(symbol, flag))
222 }
223 }
224
Mårten Kongstad40da9702024-04-27 01:42:51 +0200225 override fun visitMethod(method: MethodItem) {
226 getFlagOrNull(method)?.let { flag ->
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200227 val methodName = buildString {
Mårten Kongstad40da9702024-04-27 01:42:51 +0200228 append(method.name())
229 append("(")
Mårten Kongstadb4a14bf2024-04-28 00:21:11 +0200230 method.parameters().joinTo(this, separator = "") { it.type().internalName() }
Mårten Kongstad40da9702024-04-27 01:42:51 +0200231 append(")")
232 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200233 val symbol = Symbol.createMethod(method.containingClass().qualifiedName(), methodName)
Mårten Kongstad40da9702024-04-27 01:42:51 +0200234 output.add(Pair(symbol, flag))
235 }
236 }
237
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200238 private fun getFlagOrNull(item: Item): Flag? {
239 return item.modifiers
240 .findAnnotation("android.annotation.FlaggedApi")
241 ?.findAttribute("value")
242 ?.value
243 ?.let { Flag(it.value() as String) }
244 }
Mårten Kongstad20de4052024-04-16 11:33:56 +0200245 }
246 val codebase = ApiFile.parseApi(path, input)
247 codebase.accept(visitor)
248 return output
249}
250
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200251internal fun parseFlagValues(input: InputStream): Map<Flag, Boolean> {
252 val parsedFlags = Aconfig.parsed_flags.parseFrom(input).getParsedFlagList()
253 return parsedFlags.associateBy(
254 { Flag("${it.getPackage()}.${it.getName()}") },
255 { it.getState() == Aconfig.flag_state.ENABLED })
256}
257
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200258internal fun parseApiVersions(input: InputStream): Set<Symbol> {
259 fun Node.getAttribute(name: String): String? = getAttributes()?.getNamedItem(name)?.getNodeValue()
260
261 val output = mutableSetOf<Symbol>()
262 val factory = DocumentBuilderFactory.newInstance()
263 val parser = factory.newDocumentBuilder()
264 val document = parser.parse(input)
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200265
266 val classes = document.getElementsByTagName("class")
267 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
268 for (i in 0.rangeUntil(classes.getLength())) {
269 val cls = classes.item(i)
270 val className =
271 requireNotNull(cls.getAttribute("name")) {
272 "Bad XML: <class> element without name attribute"
273 }
Mårten Kongstadc3f05a62024-05-06 21:42:15 +0200274 var superclass: String? = null
Mårten Kongstad7c3571f2024-05-06 14:53:54 +0200275 val interfaces = mutableSetOf<String>()
276 val children = cls.getChildNodes()
277 for (j in 0.rangeUntil(children.getLength())) {
278 val child = children.item(j)
Mårten Kongstadc3f05a62024-05-06 21:42:15 +0200279 when (child.getNodeName()) {
280 "extends" -> {
281 superclass =
282 requireNotNull(child.getAttribute("name")) {
283 "Bad XML: <extends> element without name attribute"
284 }
285 }
286 "implements" -> {
287 val interfaceName =
288 requireNotNull(child.getAttribute("name")) {
289 "Bad XML: <implements> element without name attribute"
290 }
291 interfaces.add(interfaceName)
292 }
Mårten Kongstad7c3571f2024-05-06 14:53:54 +0200293 }
294 }
Mårten Kongstadc3f05a62024-05-06 21:42:15 +0200295 output.add(Symbol.createClass(className, superclass, interfaces))
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200296 }
297
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200298 val fields = document.getElementsByTagName("field")
299 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
300 for (i in 0.rangeUntil(fields.getLength())) {
301 val field = fields.item(i)
Mårten Kongstad04e45642024-04-26 05:39:03 +0200302 val fieldName =
303 requireNotNull(field.getAttribute("name")) {
304 "Bad XML: <field> element without name attribute"
305 }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200306 val className =
Mårten Kongstadece054c2024-05-02 09:45:11 +0200307 requireNotNull(field.getParentNode()?.getAttribute("name")) {
308 "Bad XML: top level <field> element"
309 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200310 output.add(Symbol.createField(className, fieldName))
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200311 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200312
Mårten Kongstad40da9702024-04-27 01:42:51 +0200313 val methods = document.getElementsByTagName("method")
314 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
315 for (i in 0.rangeUntil(methods.getLength())) {
316 val method = methods.item(i)
317 val methodSignature =
318 requireNotNull(method.getAttribute("name")) {
319 "Bad XML: <method> element without name attribute"
320 }
321 val methodSignatureParts = methodSignature.split(Regex("\\(|\\)"))
322 if (methodSignatureParts.size != 3) {
Mårten Kongstad9aef0d92024-04-29 10:25:34 +0200323 throw Exception("Bad XML: method signature '$methodSignature'")
Mårten Kongstad40da9702024-04-27 01:42:51 +0200324 }
Mårten Kongstadcd93aeb2024-05-02 10:19:18 +0200325 var (methodName, methodArgs, _) = methodSignatureParts
Mårten Kongstad40da9702024-04-27 01:42:51 +0200326 val packageAndClassName =
327 requireNotNull(method.getParentNode()?.getAttribute("name")) {
Mårten Kongstad02525a82024-05-06 10:28:02 +0200328 "Bad XML: top level <method> element, or <class> element missing name attribute"
329 }
330 .replace("$", "/")
Mårten Kongstad40da9702024-04-27 01:42:51 +0200331 if (methodName == "<init>") {
332 methodName = packageAndClassName.split("/").last()
333 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200334 output.add(Symbol.createMethod(packageAndClassName, "$methodName($methodArgs)"))
Mårten Kongstad40da9702024-04-27 01:42:51 +0200335 }
336
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200337 return output
338}
339
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200340/**
341 * Find errors in the given data.
342 *
343 * @param flaggedSymbolsInSource the set of symbols that are flagged in the source code
344 * @param flags the set of flags and their values
345 * @param symbolsInOutput the set of symbols that are present in the output
346 * @return the set of errors found
347 */
348internal fun findErrors(
349 flaggedSymbolsInSource: Set<Pair<Symbol, Flag>>,
350 flags: Map<Flag, Boolean>,
351 symbolsInOutput: Set<Symbol>
352): Set<ApiError> {
Mårten Kongstadd2c70762024-05-06 14:58:18 +0200353 fun Set<Symbol>.containsSymbol(symbol: Symbol): Boolean {
354 // trivial case: the symbol is explicitly listed in api-versions.xml
355 if (contains(symbol)) {
356 return true
357 }
358
359 // non-trivial case: the symbol could be part of the surrounding class'
360 // super class or interfaces
361 val (className, memberName) =
362 when (symbol) {
363 is ClassSymbol -> return false
364 is MemberSymbol -> {
365 Pair(symbol.clazz, symbol.member)
366 }
367 }
368 val clazz = find { it is ClassSymbol && it.clazz == className } as? ClassSymbol?
369 if (clazz == null) {
370 return false
371 }
372
373 for (interfaceName in clazz.interfaces) {
374 // createMethod is the same as createField, except it allows parenthesis
375 val interfaceSymbol = Symbol.createMethod(interfaceName, memberName)
376 if (contains(interfaceSymbol)) {
377 return true
378 }
379 }
380
Mårten Kongstade8120392024-05-06 21:32:34 +0200381 if (clazz.superclass != null) {
382 val superclassSymbol = Symbol.createMethod(clazz.superclass, memberName)
383 return containsSymbol(superclassSymbol)
384 }
385
Mårten Kongstadd2c70762024-05-06 14:58:18 +0200386 return false
387 }
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200388 val errors = mutableSetOf<ApiError>()
389 for ((symbol, flag) in flaggedSymbolsInSource) {
390 try {
391 if (flags.getValue(flag)) {
Mårten Kongstadd2c70762024-05-06 14:58:18 +0200392 if (!symbolsInOutput.containsSymbol(symbol)) {
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200393 errors.add(EnabledFlaggedApiNotPresentError(symbol, flag))
394 }
395 } else {
Mårten Kongstadd2c70762024-05-06 14:58:18 +0200396 if (symbolsInOutput.containsSymbol(symbol)) {
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200397 errors.add(DisabledFlaggedApiIsPresentError(symbol, flag))
398 }
399 }
400 } catch (e: NoSuchElementException) {
401 errors.add(UnknownFlagError(symbol, flag))
402 }
403 }
404 return errors
405}
406
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200407fun main(args: Array<String>) = CheckCommand().main(args)