blob: 84564ba34caf2c2d945c194c0bf1152af53bc943 [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
22import com.android.tools.metalava.model.FieldItem
23import com.android.tools.metalava.model.text.ApiFile
Mårten Kongstadacfeb112024-04-16 10:30:26 +020024import com.github.ajalt.clikt.core.CliktCommand
25import com.github.ajalt.clikt.core.ProgramResult
Mårten Kongstad20de4052024-04-16 11:33:56 +020026import com.github.ajalt.clikt.parameters.options.help
27import com.github.ajalt.clikt.parameters.options.option
28import com.github.ajalt.clikt.parameters.options.required
29import com.github.ajalt.clikt.parameters.types.path
30import java.io.InputStream
Mårten Kongstadb673d3b2024-04-16 18:34:20 +020031import javax.xml.parsers.DocumentBuilderFactory
32import org.w3c.dom.Node
Mårten Kongstadacfeb112024-04-16 10:30:26 +020033
Mårten Kongstade0179972024-04-16 11:16:44 +020034/**
35 * Class representing the fully qualified name of a class, method or field.
36 *
37 * This tool reads a multitude of input formats all of which represents the fully qualified path to
38 * a Java symbol slightly differently. To keep things consistent, all parsed APIs are converted to
39 * Symbols.
40 *
41 * All parts of the fully qualified name of the Symbol are separated by a dot, e.g.:
42 * <pre>
43 * package.class.inner-class.field
44 * </pre>
45 */
46@JvmInline
47internal value class Symbol(val name: String) {
48 companion object {
49 private val FORBIDDEN_CHARS = listOf('/', '#', '$')
50
51 /** Create a new Symbol from a String that may include delimiters other than dot. */
52 fun create(name: String): Symbol {
53 var sanitizedName = name
54 for (ch in FORBIDDEN_CHARS) {
55 sanitizedName = sanitizedName.replace(ch, '.')
56 }
57 return Symbol(sanitizedName)
58 }
59 }
60
61 init {
62 require(!name.isEmpty()) { "empty string" }
63 for (ch in FORBIDDEN_CHARS) {
64 require(!name.contains(ch)) { "$name: contains $ch" }
65 }
66 }
67
68 override fun toString(): String = name.toString()
69}
70
Mårten Kongstaddc3fc2e2024-04-16 11:23:22 +020071/**
72 * Class representing the fully qualified name of an aconfig flag.
73 *
74 * This includes both the flag's package and name, separated by a dot, e.g.:
75 * <pre>
76 * com.android.aconfig.test.disabled_ro
77 * <pre>
78 */
79@JvmInline
80internal value class Flag(val name: String) {
81 override fun toString(): String = name.toString()
82}
83
Mårten Kongstad9238a3a2024-04-16 13:19:50 +020084internal sealed class ApiError {
85 abstract val symbol: Symbol
86 abstract val flag: Flag
87}
88
89internal data class EnabledFlaggedApiNotPresentError(
90 override val symbol: Symbol,
91 override val flag: Flag
92) : ApiError() {
93 override fun toString(): String {
94 return "error: enabled @FlaggedApi not present in built artifact: symbol=$symbol flag=$flag"
95 }
96}
97
98internal data class DisabledFlaggedApiIsPresentError(
99 override val symbol: Symbol,
100 override val flag: Flag
101) : ApiError() {
102 override fun toString(): String {
103 return "error: disabled @FlaggedApi is present in built artifact: symbol=$symbol flag=$flag"
104 }
105}
106
107internal data class UnknownFlagError(override val symbol: Symbol, override val flag: Flag) :
108 ApiError() {
109 override fun toString(): String {
110 return "error: unknown flag: symbol=$symbol flag=$flag"
111 }
112}
113
Mårten Kongstad20de4052024-04-16 11:33:56 +0200114class CheckCommand :
115 CliktCommand(
116 help =
117 """
118Check that all flagged APIs are used in the correct way.
119
120This tool reads the API signature file and checks that all flagged APIs are used in the correct way.
121
122The tool will exit with a non-zero exit code if any flagged APIs are found to be used in the incorrect way.
123""") {
124 private val apiSignaturePath by
125 option("--api-signature")
126 .help(
127 """
128 Path to API signature file.
129 Usually named *current.txt.
130 Tip: `m frameworks-base-api-current.txt` will generate a file that includes all platform and mainline APIs.
131 """)
132 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
133 .required()
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200134 private val flagValuesPath by
135 option("--flag-values")
136 .help(
137 """
138 Path to aconfig parsed_flags binary proto file.
139 Tip: `m all_aconfig_declarations` will generate a file that includes all information about all flags.
140 """)
141 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
142 .required()
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200143 private val apiVersionsPath by
144 option("--api-versions")
145 .help(
146 """
147 Path to API versions XML file.
148 Usually named xml-versions.xml.
149 Tip: `m sdk dist` will generate a file that includes all platform and mainline APIs.
150 """)
151 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
152 .required()
Mårten Kongstad20de4052024-04-16 11:33:56 +0200153
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200154 override fun run() {
Mårten Kongstad20de4052024-04-16 11:33:56 +0200155 val flaggedSymbols =
156 apiSignaturePath.toFile().inputStream().use {
157 parseApiSignature(apiSignaturePath.toString(), it)
158 }
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200159 val flags = flagValuesPath.toFile().inputStream().use { parseFlagValues(it) }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200160 val exportedSymbols = apiVersionsPath.toFile().inputStream().use { parseApiVersions(it) }
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200161 val errors = findErrors(flaggedSymbols, flags, exportedSymbols)
162 for (e in errors) {
163 println(e)
164 }
165 throw ProgramResult(errors.size)
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200166 }
167}
168
Mårten Kongstad20de4052024-04-16 11:33:56 +0200169internal fun parseApiSignature(path: String, input: InputStream): Set<Pair<Symbol, Flag>> {
170 // TODO(334870672): add support for classes and metods
171 val output = mutableSetOf<Pair<Symbol, Flag>>()
172 val visitor =
173 object : BaseItemVisitor() {
174 override fun visitField(field: FieldItem) {
175 val flag =
176 field.modifiers
177 .findAnnotation("android.annotation.FlaggedApi")
178 ?.findAttribute("value")
179 ?.value
180 ?.value() as? String
181 if (flag != null) {
182 val symbol = Symbol.create(field.baselineElementId())
183 output.add(Pair(symbol, Flag(flag)))
184 }
185 }
186 }
187 val codebase = ApiFile.parseApi(path, input)
188 codebase.accept(visitor)
189 return output
190}
191
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200192internal fun parseFlagValues(input: InputStream): Map<Flag, Boolean> {
193 val parsedFlags = Aconfig.parsed_flags.parseFrom(input).getParsedFlagList()
194 return parsedFlags.associateBy(
195 { Flag("${it.getPackage()}.${it.getName()}") },
196 { it.getState() == Aconfig.flag_state.ENABLED })
197}
198
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200199internal fun parseApiVersions(input: InputStream): Set<Symbol> {
200 fun Node.getAttribute(name: String): String? = getAttributes()?.getNamedItem(name)?.getNodeValue()
201
202 val output = mutableSetOf<Symbol>()
203 val factory = DocumentBuilderFactory.newInstance()
204 val parser = factory.newDocumentBuilder()
205 val document = parser.parse(input)
206 val fields = document.getElementsByTagName("field")
207 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
208 for (i in 0.rangeUntil(fields.getLength())) {
209 val field = fields.item(i)
210 val fieldName = field.getAttribute("name")
211 val className =
212 requireNotNull(field.getParentNode()) { "Bad XML: top level <field> element" }
213 .getAttribute("name")
214 output.add(Symbol.create("$className.$fieldName"))
215 }
216 return output
217}
218
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200219/**
220 * Find errors in the given data.
221 *
222 * @param flaggedSymbolsInSource the set of symbols that are flagged in the source code
223 * @param flags the set of flags and their values
224 * @param symbolsInOutput the set of symbols that are present in the output
225 * @return the set of errors found
226 */
227internal fun findErrors(
228 flaggedSymbolsInSource: Set<Pair<Symbol, Flag>>,
229 flags: Map<Flag, Boolean>,
230 symbolsInOutput: Set<Symbol>
231): Set<ApiError> {
232 val errors = mutableSetOf<ApiError>()
233 for ((symbol, flag) in flaggedSymbolsInSource) {
234 try {
235 if (flags.getValue(flag)) {
236 if (!symbolsInOutput.contains(symbol)) {
237 errors.add(EnabledFlaggedApiNotPresentError(symbol, flag))
238 }
239 } else {
240 if (symbolsInOutput.contains(symbol)) {
241 errors.add(DisabledFlaggedApiIsPresentError(symbol, flag))
242 }
243 }
244 } catch (e: NoSuchElementException) {
245 errors.add(UnknownFlagError(symbol, flag))
246 }
247 }
248 return errors
249}
250
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200251fun main(args: Array<String>) = CheckCommand().main(args)