blob: 867ee94a9aa30ca1482bee993e0b81d6f2a16172 [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 Kongstad7c3571f2024-05-06 14:53:54 +020061 fun createClass(clazz: String, interfaces: Set<String>): Symbol {
62 return ClassSymbol(toInternalFormat(clazz), interfaces.map { toInternalFormat(it) }.toSet())
Mårten Kongstada1fe3712024-05-06 13:46:21 +020063 }
64
65 fun createField(clazz: String, field: String): Symbol {
66 require(!field.contains("(") && !field.contains(")"))
67 return MemberSymbol(toInternalFormat(clazz), toInternalFormat(field))
68 }
69
70 fun createMethod(clazz: String, method: String): Symbol {
71 return MemberSymbol(toInternalFormat(clazz), toInternalFormat(method))
72 }
73
74 protected fun toInternalFormat(name: String): String {
75 var internalName = name
Mårten Kongstade0179972024-04-16 11:16:44 +020076 for (ch in FORBIDDEN_CHARS) {
Mårten Kongstada1fe3712024-05-06 13:46:21 +020077 internalName = internalName.replace(ch, '/')
Mårten Kongstade0179972024-04-16 11:16:44 +020078 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +020079 return internalName
Mårten Kongstade0179972024-04-16 11:16:44 +020080 }
81 }
82
Mårten Kongstada1fe3712024-05-06 13:46:21 +020083 abstract fun toPrettyString(): String
84}
Mårten Kongstade0179972024-04-16 11:16:44 +020085
Mårten Kongstad7c3571f2024-05-06 14:53:54 +020086internal data class ClassSymbol(val clazz: String, val interfaces: Set<String>) : Symbol() {
Mårten Kongstada1fe3712024-05-06 13:46:21 +020087 override fun toPrettyString(): String = "$clazz"
88}
89
90internal data class MemberSymbol(val clazz: String, val member: String) : Symbol() {
91 override fun toPrettyString(): String = "$clazz/$member"
Mårten Kongstade0179972024-04-16 11:16:44 +020092}
93
Mårten Kongstaddc3fc2e2024-04-16 11:23:22 +020094/**
95 * Class representing the fully qualified name of an aconfig flag.
96 *
97 * This includes both the flag's package and name, separated by a dot, e.g.:
98 * <pre>
99 * com.android.aconfig.test.disabled_ro
100 * <pre>
101 */
102@JvmInline
103internal value class Flag(val name: String) {
104 override fun toString(): String = name.toString()
105}
106
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200107internal sealed class ApiError {
108 abstract val symbol: Symbol
109 abstract val flag: Flag
110}
111
112internal data class EnabledFlaggedApiNotPresentError(
113 override val symbol: Symbol,
114 override val flag: Flag
115) : ApiError() {
116 override fun toString(): String {
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200117 return "error: enabled @FlaggedApi not present in built artifact: symbol=${symbol.toPrettyString()} flag=$flag"
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200118 }
119}
120
121internal data class DisabledFlaggedApiIsPresentError(
122 override val symbol: Symbol,
123 override val flag: Flag
124) : ApiError() {
125 override fun toString(): String {
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200126 return "error: disabled @FlaggedApi is present in built artifact: symbol=${symbol.toPrettyString()} flag=$flag"
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200127 }
128}
129
130internal data class UnknownFlagError(override val symbol: Symbol, override val flag: Flag) :
131 ApiError() {
132 override fun toString(): String {
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200133 return "error: unknown flag: symbol=${symbol.toPrettyString()} flag=$flag"
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200134 }
135}
136
Mårten Kongstad20de4052024-04-16 11:33:56 +0200137class CheckCommand :
138 CliktCommand(
139 help =
140 """
141Check that all flagged APIs are used in the correct way.
142
143This tool reads the API signature file and checks that all flagged APIs are used in the correct way.
144
145The tool will exit with a non-zero exit code if any flagged APIs are found to be used in the incorrect way.
146""") {
147 private val apiSignaturePath by
148 option("--api-signature")
149 .help(
150 """
151 Path to API signature file.
152 Usually named *current.txt.
153 Tip: `m frameworks-base-api-current.txt` will generate a file that includes all platform and mainline APIs.
154 """)
155 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
156 .required()
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200157 private val flagValuesPath by
158 option("--flag-values")
159 .help(
160 """
161 Path to aconfig parsed_flags binary proto file.
162 Tip: `m all_aconfig_declarations` will generate a file that includes all information about all flags.
163 """)
164 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
165 .required()
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200166 private val apiVersionsPath by
167 option("--api-versions")
168 .help(
169 """
170 Path to API versions XML file.
171 Usually named xml-versions.xml.
172 Tip: `m sdk dist` will generate a file that includes all platform and mainline APIs.
173 """)
174 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
175 .required()
Mårten Kongstad20de4052024-04-16 11:33:56 +0200176
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200177 override fun run() {
Mårten Kongstad20de4052024-04-16 11:33:56 +0200178 val flaggedSymbols =
179 apiSignaturePath.toFile().inputStream().use {
180 parseApiSignature(apiSignaturePath.toString(), it)
181 }
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200182 val flags = flagValuesPath.toFile().inputStream().use { parseFlagValues(it) }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200183 val exportedSymbols = apiVersionsPath.toFile().inputStream().use { parseApiVersions(it) }
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200184 val errors = findErrors(flaggedSymbols, flags, exportedSymbols)
185 for (e in errors) {
186 println(e)
187 }
188 throw ProgramResult(errors.size)
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200189 }
190}
191
Mårten Kongstad20de4052024-04-16 11:33:56 +0200192internal fun parseApiSignature(path: String, input: InputStream): Set<Pair<Symbol, Flag>> {
Mårten Kongstad20de4052024-04-16 11:33:56 +0200193 val output = mutableSetOf<Pair<Symbol, Flag>>()
194 val visitor =
195 object : BaseItemVisitor() {
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200196 override fun visitClass(cls: ClassItem) {
197 getFlagOrNull(cls)?.let { flag ->
Mårten Kongstad7c3571f2024-05-06 14:53:54 +0200198 val symbol =
199 Symbol.createClass(
200 cls.baselineElementId(),
Mårten Kongstad04d8b462024-05-06 16:26:40 +0200201 cls.allInterfaces()
202 .map { it.baselineElementId() }
203 .filter { it != cls.baselineElementId() }
204 .toSet())
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200205 output.add(Pair(symbol, flag))
Mårten Kongstad20de4052024-04-16 11:33:56 +0200206 }
207 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200208
209 override fun visitField(field: FieldItem) {
210 getFlagOrNull(field)?.let { flag ->
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200211 val symbol =
212 Symbol.createField(field.containingClass().baselineElementId(), field.name())
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200213 output.add(Pair(symbol, flag))
214 }
215 }
216
Mårten Kongstad40da9702024-04-27 01:42:51 +0200217 override fun visitMethod(method: MethodItem) {
218 getFlagOrNull(method)?.let { flag ->
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200219 val methodName = buildString {
Mårten Kongstad40da9702024-04-27 01:42:51 +0200220 append(method.name())
221 append("(")
Mårten Kongstadb4a14bf2024-04-28 00:21:11 +0200222 method.parameters().joinTo(this, separator = "") { it.type().internalName() }
Mårten Kongstad40da9702024-04-27 01:42:51 +0200223 append(")")
224 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200225 val symbol = Symbol.createMethod(method.containingClass().qualifiedName(), methodName)
Mårten Kongstad40da9702024-04-27 01:42:51 +0200226 output.add(Pair(symbol, flag))
227 }
228 }
229
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200230 private fun getFlagOrNull(item: Item): Flag? {
231 return item.modifiers
232 .findAnnotation("android.annotation.FlaggedApi")
233 ?.findAttribute("value")
234 ?.value
235 ?.let { Flag(it.value() as String) }
236 }
Mårten Kongstad20de4052024-04-16 11:33:56 +0200237 }
238 val codebase = ApiFile.parseApi(path, input)
239 codebase.accept(visitor)
240 return output
241}
242
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200243internal fun parseFlagValues(input: InputStream): Map<Flag, Boolean> {
244 val parsedFlags = Aconfig.parsed_flags.parseFrom(input).getParsedFlagList()
245 return parsedFlags.associateBy(
246 { Flag("${it.getPackage()}.${it.getName()}") },
247 { it.getState() == Aconfig.flag_state.ENABLED })
248}
249
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200250internal fun parseApiVersions(input: InputStream): Set<Symbol> {
251 fun Node.getAttribute(name: String): String? = getAttributes()?.getNamedItem(name)?.getNodeValue()
252
253 val output = mutableSetOf<Symbol>()
254 val factory = DocumentBuilderFactory.newInstance()
255 val parser = factory.newDocumentBuilder()
256 val document = parser.parse(input)
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200257
258 val classes = document.getElementsByTagName("class")
259 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
260 for (i in 0.rangeUntil(classes.getLength())) {
261 val cls = classes.item(i)
262 val className =
263 requireNotNull(cls.getAttribute("name")) {
264 "Bad XML: <class> element without name attribute"
265 }
Mårten Kongstad7c3571f2024-05-06 14:53:54 +0200266 val interfaces = mutableSetOf<String>()
267 val children = cls.getChildNodes()
268 for (j in 0.rangeUntil(children.getLength())) {
269 val child = children.item(j)
270 if (child.getNodeName() == "implements") {
271 val interfaceName =
272 requireNotNull(child.getAttribute("name")) {
273 "Bad XML: <implements> element without name attribute"
274 }
275 interfaces.add(interfaceName)
276 }
277 }
278 output.add(Symbol.createClass(className, interfaces))
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200279 }
280
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200281 val fields = document.getElementsByTagName("field")
282 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
283 for (i in 0.rangeUntil(fields.getLength())) {
284 val field = fields.item(i)
Mårten Kongstad04e45642024-04-26 05:39:03 +0200285 val fieldName =
286 requireNotNull(field.getAttribute("name")) {
287 "Bad XML: <field> element without name attribute"
288 }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200289 val className =
Mårten Kongstadece054c2024-05-02 09:45:11 +0200290 requireNotNull(field.getParentNode()?.getAttribute("name")) {
291 "Bad XML: top level <field> element"
292 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200293 output.add(Symbol.createField(className, fieldName))
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200294 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200295
Mårten Kongstad40da9702024-04-27 01:42:51 +0200296 val methods = document.getElementsByTagName("method")
297 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
298 for (i in 0.rangeUntil(methods.getLength())) {
299 val method = methods.item(i)
300 val methodSignature =
301 requireNotNull(method.getAttribute("name")) {
302 "Bad XML: <method> element without name attribute"
303 }
304 val methodSignatureParts = methodSignature.split(Regex("\\(|\\)"))
305 if (methodSignatureParts.size != 3) {
Mårten Kongstad9aef0d92024-04-29 10:25:34 +0200306 throw Exception("Bad XML: method signature '$methodSignature'")
Mårten Kongstad40da9702024-04-27 01:42:51 +0200307 }
Mårten Kongstadcd93aeb2024-05-02 10:19:18 +0200308 var (methodName, methodArgs, _) = methodSignatureParts
Mårten Kongstad40da9702024-04-27 01:42:51 +0200309 val packageAndClassName =
310 requireNotNull(method.getParentNode()?.getAttribute("name")) {
Mårten Kongstad02525a82024-05-06 10:28:02 +0200311 "Bad XML: top level <method> element, or <class> element missing name attribute"
312 }
313 .replace("$", "/")
Mårten Kongstad40da9702024-04-27 01:42:51 +0200314 if (methodName == "<init>") {
315 methodName = packageAndClassName.split("/").last()
316 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200317 output.add(Symbol.createMethod(packageAndClassName, "$methodName($methodArgs)"))
Mårten Kongstad40da9702024-04-27 01:42:51 +0200318 }
319
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200320 return output
321}
322
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200323/**
324 * Find errors in the given data.
325 *
326 * @param flaggedSymbolsInSource the set of symbols that are flagged in the source code
327 * @param flags the set of flags and their values
328 * @param symbolsInOutput the set of symbols that are present in the output
329 * @return the set of errors found
330 */
331internal fun findErrors(
332 flaggedSymbolsInSource: Set<Pair<Symbol, Flag>>,
333 flags: Map<Flag, Boolean>,
334 symbolsInOutput: Set<Symbol>
335): Set<ApiError> {
Mårten Kongstadd2c70762024-05-06 14:58:18 +0200336 fun Set<Symbol>.containsSymbol(symbol: Symbol): Boolean {
337 // trivial case: the symbol is explicitly listed in api-versions.xml
338 if (contains(symbol)) {
339 return true
340 }
341
342 // non-trivial case: the symbol could be part of the surrounding class'
343 // super class or interfaces
344 val (className, memberName) =
345 when (symbol) {
346 is ClassSymbol -> return false
347 is MemberSymbol -> {
348 Pair(symbol.clazz, symbol.member)
349 }
350 }
351 val clazz = find { it is ClassSymbol && it.clazz == className } as? ClassSymbol?
352 if (clazz == null) {
353 return false
354 }
355
356 for (interfaceName in clazz.interfaces) {
357 // createMethod is the same as createField, except it allows parenthesis
358 val interfaceSymbol = Symbol.createMethod(interfaceName, memberName)
359 if (contains(interfaceSymbol)) {
360 return true
361 }
362 }
363
364 return false
365 }
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200366 val errors = mutableSetOf<ApiError>()
367 for ((symbol, flag) in flaggedSymbolsInSource) {
368 try {
369 if (flags.getValue(flag)) {
Mårten Kongstadd2c70762024-05-06 14:58:18 +0200370 if (!symbolsInOutput.containsSymbol(symbol)) {
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200371 errors.add(EnabledFlaggedApiNotPresentError(symbol, flag))
372 }
373 } else {
Mårten Kongstadd2c70762024-05-06 14:58:18 +0200374 if (symbolsInOutput.containsSymbol(symbol)) {
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200375 errors.add(DisabledFlaggedApiIsPresentError(symbol, flag))
376 }
377 }
378 } catch (e: NoSuchElementException) {
379 errors.add(UnknownFlagError(symbol, flag))
380 }
381 }
382 return errors
383}
384
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200385fun main(args: Array<String>) = CheckCommand().main(args)