blob: 143243af85ed0898e34a78e76cbd4bdc15f972f8 [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 *
44 * All parts of the fully qualified name of the Symbol are separated by a dot, e.g.:
45 * <pre>
46 * package.class.inner-class.field
47 * </pre>
48 */
49@JvmInline
50internal value class Symbol(val name: String) {
51 companion object {
52 private val FORBIDDEN_CHARS = listOf('/', '#', '$')
53
54 /** Create a new Symbol from a String that may include delimiters other than dot. */
55 fun create(name: String): Symbol {
56 var sanitizedName = name
57 for (ch in FORBIDDEN_CHARS) {
58 sanitizedName = sanitizedName.replace(ch, '.')
59 }
60 return Symbol(sanitizedName)
61 }
62 }
63
64 init {
65 require(!name.isEmpty()) { "empty string" }
66 for (ch in FORBIDDEN_CHARS) {
67 require(!name.contains(ch)) { "$name: contains $ch" }
68 }
69 }
70
71 override fun toString(): String = name.toString()
72}
73
Mårten Kongstaddc3fc2e2024-04-16 11:23:22 +020074/**
75 * Class representing the fully qualified name of an aconfig flag.
76 *
77 * This includes both the flag's package and name, separated by a dot, e.g.:
78 * <pre>
79 * com.android.aconfig.test.disabled_ro
80 * <pre>
81 */
82@JvmInline
83internal value class Flag(val name: String) {
84 override fun toString(): String = name.toString()
85}
86
Mårten Kongstad9238a3a2024-04-16 13:19:50 +020087internal sealed class ApiError {
88 abstract val symbol: Symbol
89 abstract val flag: Flag
90}
91
92internal data class EnabledFlaggedApiNotPresentError(
93 override val symbol: Symbol,
94 override val flag: Flag
95) : ApiError() {
96 override fun toString(): String {
97 return "error: enabled @FlaggedApi not present in built artifact: symbol=$symbol flag=$flag"
98 }
99}
100
101internal data class DisabledFlaggedApiIsPresentError(
102 override val symbol: Symbol,
103 override val flag: Flag
104) : ApiError() {
105 override fun toString(): String {
106 return "error: disabled @FlaggedApi is present in built artifact: symbol=$symbol flag=$flag"
107 }
108}
109
110internal data class UnknownFlagError(override val symbol: Symbol, override val flag: Flag) :
111 ApiError() {
112 override fun toString(): String {
113 return "error: unknown flag: symbol=$symbol flag=$flag"
114 }
115}
116
Mårten Kongstad20de4052024-04-16 11:33:56 +0200117class CheckCommand :
118 CliktCommand(
119 help =
120 """
121Check that all flagged APIs are used in the correct way.
122
123This tool reads the API signature file and checks that all flagged APIs are used in the correct way.
124
125The tool will exit with a non-zero exit code if any flagged APIs are found to be used in the incorrect way.
126""") {
127 private val apiSignaturePath by
128 option("--api-signature")
129 .help(
130 """
131 Path to API signature file.
132 Usually named *current.txt.
133 Tip: `m frameworks-base-api-current.txt` will generate a file that includes all platform and mainline APIs.
134 """)
135 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
136 .required()
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200137 private val flagValuesPath by
138 option("--flag-values")
139 .help(
140 """
141 Path to aconfig parsed_flags binary proto file.
142 Tip: `m all_aconfig_declarations` will generate a file that includes all information about all flags.
143 """)
144 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
145 .required()
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200146 private val apiVersionsPath by
147 option("--api-versions")
148 .help(
149 """
150 Path to API versions XML file.
151 Usually named xml-versions.xml.
152 Tip: `m sdk dist` will generate a file that includes all platform and mainline APIs.
153 """)
154 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
155 .required()
Mårten Kongstad20de4052024-04-16 11:33:56 +0200156
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200157 override fun run() {
Mårten Kongstad20de4052024-04-16 11:33:56 +0200158 val flaggedSymbols =
159 apiSignaturePath.toFile().inputStream().use {
160 parseApiSignature(apiSignaturePath.toString(), it)
161 }
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200162 val flags = flagValuesPath.toFile().inputStream().use { parseFlagValues(it) }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200163 val exportedSymbols = apiVersionsPath.toFile().inputStream().use { parseApiVersions(it) }
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200164 val errors = findErrors(flaggedSymbols, flags, exportedSymbols)
165 for (e in errors) {
166 println(e)
167 }
168 throw ProgramResult(errors.size)
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200169 }
170}
171
Mårten Kongstad20de4052024-04-16 11:33:56 +0200172internal fun parseApiSignature(path: String, input: InputStream): Set<Pair<Symbol, Flag>> {
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200173 // TODO(334870672): add support for metods
Mårten Kongstad20de4052024-04-16 11:33:56 +0200174 val output = mutableSetOf<Pair<Symbol, Flag>>()
175 val visitor =
176 object : BaseItemVisitor() {
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200177 override fun visitClass(cls: ClassItem) {
178 getFlagOrNull(cls)?.let { flag ->
179 val symbol = Symbol.create(cls.baselineElementId())
180 output.add(Pair(symbol, flag))
Mårten Kongstad20de4052024-04-16 11:33:56 +0200181 }
182 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200183
184 override fun visitField(field: FieldItem) {
185 getFlagOrNull(field)?.let { flag ->
186 val symbol = Symbol.create(field.baselineElementId())
187 output.add(Pair(symbol, flag))
188 }
189 }
190
Mårten Kongstad40da9702024-04-27 01:42:51 +0200191 override fun visitMethod(method: MethodItem) {
192 getFlagOrNull(method)?.let { flag ->
193 val name = buildString {
194 append(method.containingClass().qualifiedName())
195 append(".")
196 append(method.name())
197 append("(")
198 // TODO(334870672): replace this early return with proper parsing of the command line
199 // arguments, followed by translation to Lname/of/class; + III format
200 if (!method.parameters().isEmpty()) {
201 return
202 }
203 append(")")
204 }
205 val symbol = Symbol.create(name)
206 output.add(Pair(symbol, flag))
207 }
208 }
209
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200210 private fun getFlagOrNull(item: Item): Flag? {
211 return item.modifiers
212 .findAnnotation("android.annotation.FlaggedApi")
213 ?.findAttribute("value")
214 ?.value
215 ?.let { Flag(it.value() as String) }
216 }
Mårten Kongstad20de4052024-04-16 11:33:56 +0200217 }
218 val codebase = ApiFile.parseApi(path, input)
219 codebase.accept(visitor)
220 return output
221}
222
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200223internal fun parseFlagValues(input: InputStream): Map<Flag, Boolean> {
224 val parsedFlags = Aconfig.parsed_flags.parseFrom(input).getParsedFlagList()
225 return parsedFlags.associateBy(
226 { Flag("${it.getPackage()}.${it.getName()}") },
227 { it.getState() == Aconfig.flag_state.ENABLED })
228}
229
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200230internal fun parseApiVersions(input: InputStream): Set<Symbol> {
231 fun Node.getAttribute(name: String): String? = getAttributes()?.getNamedItem(name)?.getNodeValue()
232
233 val output = mutableSetOf<Symbol>()
234 val factory = DocumentBuilderFactory.newInstance()
235 val parser = factory.newDocumentBuilder()
236 val document = parser.parse(input)
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200237
238 val classes = document.getElementsByTagName("class")
239 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
240 for (i in 0.rangeUntil(classes.getLength())) {
241 val cls = classes.item(i)
242 val className =
243 requireNotNull(cls.getAttribute("name")) {
244 "Bad XML: <class> element without name attribute"
245 }
246 output.add(Symbol.create(className))
247 }
248
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200249 val fields = document.getElementsByTagName("field")
250 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
251 for (i in 0.rangeUntil(fields.getLength())) {
252 val field = fields.item(i)
Mårten Kongstad04e45642024-04-26 05:39:03 +0200253 val fieldName =
254 requireNotNull(field.getAttribute("name")) {
255 "Bad XML: <field> element without name attribute"
256 }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200257 val className =
258 requireNotNull(field.getParentNode()) { "Bad XML: top level <field> element" }
259 .getAttribute("name")
260 output.add(Symbol.create("$className.$fieldName"))
261 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200262
Mårten Kongstad40da9702024-04-27 01:42:51 +0200263 val methods = document.getElementsByTagName("method")
264 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
265 for (i in 0.rangeUntil(methods.getLength())) {
266 val method = methods.item(i)
267 val methodSignature =
268 requireNotNull(method.getAttribute("name")) {
269 "Bad XML: <method> element without name attribute"
270 }
271 val methodSignatureParts = methodSignature.split(Regex("\\(|\\)"))
272 if (methodSignatureParts.size != 3) {
273 throw Exception("Bad XML: method signature '$methodSignature': debug $methodSignatureParts")
274 }
275 var (methodName, methodArgs, methodReturnValue) = methodSignatureParts
276 val packageAndClassName =
277 requireNotNull(method.getParentNode()?.getAttribute("name")) {
278 "Bad XML: top level <method> element, or <class> element missing name attribute"
279 }
280 if (methodName == "<init>") {
281 methodName = packageAndClassName.split("/").last()
282 }
283 output.add(Symbol.create("$packageAndClassName.$methodName($methodArgs)"))
284 }
285
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200286 return output
287}
288
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200289/**
290 * Find errors in the given data.
291 *
292 * @param flaggedSymbolsInSource the set of symbols that are flagged in the source code
293 * @param flags the set of flags and their values
294 * @param symbolsInOutput the set of symbols that are present in the output
295 * @return the set of errors found
296 */
297internal fun findErrors(
298 flaggedSymbolsInSource: Set<Pair<Symbol, Flag>>,
299 flags: Map<Flag, Boolean>,
300 symbolsInOutput: Set<Symbol>
301): Set<ApiError> {
302 val errors = mutableSetOf<ApiError>()
303 for ((symbol, flag) in flaggedSymbolsInSource) {
304 try {
305 if (flags.getValue(flag)) {
306 if (!symbolsInOutput.contains(symbol)) {
307 errors.add(EnabledFlaggedApiNotPresentError(symbol, flag))
308 }
309 } else {
310 if (symbolsInOutput.contains(symbol)) {
311 errors.add(DisabledFlaggedApiIsPresentError(symbol, flag))
312 }
313 }
314 } catch (e: NoSuchElementException) {
315 errors.add(UnknownFlagError(symbol, flag))
316 }
317 }
318 return errors
319}
320
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200321fun main(args: Array<String>) = CheckCommand().main(args)