blob: 1d2440dee8e580a4824f3d2634f872390127333e [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 Kongstadaa41dac2024-05-22 15:13:54 +0200208 if (cls.isInterface()) {
209 "java/lang/Object"
210 } else {
211 cls.superClass()?.baselineElementId()
212 },
Mårten Kongstad04d8b462024-05-06 16:26:40 +0200213 cls.allInterfaces()
214 .map { it.baselineElementId() }
215 .filter { it != cls.baselineElementId() }
216 .toSet())
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200217 output.add(Pair(symbol, flag))
Mårten Kongstad20de4052024-04-16 11:33:56 +0200218 }
219 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200220
221 override fun visitField(field: FieldItem) {
222 getFlagOrNull(field)?.let { flag ->
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200223 val symbol =
224 Symbol.createField(field.containingClass().baselineElementId(), field.name())
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200225 output.add(Pair(symbol, flag))
226 }
227 }
228
Mårten Kongstad40da9702024-04-27 01:42:51 +0200229 override fun visitMethod(method: MethodItem) {
230 getFlagOrNull(method)?.let { flag ->
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200231 val methodName = buildString {
Mårten Kongstad40da9702024-04-27 01:42:51 +0200232 append(method.name())
233 append("(")
Mårten Kongstadb4a14bf2024-04-28 00:21:11 +0200234 method.parameters().joinTo(this, separator = "") { it.type().internalName() }
Mårten Kongstad40da9702024-04-27 01:42:51 +0200235 append(")")
236 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200237 val symbol = Symbol.createMethod(method.containingClass().qualifiedName(), methodName)
Mårten Kongstad40da9702024-04-27 01:42:51 +0200238 output.add(Pair(symbol, flag))
239 }
240 }
241
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200242 private fun getFlagOrNull(item: Item): Flag? {
243 return item.modifiers
244 .findAnnotation("android.annotation.FlaggedApi")
245 ?.findAttribute("value")
246 ?.value
247 ?.let { Flag(it.value() as String) }
248 }
Mårten Kongstad20de4052024-04-16 11:33:56 +0200249 }
250 val codebase = ApiFile.parseApi(path, input)
251 codebase.accept(visitor)
252 return output
253}
254
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200255internal fun parseFlagValues(input: InputStream): Map<Flag, Boolean> {
256 val parsedFlags = Aconfig.parsed_flags.parseFrom(input).getParsedFlagList()
257 return parsedFlags.associateBy(
258 { Flag("${it.getPackage()}.${it.getName()}") },
259 { it.getState() == Aconfig.flag_state.ENABLED })
260}
261
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200262internal fun parseApiVersions(input: InputStream): Set<Symbol> {
263 fun Node.getAttribute(name: String): String? = getAttributes()?.getNamedItem(name)?.getNodeValue()
264
265 val output = mutableSetOf<Symbol>()
266 val factory = DocumentBuilderFactory.newInstance()
267 val parser = factory.newDocumentBuilder()
268 val document = parser.parse(input)
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200269
270 val classes = document.getElementsByTagName("class")
271 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
272 for (i in 0.rangeUntil(classes.getLength())) {
273 val cls = classes.item(i)
274 val className =
275 requireNotNull(cls.getAttribute("name")) {
276 "Bad XML: <class> element without name attribute"
277 }
Mårten Kongstadc3f05a62024-05-06 21:42:15 +0200278 var superclass: String? = null
Mårten Kongstad7c3571f2024-05-06 14:53:54 +0200279 val interfaces = mutableSetOf<String>()
280 val children = cls.getChildNodes()
281 for (j in 0.rangeUntil(children.getLength())) {
282 val child = children.item(j)
Mårten Kongstadc3f05a62024-05-06 21:42:15 +0200283 when (child.getNodeName()) {
284 "extends" -> {
285 superclass =
286 requireNotNull(child.getAttribute("name")) {
287 "Bad XML: <extends> element without name attribute"
288 }
289 }
290 "implements" -> {
291 val interfaceName =
292 requireNotNull(child.getAttribute("name")) {
293 "Bad XML: <implements> element without name attribute"
294 }
295 interfaces.add(interfaceName)
296 }
Mårten Kongstad7c3571f2024-05-06 14:53:54 +0200297 }
298 }
Mårten Kongstadc3f05a62024-05-06 21:42:15 +0200299 output.add(Symbol.createClass(className, superclass, interfaces))
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200300 }
301
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200302 val fields = document.getElementsByTagName("field")
303 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
304 for (i in 0.rangeUntil(fields.getLength())) {
305 val field = fields.item(i)
Mårten Kongstad04e45642024-04-26 05:39:03 +0200306 val fieldName =
307 requireNotNull(field.getAttribute("name")) {
308 "Bad XML: <field> element without name attribute"
309 }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200310 val className =
Mårten Kongstadece054c2024-05-02 09:45:11 +0200311 requireNotNull(field.getParentNode()?.getAttribute("name")) {
312 "Bad XML: top level <field> element"
313 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200314 output.add(Symbol.createField(className, fieldName))
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200315 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200316
Mårten Kongstad40da9702024-04-27 01:42:51 +0200317 val methods = document.getElementsByTagName("method")
318 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
319 for (i in 0.rangeUntil(methods.getLength())) {
320 val method = methods.item(i)
321 val methodSignature =
322 requireNotNull(method.getAttribute("name")) {
323 "Bad XML: <method> element without name attribute"
324 }
325 val methodSignatureParts = methodSignature.split(Regex("\\(|\\)"))
326 if (methodSignatureParts.size != 3) {
Mårten Kongstad9aef0d92024-04-29 10:25:34 +0200327 throw Exception("Bad XML: method signature '$methodSignature'")
Mårten Kongstad40da9702024-04-27 01:42:51 +0200328 }
Mårten Kongstadcd93aeb2024-05-02 10:19:18 +0200329 var (methodName, methodArgs, _) = methodSignatureParts
Mårten Kongstad40da9702024-04-27 01:42:51 +0200330 val packageAndClassName =
331 requireNotNull(method.getParentNode()?.getAttribute("name")) {
Mårten Kongstad02525a82024-05-06 10:28:02 +0200332 "Bad XML: top level <method> element, or <class> element missing name attribute"
333 }
334 .replace("$", "/")
Mårten Kongstad40da9702024-04-27 01:42:51 +0200335 if (methodName == "<init>") {
336 methodName = packageAndClassName.split("/").last()
337 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200338 output.add(Symbol.createMethod(packageAndClassName, "$methodName($methodArgs)"))
Mårten Kongstad40da9702024-04-27 01:42:51 +0200339 }
340
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200341 return output
342}
343
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200344/**
345 * Find errors in the given data.
346 *
347 * @param flaggedSymbolsInSource the set of symbols that are flagged in the source code
348 * @param flags the set of flags and their values
349 * @param symbolsInOutput the set of symbols that are present in the output
350 * @return the set of errors found
351 */
352internal fun findErrors(
353 flaggedSymbolsInSource: Set<Pair<Symbol, Flag>>,
354 flags: Map<Flag, Boolean>,
355 symbolsInOutput: Set<Symbol>
356): Set<ApiError> {
Mårten Kongstadd2c70762024-05-06 14:58:18 +0200357 fun Set<Symbol>.containsSymbol(symbol: Symbol): Boolean {
358 // trivial case: the symbol is explicitly listed in api-versions.xml
359 if (contains(symbol)) {
360 return true
361 }
362
363 // non-trivial case: the symbol could be part of the surrounding class'
364 // super class or interfaces
365 val (className, memberName) =
366 when (symbol) {
367 is ClassSymbol -> return false
368 is MemberSymbol -> {
369 Pair(symbol.clazz, symbol.member)
370 }
371 }
372 val clazz = find { it is ClassSymbol && it.clazz == className } as? ClassSymbol?
373 if (clazz == null) {
374 return false
375 }
376
377 for (interfaceName in clazz.interfaces) {
378 // createMethod is the same as createField, except it allows parenthesis
379 val interfaceSymbol = Symbol.createMethod(interfaceName, memberName)
380 if (contains(interfaceSymbol)) {
381 return true
382 }
383 }
384
Mårten Kongstade8120392024-05-06 21:32:34 +0200385 if (clazz.superclass != null) {
386 val superclassSymbol = Symbol.createMethod(clazz.superclass, memberName)
387 return containsSymbol(superclassSymbol)
388 }
389
Mårten Kongstadd2c70762024-05-06 14:58:18 +0200390 return false
391 }
Mårten Kongstad0d44e722024-05-08 10:00:32 +0200392
393 /**
394 * Returns whether the given flag is enabled for the given symbol.
395 *
396 * A flagged member inside a flagged class is ignored (and the flag value considered disabled) if
397 * the class' flag is disabled.
398 *
399 * @param symbol the symbol to check
400 * @param flag the flag to check
401 * @return whether the flag is enabled for the given symbol
402 */
403 fun isFlagEnabledForSymbol(symbol: Symbol, flag: Flag): Boolean {
404 when (symbol) {
405 is ClassSymbol -> return flags.getValue(flag)
406 is MemberSymbol -> {
407 val memberFlagValue = flags.getValue(flag)
408 if (!memberFlagValue) {
409 return false
410 }
411 // Special case: if the MemberSymbol's flag is enabled, but the outer
412 // ClassSymbol's flag (if the class is flagged) is disabled, consider
413 // the MemberSymbol's flag as disabled:
414 //
415 // @FlaggedApi(this-flag-is-disabled) Clazz {
416 // @FlaggedApi(this-flag-is-enabled) method(); // The Clazz' flag "wins"
417 // }
418 //
419 // Note: the current implementation does not handle nested classes.
420 val classFlagValue =
421 flaggedSymbolsInSource
422 .find { it.first.toPrettyString() == symbol.clazz }
423 ?.let { flags.getValue(it.second) }
424 ?: true
425 return classFlagValue
426 }
427 }
428 }
429
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200430 val errors = mutableSetOf<ApiError>()
431 for ((symbol, flag) in flaggedSymbolsInSource) {
432 try {
Mårten Kongstad0d44e722024-05-08 10:00:32 +0200433 if (isFlagEnabledForSymbol(symbol, flag)) {
Mårten Kongstadd2c70762024-05-06 14:58:18 +0200434 if (!symbolsInOutput.containsSymbol(symbol)) {
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200435 errors.add(EnabledFlaggedApiNotPresentError(symbol, flag))
436 }
437 } else {
Mårten Kongstadd2c70762024-05-06 14:58:18 +0200438 if (symbolsInOutput.containsSymbol(symbol)) {
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200439 errors.add(DisabledFlaggedApiIsPresentError(symbol, flag))
440 }
441 }
442 } catch (e: NoSuchElementException) {
443 errors.add(UnknownFlagError(symbol, flag))
444 }
445 }
446 return errors
447}
448
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200449fun main(args: Array<String>) = CheckCommand().main(args)