blob: 0f2fbef719715b392b28b3c2c0555a3fb9a762b1 [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 Kongstad18ff19a2024-04-26 05:48:57 +0200181 // TODO(334870672): add support for metods
Mårten Kongstad20de4052024-04-16 11:33:56 +0200182 val output = mutableSetOf<Pair<Symbol, Flag>>()
183 val visitor =
184 object : BaseItemVisitor() {
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200185 override fun visitClass(cls: ClassItem) {
186 getFlagOrNull(cls)?.let { flag ->
187 val symbol = Symbol.create(cls.baselineElementId())
188 output.add(Pair(symbol, flag))
Mårten Kongstad20de4052024-04-16 11:33:56 +0200189 }
190 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200191
192 override fun visitField(field: FieldItem) {
193 getFlagOrNull(field)?.let { flag ->
194 val symbol = Symbol.create(field.baselineElementId())
195 output.add(Pair(symbol, flag))
196 }
197 }
198
Mårten Kongstad40da9702024-04-27 01:42:51 +0200199 override fun visitMethod(method: MethodItem) {
200 getFlagOrNull(method)?.let { flag ->
201 val name = buildString {
202 append(method.containingClass().qualifiedName())
203 append(".")
204 append(method.name())
205 append("(")
206 // TODO(334870672): replace this early return with proper parsing of the command line
207 // arguments, followed by translation to Lname/of/class; + III format
208 if (!method.parameters().isEmpty()) {
209 return
210 }
211 append(")")
212 }
213 val symbol = Symbol.create(name)
214 output.add(Pair(symbol, flag))
215 }
216 }
217
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200218 private fun getFlagOrNull(item: Item): Flag? {
219 return item.modifiers
220 .findAnnotation("android.annotation.FlaggedApi")
221 ?.findAttribute("value")
222 ?.value
223 ?.let { Flag(it.value() as String) }
224 }
Mårten Kongstad20de4052024-04-16 11:33:56 +0200225 }
226 val codebase = ApiFile.parseApi(path, input)
227 codebase.accept(visitor)
228 return output
229}
230
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200231internal fun parseFlagValues(input: InputStream): Map<Flag, Boolean> {
232 val parsedFlags = Aconfig.parsed_flags.parseFrom(input).getParsedFlagList()
233 return parsedFlags.associateBy(
234 { Flag("${it.getPackage()}.${it.getName()}") },
235 { it.getState() == Aconfig.flag_state.ENABLED })
236}
237
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200238internal fun parseApiVersions(input: InputStream): Set<Symbol> {
239 fun Node.getAttribute(name: String): String? = getAttributes()?.getNamedItem(name)?.getNodeValue()
240
241 val output = mutableSetOf<Symbol>()
242 val factory = DocumentBuilderFactory.newInstance()
243 val parser = factory.newDocumentBuilder()
244 val document = parser.parse(input)
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200245
246 val classes = document.getElementsByTagName("class")
247 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
248 for (i in 0.rangeUntil(classes.getLength())) {
249 val cls = classes.item(i)
250 val className =
251 requireNotNull(cls.getAttribute("name")) {
252 "Bad XML: <class> element without name attribute"
253 }
Mårten Kongstad8d74fd02024-04-28 00:50:11 +0200254 output.add(Symbol.create(className.replace("/", ".")))
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200255 }
256
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200257 val fields = document.getElementsByTagName("field")
258 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
259 for (i in 0.rangeUntil(fields.getLength())) {
260 val field = fields.item(i)
Mårten Kongstad04e45642024-04-26 05:39:03 +0200261 val fieldName =
262 requireNotNull(field.getAttribute("name")) {
263 "Bad XML: <field> element without name attribute"
264 }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200265 val className =
Mårten Kongstadece054c2024-05-02 09:45:11 +0200266 requireNotNull(field.getParentNode()?.getAttribute("name")) {
267 "Bad XML: top level <field> element"
268 }
Mårten Kongstad8d74fd02024-04-28 00:50:11 +0200269 output.add(Symbol.create("${className.replace("/", ".")}.$fieldName"))
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200270 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200271
Mårten Kongstad40da9702024-04-27 01:42:51 +0200272 val methods = document.getElementsByTagName("method")
273 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
274 for (i in 0.rangeUntil(methods.getLength())) {
275 val method = methods.item(i)
276 val methodSignature =
277 requireNotNull(method.getAttribute("name")) {
278 "Bad XML: <method> element without name attribute"
279 }
280 val methodSignatureParts = methodSignature.split(Regex("\\(|\\)"))
281 if (methodSignatureParts.size != 3) {
Mårten Kongstad9aef0d92024-04-29 10:25:34 +0200282 throw Exception("Bad XML: method signature '$methodSignature'")
Mårten Kongstad40da9702024-04-27 01:42:51 +0200283 }
284 var (methodName, methodArgs, methodReturnValue) = methodSignatureParts
285 val packageAndClassName =
286 requireNotNull(method.getParentNode()?.getAttribute("name")) {
287 "Bad XML: top level <method> element, or <class> element missing name attribute"
288 }
289 if (methodName == "<init>") {
290 methodName = packageAndClassName.split("/").last()
291 }
Mårten Kongstad8d74fd02024-04-28 00:50:11 +0200292 output.add(Symbol.create("${packageAndClassName.replace("/", ".")}.$methodName($methodArgs)"))
Mårten Kongstad40da9702024-04-27 01:42:51 +0200293 }
294
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200295 return output
296}
297
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200298/**
299 * Find errors in the given data.
300 *
301 * @param flaggedSymbolsInSource the set of symbols that are flagged in the source code
302 * @param flags the set of flags and their values
303 * @param symbolsInOutput the set of symbols that are present in the output
304 * @return the set of errors found
305 */
306internal fun findErrors(
307 flaggedSymbolsInSource: Set<Pair<Symbol, Flag>>,
308 flags: Map<Flag, Boolean>,
309 symbolsInOutput: Set<Symbol>
310): Set<ApiError> {
311 val errors = mutableSetOf<ApiError>()
312 for ((symbol, flag) in flaggedSymbolsInSource) {
313 try {
314 if (flags.getValue(flag)) {
315 if (!symbolsInOutput.contains(symbol)) {
316 errors.add(EnabledFlaggedApiNotPresentError(symbol, flag))
317 }
318 } else {
319 if (symbolsInOutput.contains(symbol)) {
320 errors.add(DisabledFlaggedApiIsPresentError(symbol, flag))
321 }
322 }
323 } catch (e: NoSuchElementException) {
324 errors.add(UnknownFlagError(symbol, flag))
325 }
326 }
327 return errors
328}
329
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200330fun main(args: Array<String>) = CheckCommand().main(args)