blob: e15e7fa6eb2d4444339f479e075696da3e72aaa4 [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 */
57@JvmInline
58internal value class Symbol(val name: String) {
59 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
62 /** Create a new Symbol from a String that may include delimiters other than dot. */
63 fun create(name: String): Symbol {
64 var sanitizedName = name
65 for (ch in FORBIDDEN_CHARS) {
Mårten Kongstadece054c2024-05-02 09:45:11 +020066 sanitizedName = sanitizedName.replace(ch, '/')
Mårten Kongstade0179972024-04-16 11:16:44 +020067 }
68 return Symbol(sanitizedName)
69 }
70 }
71
72 init {
73 require(!name.isEmpty()) { "empty string" }
74 for (ch in FORBIDDEN_CHARS) {
75 require(!name.contains(ch)) { "$name: contains $ch" }
76 }
77 }
78
79 override fun toString(): String = name.toString()
80}
81
Mårten Kongstaddc3fc2e2024-04-16 11:23:22 +020082/**
83 * Class representing the fully qualified name of an aconfig flag.
84 *
85 * This includes both the flag's package and name, separated by a dot, e.g.:
86 * <pre>
87 * com.android.aconfig.test.disabled_ro
88 * <pre>
89 */
90@JvmInline
91internal value class Flag(val name: String) {
92 override fun toString(): String = name.toString()
93}
94
Mårten Kongstad9238a3a2024-04-16 13:19:50 +020095internal sealed class ApiError {
96 abstract val symbol: Symbol
97 abstract val flag: Flag
98}
99
100internal data class EnabledFlaggedApiNotPresentError(
101 override val symbol: Symbol,
102 override val flag: Flag
103) : ApiError() {
104 override fun toString(): String {
105 return "error: enabled @FlaggedApi not present in built artifact: symbol=$symbol flag=$flag"
106 }
107}
108
109internal data class DisabledFlaggedApiIsPresentError(
110 override val symbol: Symbol,
111 override val flag: Flag
112) : ApiError() {
113 override fun toString(): String {
114 return "error: disabled @FlaggedApi is present in built artifact: symbol=$symbol flag=$flag"
115 }
116}
117
118internal data class UnknownFlagError(override val symbol: Symbol, override val flag: Flag) :
119 ApiError() {
120 override fun toString(): String {
121 return "error: unknown flag: symbol=$symbol flag=$flag"
122 }
123}
124
Mårten Kongstad20de4052024-04-16 11:33:56 +0200125class CheckCommand :
126 CliktCommand(
127 help =
128 """
129Check that all flagged APIs are used in the correct way.
130
131This tool reads the API signature file and checks that all flagged APIs are used in the correct way.
132
133The tool will exit with a non-zero exit code if any flagged APIs are found to be used in the incorrect way.
134""") {
135 private val apiSignaturePath by
136 option("--api-signature")
137 .help(
138 """
139 Path to API signature file.
140 Usually named *current.txt.
141 Tip: `m frameworks-base-api-current.txt` will generate a file that includes all platform and mainline APIs.
142 """)
143 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
144 .required()
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200145 private val flagValuesPath by
146 option("--flag-values")
147 .help(
148 """
149 Path to aconfig parsed_flags binary proto file.
150 Tip: `m all_aconfig_declarations` will generate a file that includes all information about all flags.
151 """)
152 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
153 .required()
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200154 private val apiVersionsPath by
155 option("--api-versions")
156 .help(
157 """
158 Path to API versions XML file.
159 Usually named xml-versions.xml.
160 Tip: `m sdk dist` will generate a file that includes all platform and mainline APIs.
161 """)
162 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
163 .required()
Mårten Kongstad20de4052024-04-16 11:33:56 +0200164
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200165 override fun run() {
Mårten Kongstad20de4052024-04-16 11:33:56 +0200166 val flaggedSymbols =
167 apiSignaturePath.toFile().inputStream().use {
168 parseApiSignature(apiSignaturePath.toString(), it)
169 }
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200170 val flags = flagValuesPath.toFile().inputStream().use { parseFlagValues(it) }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200171 val exportedSymbols = apiVersionsPath.toFile().inputStream().use { parseApiVersions(it) }
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200172 val errors = findErrors(flaggedSymbols, flags, exportedSymbols)
173 for (e in errors) {
174 println(e)
175 }
176 throw ProgramResult(errors.size)
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200177 }
178}
179
Mårten Kongstad20de4052024-04-16 11:33:56 +0200180internal fun parseApiSignature(path: String, input: InputStream): Set<Pair<Symbol, Flag>> {
Mårten Kongstad20de4052024-04-16 11:33:56 +0200181 val output = mutableSetOf<Pair<Symbol, Flag>>()
182 val visitor =
183 object : BaseItemVisitor() {
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200184 override fun visitClass(cls: ClassItem) {
185 getFlagOrNull(cls)?.let { flag ->
186 val symbol = Symbol.create(cls.baselineElementId())
187 output.add(Pair(symbol, flag))
Mårten Kongstad20de4052024-04-16 11:33:56 +0200188 }
189 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200190
191 override fun visitField(field: FieldItem) {
192 getFlagOrNull(field)?.let { flag ->
193 val symbol = Symbol.create(field.baselineElementId())
194 output.add(Pair(symbol, flag))
195 }
196 }
197
Mårten Kongstad40da9702024-04-27 01:42:51 +0200198 override fun visitMethod(method: MethodItem) {
199 getFlagOrNull(method)?.let { flag ->
200 val name = buildString {
201 append(method.containingClass().qualifiedName())
202 append(".")
203 append(method.name())
204 append("(")
Mårten Kongstadb4a14bf2024-04-28 00:21:11 +0200205 method.parameters().joinTo(this, separator = "") { it.type().internalName() }
Mårten Kongstad40da9702024-04-27 01:42:51 +0200206 append(")")
207 }
208 val symbol = Symbol.create(name)
209 output.add(Pair(symbol, flag))
210 }
211 }
212
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200213 private fun getFlagOrNull(item: Item): Flag? {
214 return item.modifiers
215 .findAnnotation("android.annotation.FlaggedApi")
216 ?.findAttribute("value")
217 ?.value
218 ?.let { Flag(it.value() as String) }
219 }
Mårten Kongstad20de4052024-04-16 11:33:56 +0200220 }
221 val codebase = ApiFile.parseApi(path, input)
222 codebase.accept(visitor)
223 return output
224}
225
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200226internal fun parseFlagValues(input: InputStream): Map<Flag, Boolean> {
227 val parsedFlags = Aconfig.parsed_flags.parseFrom(input).getParsedFlagList()
228 return parsedFlags.associateBy(
229 { Flag("${it.getPackage()}.${it.getName()}") },
230 { it.getState() == Aconfig.flag_state.ENABLED })
231}
232
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200233internal fun parseApiVersions(input: InputStream): Set<Symbol> {
234 fun Node.getAttribute(name: String): String? = getAttributes()?.getNamedItem(name)?.getNodeValue()
235
236 val output = mutableSetOf<Symbol>()
237 val factory = DocumentBuilderFactory.newInstance()
238 val parser = factory.newDocumentBuilder()
239 val document = parser.parse(input)
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200240
241 val classes = document.getElementsByTagName("class")
242 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
243 for (i in 0.rangeUntil(classes.getLength())) {
244 val cls = classes.item(i)
245 val className =
246 requireNotNull(cls.getAttribute("name")) {
247 "Bad XML: <class> element without name attribute"
248 }
Mårten Kongstad8d74fd02024-04-28 00:50:11 +0200249 output.add(Symbol.create(className.replace("/", ".")))
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200250 }
251
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200252 val fields = document.getElementsByTagName("field")
253 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
254 for (i in 0.rangeUntil(fields.getLength())) {
255 val field = fields.item(i)
Mårten Kongstad04e45642024-04-26 05:39:03 +0200256 val fieldName =
257 requireNotNull(field.getAttribute("name")) {
258 "Bad XML: <field> element without name attribute"
259 }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200260 val className =
Mårten Kongstadece054c2024-05-02 09:45:11 +0200261 requireNotNull(field.getParentNode()?.getAttribute("name")) {
262 "Bad XML: top level <field> element"
263 }
Mårten Kongstad8d74fd02024-04-28 00:50:11 +0200264 output.add(Symbol.create("${className.replace("/", ".")}.$fieldName"))
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200265 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200266
Mårten Kongstad40da9702024-04-27 01:42:51 +0200267 val methods = document.getElementsByTagName("method")
268 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
269 for (i in 0.rangeUntil(methods.getLength())) {
270 val method = methods.item(i)
271 val methodSignature =
272 requireNotNull(method.getAttribute("name")) {
273 "Bad XML: <method> element without name attribute"
274 }
275 val methodSignatureParts = methodSignature.split(Regex("\\(|\\)"))
276 if (methodSignatureParts.size != 3) {
Mårten Kongstad9aef0d92024-04-29 10:25:34 +0200277 throw Exception("Bad XML: method signature '$methodSignature'")
Mårten Kongstad40da9702024-04-27 01:42:51 +0200278 }
279 var (methodName, methodArgs, methodReturnValue) = methodSignatureParts
280 val packageAndClassName =
281 requireNotNull(method.getParentNode()?.getAttribute("name")) {
282 "Bad XML: top level <method> element, or <class> element missing name attribute"
283 }
284 if (methodName == "<init>") {
285 methodName = packageAndClassName.split("/").last()
286 }
Mårten Kongstad8d74fd02024-04-28 00:50:11 +0200287 output.add(Symbol.create("${packageAndClassName.replace("/", ".")}.$methodName($methodArgs)"))
Mårten Kongstad40da9702024-04-27 01:42:51 +0200288 }
289
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200290 return output
291}
292
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200293/**
294 * Find errors in the given data.
295 *
296 * @param flaggedSymbolsInSource the set of symbols that are flagged in the source code
297 * @param flags the set of flags and their values
298 * @param symbolsInOutput the set of symbols that are present in the output
299 * @return the set of errors found
300 */
301internal fun findErrors(
302 flaggedSymbolsInSource: Set<Pair<Symbol, Flag>>,
303 flags: Map<Flag, Boolean>,
304 symbolsInOutput: Set<Symbol>
305): Set<ApiError> {
306 val errors = mutableSetOf<ApiError>()
307 for ((symbol, flag) in flaggedSymbolsInSource) {
308 try {
309 if (flags.getValue(flag)) {
310 if (!symbolsInOutput.contains(symbol)) {
311 errors.add(EnabledFlaggedApiNotPresentError(symbol, flag))
312 }
313 } else {
314 if (symbolsInOutput.contains(symbol)) {
315 errors.add(DisabledFlaggedApiIsPresentError(symbol, flag))
316 }
317 }
318 } catch (e: NoSuchElementException) {
319 errors.add(UnknownFlagError(symbol, flag))
320 }
321 }
322 return errors
323}
324
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200325fun main(args: Array<String>) = CheckCommand().main(args)