blob: 0f58aa54b95ea4626675457e10de96245d841720 [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(),
201 cls.allInterfaces().map { it.baselineElementId() }.toSet())
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200202 output.add(Pair(symbol, flag))
Mårten Kongstad20de4052024-04-16 11:33:56 +0200203 }
204 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200205
206 override fun visitField(field: FieldItem) {
207 getFlagOrNull(field)?.let { flag ->
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200208 val symbol =
209 Symbol.createField(field.containingClass().baselineElementId(), field.name())
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200210 output.add(Pair(symbol, flag))
211 }
212 }
213
Mårten Kongstad40da9702024-04-27 01:42:51 +0200214 override fun visitMethod(method: MethodItem) {
215 getFlagOrNull(method)?.let { flag ->
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200216 val methodName = buildString {
Mårten Kongstad40da9702024-04-27 01:42:51 +0200217 append(method.name())
218 append("(")
Mårten Kongstadb4a14bf2024-04-28 00:21:11 +0200219 method.parameters().joinTo(this, separator = "") { it.type().internalName() }
Mårten Kongstad40da9702024-04-27 01:42:51 +0200220 append(")")
221 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200222 val symbol = Symbol.createMethod(method.containingClass().qualifiedName(), methodName)
Mårten Kongstad40da9702024-04-27 01:42:51 +0200223 output.add(Pair(symbol, flag))
224 }
225 }
226
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200227 private fun getFlagOrNull(item: Item): Flag? {
228 return item.modifiers
229 .findAnnotation("android.annotation.FlaggedApi")
230 ?.findAttribute("value")
231 ?.value
232 ?.let { Flag(it.value() as String) }
233 }
Mårten Kongstad20de4052024-04-16 11:33:56 +0200234 }
235 val codebase = ApiFile.parseApi(path, input)
236 codebase.accept(visitor)
237 return output
238}
239
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200240internal fun parseFlagValues(input: InputStream): Map<Flag, Boolean> {
241 val parsedFlags = Aconfig.parsed_flags.parseFrom(input).getParsedFlagList()
242 return parsedFlags.associateBy(
243 { Flag("${it.getPackage()}.${it.getName()}") },
244 { it.getState() == Aconfig.flag_state.ENABLED })
245}
246
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200247internal fun parseApiVersions(input: InputStream): Set<Symbol> {
248 fun Node.getAttribute(name: String): String? = getAttributes()?.getNamedItem(name)?.getNodeValue()
249
250 val output = mutableSetOf<Symbol>()
251 val factory = DocumentBuilderFactory.newInstance()
252 val parser = factory.newDocumentBuilder()
253 val document = parser.parse(input)
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200254
255 val classes = document.getElementsByTagName("class")
256 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
257 for (i in 0.rangeUntil(classes.getLength())) {
258 val cls = classes.item(i)
259 val className =
260 requireNotNull(cls.getAttribute("name")) {
261 "Bad XML: <class> element without name attribute"
262 }
Mårten Kongstad7c3571f2024-05-06 14:53:54 +0200263 val interfaces = mutableSetOf<String>()
264 val children = cls.getChildNodes()
265 for (j in 0.rangeUntil(children.getLength())) {
266 val child = children.item(j)
267 if (child.getNodeName() == "implements") {
268 val interfaceName =
269 requireNotNull(child.getAttribute("name")) {
270 "Bad XML: <implements> element without name attribute"
271 }
272 interfaces.add(interfaceName)
273 }
274 }
275 output.add(Symbol.createClass(className, interfaces))
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200276 }
277
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200278 val fields = document.getElementsByTagName("field")
279 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
280 for (i in 0.rangeUntil(fields.getLength())) {
281 val field = fields.item(i)
Mårten Kongstad04e45642024-04-26 05:39:03 +0200282 val fieldName =
283 requireNotNull(field.getAttribute("name")) {
284 "Bad XML: <field> element without name attribute"
285 }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200286 val className =
Mårten Kongstadece054c2024-05-02 09:45:11 +0200287 requireNotNull(field.getParentNode()?.getAttribute("name")) {
288 "Bad XML: top level <field> element"
289 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200290 output.add(Symbol.createField(className, fieldName))
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200291 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200292
Mårten Kongstad40da9702024-04-27 01:42:51 +0200293 val methods = document.getElementsByTagName("method")
294 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
295 for (i in 0.rangeUntil(methods.getLength())) {
296 val method = methods.item(i)
297 val methodSignature =
298 requireNotNull(method.getAttribute("name")) {
299 "Bad XML: <method> element without name attribute"
300 }
301 val methodSignatureParts = methodSignature.split(Regex("\\(|\\)"))
302 if (methodSignatureParts.size != 3) {
Mårten Kongstad9aef0d92024-04-29 10:25:34 +0200303 throw Exception("Bad XML: method signature '$methodSignature'")
Mårten Kongstad40da9702024-04-27 01:42:51 +0200304 }
Mårten Kongstadcd93aeb2024-05-02 10:19:18 +0200305 var (methodName, methodArgs, _) = methodSignatureParts
Mårten Kongstad40da9702024-04-27 01:42:51 +0200306 val packageAndClassName =
307 requireNotNull(method.getParentNode()?.getAttribute("name")) {
Mårten Kongstad02525a82024-05-06 10:28:02 +0200308 "Bad XML: top level <method> element, or <class> element missing name attribute"
309 }
310 .replace("$", "/")
Mårten Kongstad40da9702024-04-27 01:42:51 +0200311 if (methodName == "<init>") {
312 methodName = packageAndClassName.split("/").last()
313 }
Mårten Kongstada1fe3712024-05-06 13:46:21 +0200314 output.add(Symbol.createMethod(packageAndClassName, "$methodName($methodArgs)"))
Mårten Kongstad40da9702024-04-27 01:42:51 +0200315 }
316
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200317 return output
318}
319
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200320/**
321 * Find errors in the given data.
322 *
323 * @param flaggedSymbolsInSource the set of symbols that are flagged in the source code
324 * @param flags the set of flags and their values
325 * @param symbolsInOutput the set of symbols that are present in the output
326 * @return the set of errors found
327 */
328internal fun findErrors(
329 flaggedSymbolsInSource: Set<Pair<Symbol, Flag>>,
330 flags: Map<Flag, Boolean>,
331 symbolsInOutput: Set<Symbol>
332): Set<ApiError> {
333 val errors = mutableSetOf<ApiError>()
334 for ((symbol, flag) in flaggedSymbolsInSource) {
335 try {
336 if (flags.getValue(flag)) {
337 if (!symbolsInOutput.contains(symbol)) {
338 errors.add(EnabledFlaggedApiNotPresentError(symbol, flag))
339 }
340 } else {
341 if (symbolsInOutput.contains(symbol)) {
342 errors.add(DisabledFlaggedApiIsPresentError(symbol, flag))
343 }
344 }
345 } catch (e: NoSuchElementException) {
346 errors.add(UnknownFlagError(symbol, flag))
347 }
348 }
349 return errors
350}
351
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200352fun main(args: Array<String>) = CheckCommand().main(args)