blob: 4ff9880a5997649229dd0e99af2c079ea8748099 [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 Kongstada1fe3712024-05-06 13:46:21 +020061 fun createClass(clazz: String): Symbol {
62 return ClassSymbol(toInternalFormat(clazz))
63 }
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 Kongstada1fe3712024-05-06 13:46:21 +020086internal data class ClassSymbol(val clazz: String) : Symbol() {
87 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 Kongstada1fe3712024-05-06 13:46:21 +0200198 val symbol = Symbol.createClass(cls.baselineElementId())
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200199 output.add(Pair(symbol, flag))
Mårten Kongstad20de4052024-04-16 11:33:56 +0200200 }
201 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200202
203 override fun visitField(field: FieldItem) {
204 getFlagOrNull(field)?.let { flag ->
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200205 val symbol =
206 Symbol.createField(field.containingClass().baselineElementId(), field.name())
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200207 output.add(Pair(symbol, flag))
208 }
209 }
210
Mårten Kongstad40da9702024-04-27 01:42:51 +0200211 override fun visitMethod(method: MethodItem) {
212 getFlagOrNull(method)?.let { flag ->
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200213 val methodName = buildString {
Mårten Kongstad40da9702024-04-27 01:42:51 +0200214 append(method.name())
215 append("(")
Mårten Kongstadb4a14bf2024-04-28 00:21:11 +0200216 method.parameters().joinTo(this, separator = "") { it.type().internalName() }
Mårten Kongstad40da9702024-04-27 01:42:51 +0200217 append(")")
218 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200219 val symbol = Symbol.createMethod(method.containingClass().qualifiedName(), methodName)
Mårten Kongstad40da9702024-04-27 01:42:51 +0200220 output.add(Pair(symbol, flag))
221 }
222 }
223
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200224 private fun getFlagOrNull(item: Item): Flag? {
225 return item.modifiers
226 .findAnnotation("android.annotation.FlaggedApi")
227 ?.findAttribute("value")
228 ?.value
229 ?.let { Flag(it.value() as String) }
230 }
Mårten Kongstad20de4052024-04-16 11:33:56 +0200231 }
232 val codebase = ApiFile.parseApi(path, input)
233 codebase.accept(visitor)
234 return output
235}
236
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200237internal fun parseFlagValues(input: InputStream): Map<Flag, Boolean> {
238 val parsedFlags = Aconfig.parsed_flags.parseFrom(input).getParsedFlagList()
239 return parsedFlags.associateBy(
240 { Flag("${it.getPackage()}.${it.getName()}") },
241 { it.getState() == Aconfig.flag_state.ENABLED })
242}
243
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200244internal fun parseApiVersions(input: InputStream): Set<Symbol> {
245 fun Node.getAttribute(name: String): String? = getAttributes()?.getNamedItem(name)?.getNodeValue()
246
247 val output = mutableSetOf<Symbol>()
248 val factory = DocumentBuilderFactory.newInstance()
249 val parser = factory.newDocumentBuilder()
250 val document = parser.parse(input)
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200251
252 val classes = document.getElementsByTagName("class")
253 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
254 for (i in 0.rangeUntil(classes.getLength())) {
255 val cls = classes.item(i)
256 val className =
257 requireNotNull(cls.getAttribute("name")) {
258 "Bad XML: <class> element without name attribute"
259 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200260 output.add(Symbol.createClass(className))
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200261 }
262
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200263 val fields = document.getElementsByTagName("field")
264 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
265 for (i in 0.rangeUntil(fields.getLength())) {
266 val field = fields.item(i)
Mårten Kongstad04e45642024-04-26 05:39:03 +0200267 val fieldName =
268 requireNotNull(field.getAttribute("name")) {
269 "Bad XML: <field> element without name attribute"
270 }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200271 val className =
Mårten Kongstadece054c2024-05-02 09:45:11 +0200272 requireNotNull(field.getParentNode()?.getAttribute("name")) {
273 "Bad XML: top level <field> element"
274 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200275 output.add(Symbol.createField(className, fieldName))
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200276 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200277
Mårten Kongstad40da9702024-04-27 01:42:51 +0200278 val methods = document.getElementsByTagName("method")
279 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
280 for (i in 0.rangeUntil(methods.getLength())) {
281 val method = methods.item(i)
282 val methodSignature =
283 requireNotNull(method.getAttribute("name")) {
284 "Bad XML: <method> element without name attribute"
285 }
286 val methodSignatureParts = methodSignature.split(Regex("\\(|\\)"))
287 if (methodSignatureParts.size != 3) {
Mårten Kongstad9aef0d92024-04-29 10:25:34 +0200288 throw Exception("Bad XML: method signature '$methodSignature'")
Mårten Kongstad40da9702024-04-27 01:42:51 +0200289 }
Mårten Kongstadcd93aeb2024-05-02 10:19:18 +0200290 var (methodName, methodArgs, _) = methodSignatureParts
Mårten Kongstad40da9702024-04-27 01:42:51 +0200291 val packageAndClassName =
292 requireNotNull(method.getParentNode()?.getAttribute("name")) {
Mårten Kongstad02525a82024-05-06 10:28:02 +0200293 "Bad XML: top level <method> element, or <class> element missing name attribute"
294 }
295 .replace("$", "/")
Mårten Kongstad40da9702024-04-27 01:42:51 +0200296 if (methodName == "<init>") {
297 methodName = packageAndClassName.split("/").last()
298 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200299 output.add(Symbol.createMethod(packageAndClassName, "$methodName($methodArgs)"))
Mårten Kongstad40da9702024-04-27 01:42:51 +0200300 }
301
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200302 return output
303}
304
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200305/**
306 * Find errors in the given data.
307 *
308 * @param flaggedSymbolsInSource the set of symbols that are flagged in the source code
309 * @param flags the set of flags and their values
310 * @param symbolsInOutput the set of symbols that are present in the output
311 * @return the set of errors found
312 */
313internal fun findErrors(
314 flaggedSymbolsInSource: Set<Pair<Symbol, Flag>>,
315 flags: Map<Flag, Boolean>,
316 symbolsInOutput: Set<Symbol>
317): Set<ApiError> {
318 val errors = mutableSetOf<ApiError>()
319 for ((symbol, flag) in flaggedSymbolsInSource) {
320 try {
321 if (flags.getValue(flag)) {
322 if (!symbolsInOutput.contains(symbol)) {
323 errors.add(EnabledFlaggedApiNotPresentError(symbol, flag))
324 }
325 } else {
326 if (symbolsInOutput.contains(symbol)) {
327 errors.add(DisabledFlaggedApiIsPresentError(symbol, flag))
328 }
329 }
330 } catch (e: NoSuchElementException) {
331 errors.add(UnknownFlagError(symbol, flag))
332 }
333 }
334 return errors
335}
336
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200337fun main(args: Array<String>) = CheckCommand().main(args)