blob: 918a5d9bf3b748bed6178af3003fb914c8603381 [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 Kongstad20de4052024-04-16 11:33:56 +020025import com.android.tools.metalava.model.text.ApiFile
Mårten Kongstadacfeb112024-04-16 10:30:26 +020026import com.github.ajalt.clikt.core.CliktCommand
27import com.github.ajalt.clikt.core.ProgramResult
Mårten Kongstad20de4052024-04-16 11:33:56 +020028import com.github.ajalt.clikt.parameters.options.help
29import com.github.ajalt.clikt.parameters.options.option
30import com.github.ajalt.clikt.parameters.options.required
31import com.github.ajalt.clikt.parameters.types.path
32import java.io.InputStream
Mårten Kongstadb673d3b2024-04-16 18:34:20 +020033import javax.xml.parsers.DocumentBuilderFactory
34import org.w3c.dom.Node
Mårten Kongstadacfeb112024-04-16 10:30:26 +020035
Mårten Kongstade0179972024-04-16 11:16:44 +020036/**
37 * Class representing the fully qualified name of a class, method or field.
38 *
39 * This tool reads a multitude of input formats all of which represents the fully qualified path to
40 * a Java symbol slightly differently. To keep things consistent, all parsed APIs are converted to
41 * Symbols.
42 *
43 * All parts of the fully qualified name of the Symbol are separated by a dot, e.g.:
44 * <pre>
45 * package.class.inner-class.field
46 * </pre>
47 */
48@JvmInline
49internal value class Symbol(val name: String) {
50 companion object {
51 private val FORBIDDEN_CHARS = listOf('/', '#', '$')
52
53 /** Create a new Symbol from a String that may include delimiters other than dot. */
54 fun create(name: String): Symbol {
55 var sanitizedName = name
56 for (ch in FORBIDDEN_CHARS) {
57 sanitizedName = sanitizedName.replace(ch, '.')
58 }
59 return Symbol(sanitizedName)
60 }
61 }
62
63 init {
64 require(!name.isEmpty()) { "empty string" }
65 for (ch in FORBIDDEN_CHARS) {
66 require(!name.contains(ch)) { "$name: contains $ch" }
67 }
68 }
69
70 override fun toString(): String = name.toString()
71}
72
Mårten Kongstaddc3fc2e2024-04-16 11:23:22 +020073/**
74 * Class representing the fully qualified name of an aconfig flag.
75 *
76 * This includes both the flag's package and name, separated by a dot, e.g.:
77 * <pre>
78 * com.android.aconfig.test.disabled_ro
79 * <pre>
80 */
81@JvmInline
82internal value class Flag(val name: String) {
83 override fun toString(): String = name.toString()
84}
85
Mårten Kongstad9238a3a2024-04-16 13:19:50 +020086internal sealed class ApiError {
87 abstract val symbol: Symbol
88 abstract val flag: Flag
89}
90
91internal data class EnabledFlaggedApiNotPresentError(
92 override val symbol: Symbol,
93 override val flag: Flag
94) : ApiError() {
95 override fun toString(): String {
96 return "error: enabled @FlaggedApi not present in built artifact: symbol=$symbol flag=$flag"
97 }
98}
99
100internal data class DisabledFlaggedApiIsPresentError(
101 override val symbol: Symbol,
102 override val flag: Flag
103) : ApiError() {
104 override fun toString(): String {
105 return "error: disabled @FlaggedApi is present in built artifact: symbol=$symbol flag=$flag"
106 }
107}
108
109internal data class UnknownFlagError(override val symbol: Symbol, override val flag: Flag) :
110 ApiError() {
111 override fun toString(): String {
112 return "error: unknown flag: symbol=$symbol flag=$flag"
113 }
114}
115
Mårten Kongstad20de4052024-04-16 11:33:56 +0200116class CheckCommand :
117 CliktCommand(
118 help =
119 """
120Check that all flagged APIs are used in the correct way.
121
122This tool reads the API signature file and checks that all flagged APIs are used in the correct way.
123
124The tool will exit with a non-zero exit code if any flagged APIs are found to be used in the incorrect way.
125""") {
126 private val apiSignaturePath by
127 option("--api-signature")
128 .help(
129 """
130 Path to API signature file.
131 Usually named *current.txt.
132 Tip: `m frameworks-base-api-current.txt` will generate a file that includes all platform and mainline APIs.
133 """)
134 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
135 .required()
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200136 private val flagValuesPath by
137 option("--flag-values")
138 .help(
139 """
140 Path to aconfig parsed_flags binary proto file.
141 Tip: `m all_aconfig_declarations` will generate a file that includes all information about all flags.
142 """)
143 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
144 .required()
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200145 private val apiVersionsPath by
146 option("--api-versions")
147 .help(
148 """
149 Path to API versions XML file.
150 Usually named xml-versions.xml.
151 Tip: `m sdk dist` will generate a file that includes all platform and mainline APIs.
152 """)
153 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
154 .required()
Mårten Kongstad20de4052024-04-16 11:33:56 +0200155
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200156 override fun run() {
Mårten Kongstad20de4052024-04-16 11:33:56 +0200157 val flaggedSymbols =
158 apiSignaturePath.toFile().inputStream().use {
159 parseApiSignature(apiSignaturePath.toString(), it)
160 }
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200161 val flags = flagValuesPath.toFile().inputStream().use { parseFlagValues(it) }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200162 val exportedSymbols = apiVersionsPath.toFile().inputStream().use { parseApiVersions(it) }
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200163 val errors = findErrors(flaggedSymbols, flags, exportedSymbols)
164 for (e in errors) {
165 println(e)
166 }
167 throw ProgramResult(errors.size)
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200168 }
169}
170
Mårten Kongstad20de4052024-04-16 11:33:56 +0200171internal fun parseApiSignature(path: String, input: InputStream): Set<Pair<Symbol, Flag>> {
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200172 // TODO(334870672): add support for metods
Mårten Kongstad20de4052024-04-16 11:33:56 +0200173 val output = mutableSetOf<Pair<Symbol, Flag>>()
174 val visitor =
175 object : BaseItemVisitor() {
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200176 override fun visitClass(cls: ClassItem) {
177 getFlagOrNull(cls)?.let { flag ->
178 val symbol = Symbol.create(cls.baselineElementId())
179 output.add(Pair(symbol, flag))
Mårten Kongstad20de4052024-04-16 11:33:56 +0200180 }
181 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200182
183 override fun visitField(field: FieldItem) {
184 getFlagOrNull(field)?.let { flag ->
185 val symbol = Symbol.create(field.baselineElementId())
186 output.add(Pair(symbol, flag))
187 }
188 }
189
190 private fun getFlagOrNull(item: Item): Flag? {
191 return item.modifiers
192 .findAnnotation("android.annotation.FlaggedApi")
193 ?.findAttribute("value")
194 ?.value
195 ?.let { Flag(it.value() as String) }
196 }
Mårten Kongstad20de4052024-04-16 11:33:56 +0200197 }
198 val codebase = ApiFile.parseApi(path, input)
199 codebase.accept(visitor)
200 return output
201}
202
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200203internal fun parseFlagValues(input: InputStream): Map<Flag, Boolean> {
204 val parsedFlags = Aconfig.parsed_flags.parseFrom(input).getParsedFlagList()
205 return parsedFlags.associateBy(
206 { Flag("${it.getPackage()}.${it.getName()}") },
207 { it.getState() == Aconfig.flag_state.ENABLED })
208}
209
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200210internal fun parseApiVersions(input: InputStream): Set<Symbol> {
211 fun Node.getAttribute(name: String): String? = getAttributes()?.getNamedItem(name)?.getNodeValue()
212
213 val output = mutableSetOf<Symbol>()
214 val factory = DocumentBuilderFactory.newInstance()
215 val parser = factory.newDocumentBuilder()
216 val document = parser.parse(input)
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200217
218 val classes = document.getElementsByTagName("class")
219 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
220 for (i in 0.rangeUntil(classes.getLength())) {
221 val cls = classes.item(i)
222 val className =
223 requireNotNull(cls.getAttribute("name")) {
224 "Bad XML: <class> element without name attribute"
225 }
226 output.add(Symbol.create(className))
227 }
228
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200229 val fields = document.getElementsByTagName("field")
230 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
231 for (i in 0.rangeUntil(fields.getLength())) {
232 val field = fields.item(i)
Mårten Kongstad04e45642024-04-26 05:39:03 +0200233 val fieldName =
234 requireNotNull(field.getAttribute("name")) {
235 "Bad XML: <field> element without name attribute"
236 }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200237 val className =
238 requireNotNull(field.getParentNode()) { "Bad XML: top level <field> element" }
239 .getAttribute("name")
240 output.add(Symbol.create("$className.$fieldName"))
241 }
Mårten Kongstad18ff19a2024-04-26 05:48:57 +0200242
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200243 return output
244}
245
Mårten Kongstad9238a3a2024-04-16 13:19:50 +0200246/**
247 * Find errors in the given data.
248 *
249 * @param flaggedSymbolsInSource the set of symbols that are flagged in the source code
250 * @param flags the set of flags and their values
251 * @param symbolsInOutput the set of symbols that are present in the output
252 * @return the set of errors found
253 */
254internal fun findErrors(
255 flaggedSymbolsInSource: Set<Pair<Symbol, Flag>>,
256 flags: Map<Flag, Boolean>,
257 symbolsInOutput: Set<Symbol>
258): Set<ApiError> {
259 val errors = mutableSetOf<ApiError>()
260 for ((symbol, flag) in flaggedSymbolsInSource) {
261 try {
262 if (flags.getValue(flag)) {
263 if (!symbolsInOutput.contains(symbol)) {
264 errors.add(EnabledFlaggedApiNotPresentError(symbol, flag))
265 }
266 } else {
267 if (symbolsInOutput.contains(symbol)) {
268 errors.add(DisabledFlaggedApiIsPresentError(symbol, flag))
269 }
270 }
271 } catch (e: NoSuchElementException) {
272 errors.add(UnknownFlagError(symbol, flag))
273 }
274 }
275 return errors
276}
277
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200278fun main(args: Array<String>) = CheckCommand().main(args)