blob: 26d3c9c40b2a675a16f3a53c1401ea098f85b557 [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 Kongstadd5ce20f2024-06-10 13:59:46 +020029import com.github.ajalt.clikt.core.subcommands
Mårten Kongstad20de4052024-04-16 11:33:56 +020030import com.github.ajalt.clikt.parameters.options.help
31import com.github.ajalt.clikt.parameters.options.option
32import com.github.ajalt.clikt.parameters.options.required
33import com.github.ajalt.clikt.parameters.types.path
34import java.io.InputStream
Mårten Kongstadb673d3b2024-04-16 18:34:20 +020035import javax.xml.parsers.DocumentBuilderFactory
36import org.w3c.dom.Node
Mårten Kongstadacfeb112024-04-16 10:30:26 +020037
Mårten Kongstade0179972024-04-16 11:16:44 +020038/**
39 * Class representing the fully qualified name of a class, method or field.
40 *
41 * This tool reads a multitude of input formats all of which represents the fully qualified path to
42 * a Java symbol slightly differently. To keep things consistent, all parsed APIs are converted to
43 * Symbols.
44 *
Mårten Kongstadece054c2024-05-02 09:45:11 +020045 * Symbols are encoded using the format similar to the one described in section 4.3.2 of the JVM
46 * spec [1], that is, "package.class.inner-class.method(int, int[], android.util.Clazz)" is
47 * represented as
Mårten Kongstade0179972024-04-16 11:16:44 +020048 * <pre>
Mårten Kongstadece054c2024-05-02 09:45:11 +020049 * package.class.inner-class.method(II[Landroid/util/Clazz;)
50 * <pre>
51 *
52 * Where possible, the format has been simplified (to make translation of the
53 * various input formats easier): for instance, only / is used as delimiter (#
54 * and $ are never used).
55 *
56 * 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 +020057 */
Mårten Kongstada1fe3712024-05-06 13:46:21 +020058internal sealed class Symbol {
Mårten Kongstade0179972024-04-16 11:16:44 +020059 companion object {
Mårten Kongstadece054c2024-05-02 09:45:11 +020060 private val FORBIDDEN_CHARS = listOf('#', '$', '.')
Mårten Kongstade0179972024-04-16 11:16:44 +020061
Mårten Kongstadc3f05a62024-05-06 21:42:15 +020062 fun createClass(clazz: String, superclass: String?, interfaces: Set<String>): Symbol {
63 return ClassSymbol(
64 toInternalFormat(clazz),
65 superclass?.let { toInternalFormat(it) },
66 interfaces.map { toInternalFormat(it) }.toSet())
Mårten Kongstada1fe3712024-05-06 13:46:21 +020067 }
68
69 fun createField(clazz: String, field: String): Symbol {
70 require(!field.contains("(") && !field.contains(")"))
71 return MemberSymbol(toInternalFormat(clazz), toInternalFormat(field))
72 }
73
74 fun createMethod(clazz: String, method: String): Symbol {
75 return MemberSymbol(toInternalFormat(clazz), toInternalFormat(method))
76 }
77
78 protected fun toInternalFormat(name: String): String {
79 var internalName = name
Mårten Kongstade0179972024-04-16 11:16:44 +020080 for (ch in FORBIDDEN_CHARS) {
Mårten Kongstada1fe3712024-05-06 13:46:21 +020081 internalName = internalName.replace(ch, '/')
Mårten Kongstade0179972024-04-16 11:16:44 +020082 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +020083 return internalName
Mårten Kongstade0179972024-04-16 11:16:44 +020084 }
85 }
86
Mårten Kongstada1fe3712024-05-06 13:46:21 +020087 abstract fun toPrettyString(): String
88}
Mårten Kongstade0179972024-04-16 11:16:44 +020089
Mårten Kongstadc3f05a62024-05-06 21:42:15 +020090internal data class ClassSymbol(
91 val clazz: String,
92 val superclass: String?,
93 val interfaces: Set<String>
94) : Symbol() {
Mårten Kongstada1fe3712024-05-06 13:46:21 +020095 override fun toPrettyString(): String = "$clazz"
96}
97
98internal data class MemberSymbol(val clazz: String, val member: String) : Symbol() {
99 override fun toPrettyString(): String = "$clazz/$member"
Mårten Kongstade0179972024-04-16 11:16:44 +0200100}
101
Mårten Kongstaddc3fc2e2024-04-16 11:23:22 +0200102/**
103 * Class representing the fully qualified name of an aconfig flag.
104 *
105 * This includes both the flag's package and name, separated by a dot, e.g.:
106 * <pre>
107 * com.android.aconfig.test.disabled_ro
108 * <pre>
109 */
110@JvmInline
111internal value class Flag(val name: String) {
112 override fun toString(): String = name.toString()
113}
114
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200115internal sealed class ApiError {
116 abstract val symbol: Symbol
117 abstract val flag: Flag
118}
119
120internal data class EnabledFlaggedApiNotPresentError(
121 override val symbol: Symbol,
122 override val flag: Flag
123) : ApiError() {
124 override fun toString(): String {
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200125 return "error: enabled @FlaggedApi not present in built artifact: symbol=${symbol.toPrettyString()} flag=$flag"
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200126 }
127}
128
129internal data class DisabledFlaggedApiIsPresentError(
130 override val symbol: Symbol,
131 override val flag: Flag
132) : ApiError() {
133 override fun toString(): String {
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200134 return "error: disabled @FlaggedApi is present in built artifact: symbol=${symbol.toPrettyString()} flag=$flag"
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200135 }
136}
137
138internal data class UnknownFlagError(override val symbol: Symbol, override val flag: Flag) :
139 ApiError() {
140 override fun toString(): String {
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200141 return "error: unknown flag: symbol=${symbol.toPrettyString()} flag=$flag"
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200142 }
143}
144
Mårten Kongstadd5ce20f2024-06-10 13:59:46 +0200145class MainCommand : CliktCommand() {
146 override fun run() {}
147}
148
Mårten Kongstad20de4052024-04-16 11:33:56 +0200149class CheckCommand :
150 CliktCommand(
151 help =
152 """
153Check that all flagged APIs are used in the correct way.
154
155This tool reads the API signature file and checks that all flagged APIs are used in the correct way.
156
157The tool will exit with a non-zero exit code if any flagged APIs are found to be used in the incorrect way.
158""") {
159 private val apiSignaturePath by
160 option("--api-signature")
161 .help(
162 """
163 Path to API signature file.
164 Usually named *current.txt.
165 Tip: `m frameworks-base-api-current.txt` will generate a file that includes all platform and mainline APIs.
166 """)
167 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
168 .required()
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200169 private val flagValuesPath by
170 option("--flag-values")
171 .help(
172 """
173 Path to aconfig parsed_flags binary proto file.
174 Tip: `m all_aconfig_declarations` will generate a file that includes all information about all flags.
175 """)
176 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
177 .required()
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200178 private val apiVersionsPath by
179 option("--api-versions")
180 .help(
181 """
182 Path to API versions XML file.
183 Usually named xml-versions.xml.
184 Tip: `m sdk dist` will generate a file that includes all platform and mainline APIs.
185 """)
186 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
187 .required()
Mårten Kongstad20de4052024-04-16 11:33:56 +0200188
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200189 override fun run() {
Mårten Kongstad20de4052024-04-16 11:33:56 +0200190 val flaggedSymbols =
191 apiSignaturePath.toFile().inputStream().use {
192 parseApiSignature(apiSignaturePath.toString(), it)
193 }
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200194 val flags = flagValuesPath.toFile().inputStream().use { parseFlagValues(it) }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200195 val exportedSymbols = apiVersionsPath.toFile().inputStream().use { parseApiVersions(it) }
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200196 val errors = findErrors(flaggedSymbols, flags, exportedSymbols)
197 for (e in errors) {
198 println(e)
199 }
200 throw ProgramResult(errors.size)
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200201 }
202}
203
Mårten Kongstad20de4052024-04-16 11:33:56 +0200204internal fun parseApiSignature(path: String, input: InputStream): Set<Pair<Symbol, Flag>> {
Mårten Kongstad20de4052024-04-16 11:33:56 +0200205 val output = mutableSetOf<Pair<Symbol, Flag>>()
206 val visitor =
207 object : BaseItemVisitor() {
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200208 override fun visitClass(cls: ClassItem) {
209 getFlagOrNull(cls)?.let { flag ->
Mårten Kongstad7c3571f2024-05-06 14:53:54 +0200210 val symbol =
211 Symbol.createClass(
212 cls.baselineElementId(),
Mårten Kongstadaa41dac2024-05-22 15:13:54 +0200213 if (cls.isInterface()) {
214 "java/lang/Object"
215 } else {
216 cls.superClass()?.baselineElementId()
217 },
Mårten Kongstad04d8b462024-05-06 16:26:40 +0200218 cls.allInterfaces()
219 .map { it.baselineElementId() }
220 .filter { it != cls.baselineElementId() }
221 .toSet())
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200222 output.add(Pair(symbol, flag))
Mårten Kongstad20de4052024-04-16 11:33:56 +0200223 }
224 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200225
226 override fun visitField(field: FieldItem) {
227 getFlagOrNull(field)?.let { flag ->
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200228 val symbol =
229 Symbol.createField(field.containingClass().baselineElementId(), field.name())
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200230 output.add(Pair(symbol, flag))
231 }
232 }
233
Mårten Kongstad40da9702024-04-27 01:42:51 +0200234 override fun visitMethod(method: MethodItem) {
235 getFlagOrNull(method)?.let { flag ->
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200236 val methodName = buildString {
Mårten Kongstad40da9702024-04-27 01:42:51 +0200237 append(method.name())
238 append("(")
Mårten Kongstadb4a14bf2024-04-28 00:21:11 +0200239 method.parameters().joinTo(this, separator = "") { it.type().internalName() }
Mårten Kongstad40da9702024-04-27 01:42:51 +0200240 append(")")
241 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200242 val symbol = Symbol.createMethod(method.containingClass().qualifiedName(), methodName)
Mårten Kongstad40da9702024-04-27 01:42:51 +0200243 output.add(Pair(symbol, flag))
244 }
245 }
246
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200247 private fun getFlagOrNull(item: Item): Flag? {
248 return item.modifiers
249 .findAnnotation("android.annotation.FlaggedApi")
250 ?.findAttribute("value")
251 ?.value
252 ?.let { Flag(it.value() as String) }
253 }
Mårten Kongstad20de4052024-04-16 11:33:56 +0200254 }
255 val codebase = ApiFile.parseApi(path, input)
256 codebase.accept(visitor)
257 return output
258}
259
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200260internal fun parseFlagValues(input: InputStream): Map<Flag, Boolean> {
261 val parsedFlags = Aconfig.parsed_flags.parseFrom(input).getParsedFlagList()
262 return parsedFlags.associateBy(
263 { Flag("${it.getPackage()}.${it.getName()}") },
264 { it.getState() == Aconfig.flag_state.ENABLED })
265}
266
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200267internal fun parseApiVersions(input: InputStream): Set<Symbol> {
268 fun Node.getAttribute(name: String): String? = getAttributes()?.getNamedItem(name)?.getNodeValue()
269
270 val output = mutableSetOf<Symbol>()
271 val factory = DocumentBuilderFactory.newInstance()
272 val parser = factory.newDocumentBuilder()
273 val document = parser.parse(input)
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200274
275 val classes = document.getElementsByTagName("class")
276 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
277 for (i in 0.rangeUntil(classes.getLength())) {
278 val cls = classes.item(i)
279 val className =
280 requireNotNull(cls.getAttribute("name")) {
281 "Bad XML: <class> element without name attribute"
282 }
Mårten Kongstadc3f05a62024-05-06 21:42:15 +0200283 var superclass: String? = null
Mårten Kongstad7c3571f2024-05-06 14:53:54 +0200284 val interfaces = mutableSetOf<String>()
285 val children = cls.getChildNodes()
286 for (j in 0.rangeUntil(children.getLength())) {
287 val child = children.item(j)
Mårten Kongstadc3f05a62024-05-06 21:42:15 +0200288 when (child.getNodeName()) {
289 "extends" -> {
290 superclass =
291 requireNotNull(child.getAttribute("name")) {
292 "Bad XML: <extends> element without name attribute"
293 }
294 }
295 "implements" -> {
296 val interfaceName =
297 requireNotNull(child.getAttribute("name")) {
298 "Bad XML: <implements> element without name attribute"
299 }
300 interfaces.add(interfaceName)
301 }
Mårten Kongstad7c3571f2024-05-06 14:53:54 +0200302 }
303 }
Mårten Kongstadc3f05a62024-05-06 21:42:15 +0200304 output.add(Symbol.createClass(className, superclass, interfaces))
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200305 }
306
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200307 val fields = document.getElementsByTagName("field")
308 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
309 for (i in 0.rangeUntil(fields.getLength())) {
310 val field = fields.item(i)
Mårten Kongstad04e45642024-04-26 05:39:03 +0200311 val fieldName =
312 requireNotNull(field.getAttribute("name")) {
313 "Bad XML: <field> element without name attribute"
314 }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200315 val className =
Mårten Kongstadece054c2024-05-02 09:45:11 +0200316 requireNotNull(field.getParentNode()?.getAttribute("name")) {
317 "Bad XML: top level <field> element"
318 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200319 output.add(Symbol.createField(className, fieldName))
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200320 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200321
Mårten Kongstad40da9702024-04-27 01:42:51 +0200322 val methods = document.getElementsByTagName("method")
323 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
324 for (i in 0.rangeUntil(methods.getLength())) {
325 val method = methods.item(i)
326 val methodSignature =
327 requireNotNull(method.getAttribute("name")) {
328 "Bad XML: <method> element without name attribute"
329 }
330 val methodSignatureParts = methodSignature.split(Regex("\\(|\\)"))
331 if (methodSignatureParts.size != 3) {
Mårten Kongstad9aef0d92024-04-29 10:25:34 +0200332 throw Exception("Bad XML: method signature '$methodSignature'")
Mårten Kongstad40da9702024-04-27 01:42:51 +0200333 }
Mårten Kongstadcd93aeb2024-05-02 10:19:18 +0200334 var (methodName, methodArgs, _) = methodSignatureParts
Mårten Kongstad40da9702024-04-27 01:42:51 +0200335 val packageAndClassName =
336 requireNotNull(method.getParentNode()?.getAttribute("name")) {
Mårten Kongstad02525a82024-05-06 10:28:02 +0200337 "Bad XML: top level <method> element, or <class> element missing name attribute"
338 }
339 .replace("$", "/")
Mårten Kongstad40da9702024-04-27 01:42:51 +0200340 if (methodName == "<init>") {
341 methodName = packageAndClassName.split("/").last()
342 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200343 output.add(Symbol.createMethod(packageAndClassName, "$methodName($methodArgs)"))
Mårten Kongstad40da9702024-04-27 01:42:51 +0200344 }
345
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200346 return output
347}
348
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200349/**
350 * Find errors in the given data.
351 *
352 * @param flaggedSymbolsInSource the set of symbols that are flagged in the source code
353 * @param flags the set of flags and their values
354 * @param symbolsInOutput the set of symbols that are present in the output
355 * @return the set of errors found
356 */
357internal fun findErrors(
358 flaggedSymbolsInSource: Set<Pair<Symbol, Flag>>,
359 flags: Map<Flag, Boolean>,
360 symbolsInOutput: Set<Symbol>
361): Set<ApiError> {
Mårten Kongstadd2c70762024-05-06 14:58:18 +0200362 fun Set<Symbol>.containsSymbol(symbol: Symbol): Boolean {
363 // trivial case: the symbol is explicitly listed in api-versions.xml
364 if (contains(symbol)) {
365 return true
366 }
367
368 // non-trivial case: the symbol could be part of the surrounding class'
369 // super class or interfaces
370 val (className, memberName) =
371 when (symbol) {
372 is ClassSymbol -> return false
373 is MemberSymbol -> {
374 Pair(symbol.clazz, symbol.member)
375 }
376 }
377 val clazz = find { it is ClassSymbol && it.clazz == className } as? ClassSymbol?
378 if (clazz == null) {
379 return false
380 }
381
382 for (interfaceName in clazz.interfaces) {
383 // createMethod is the same as createField, except it allows parenthesis
384 val interfaceSymbol = Symbol.createMethod(interfaceName, memberName)
385 if (contains(interfaceSymbol)) {
386 return true
387 }
388 }
389
Mårten Kongstade8120392024-05-06 21:32:34 +0200390 if (clazz.superclass != null) {
391 val superclassSymbol = Symbol.createMethod(clazz.superclass, memberName)
392 return containsSymbol(superclassSymbol)
393 }
394
Mårten Kongstadd2c70762024-05-06 14:58:18 +0200395 return false
396 }
Mårten Kongstad0d44e722024-05-08 10:00:32 +0200397
398 /**
399 * Returns whether the given flag is enabled for the given symbol.
400 *
401 * A flagged member inside a flagged class is ignored (and the flag value considered disabled) if
402 * the class' flag is disabled.
403 *
404 * @param symbol the symbol to check
405 * @param flag the flag to check
406 * @return whether the flag is enabled for the given symbol
407 */
408 fun isFlagEnabledForSymbol(symbol: Symbol, flag: Flag): Boolean {
409 when (symbol) {
410 is ClassSymbol -> return flags.getValue(flag)
411 is MemberSymbol -> {
412 val memberFlagValue = flags.getValue(flag)
413 if (!memberFlagValue) {
414 return false
415 }
416 // Special case: if the MemberSymbol's flag is enabled, but the outer
417 // ClassSymbol's flag (if the class is flagged) is disabled, consider
418 // the MemberSymbol's flag as disabled:
419 //
420 // @FlaggedApi(this-flag-is-disabled) Clazz {
421 // @FlaggedApi(this-flag-is-enabled) method(); // The Clazz' flag "wins"
422 // }
423 //
424 // Note: the current implementation does not handle nested classes.
425 val classFlagValue =
426 flaggedSymbolsInSource
427 .find { it.first.toPrettyString() == symbol.clazz }
428 ?.let { flags.getValue(it.second) }
429 ?: true
430 return classFlagValue
431 }
432 }
433 }
434
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200435 val errors = mutableSetOf<ApiError>()
436 for ((symbol, flag) in flaggedSymbolsInSource) {
437 try {
Mårten Kongstad0d44e722024-05-08 10:00:32 +0200438 if (isFlagEnabledForSymbol(symbol, flag)) {
Mårten Kongstadd2c70762024-05-06 14:58:18 +0200439 if (!symbolsInOutput.containsSymbol(symbol)) {
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200440 errors.add(EnabledFlaggedApiNotPresentError(symbol, flag))
441 }
442 } else {
Mårten Kongstadd2c70762024-05-06 14:58:18 +0200443 if (symbolsInOutput.containsSymbol(symbol)) {
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200444 errors.add(DisabledFlaggedApiIsPresentError(symbol, flag))
445 }
446 }
447 } catch (e: NoSuchElementException) {
448 errors.add(UnknownFlagError(symbol, flag))
449 }
450 }
451 return errors
452}
453
Mårten Kongstadd5ce20f2024-06-10 13:59:46 +0200454fun main(args: Array<String>) = MainCommand().subcommands(CheckCommand()).main(args)