blob: e7eff176be68c201a183b7582d8dbb93ee3caae7 [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 Kongstad20de4052024-04-16 11:33:56 +020084class CheckCommand :
85 CliktCommand(
86 help =
87 """
88Check that all flagged APIs are used in the correct way.
89
90This tool reads the API signature file and checks that all flagged APIs are used in the correct way.
91
92The tool will exit with a non-zero exit code if any flagged APIs are found to be used in the incorrect way.
93""") {
94 private val apiSignaturePath by
95 option("--api-signature")
96 .help(
97 """
98 Path to API signature file.
99 Usually named *current.txt.
100 Tip: `m frameworks-base-api-current.txt` will generate a file that includes all platform and mainline APIs.
101 """)
102 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
103 .required()
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200104 private val flagValuesPath by
105 option("--flag-values")
106 .help(
107 """
108 Path to aconfig parsed_flags binary proto file.
109 Tip: `m all_aconfig_declarations` will generate a file that includes all information about all flags.
110 """)
111 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
112 .required()
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200113 private val apiVersionsPath by
114 option("--api-versions")
115 .help(
116 """
117 Path to API versions XML file.
118 Usually named xml-versions.xml.
119 Tip: `m sdk dist` will generate a file that includes all platform and mainline APIs.
120 """)
121 .path(mustExist = true, canBeDir = false, mustBeReadable = true)
122 .required()
Mårten Kongstad20de4052024-04-16 11:33:56 +0200123
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200124 override fun run() {
Mårten Kongstad20de4052024-04-16 11:33:56 +0200125 @Suppress("UNUSED_VARIABLE")
126 val flaggedSymbols =
127 apiSignaturePath.toFile().inputStream().use {
128 parseApiSignature(apiSignaturePath.toString(), it)
129 }
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200130 @Suppress("UNUSED_VARIABLE")
131 val flags = flagValuesPath.toFile().inputStream().use { parseFlagValues(it) }
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200132 @Suppress("UNUSED_VARIABLE")
133 val exportedSymbols = apiVersionsPath.toFile().inputStream().use { parseApiVersions(it) }
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200134 throw ProgramResult(0)
135 }
136}
137
Mårten Kongstad20de4052024-04-16 11:33:56 +0200138internal fun parseApiSignature(path: String, input: InputStream): Set<Pair<Symbol, Flag>> {
139 // TODO(334870672): add support for classes and metods
140 val output = mutableSetOf<Pair<Symbol, Flag>>()
141 val visitor =
142 object : BaseItemVisitor() {
143 override fun visitField(field: FieldItem) {
144 val flag =
145 field.modifiers
146 .findAnnotation("android.annotation.FlaggedApi")
147 ?.findAttribute("value")
148 ?.value
149 ?.value() as? String
150 if (flag != null) {
151 val symbol = Symbol.create(field.baselineElementId())
152 output.add(Pair(symbol, Flag(flag)))
153 }
154 }
155 }
156 val codebase = ApiFile.parseApi(path, input)
157 codebase.accept(visitor)
158 return output
159}
160
Mårten Kongstad387ff6c2024-04-16 12:42:14 +0200161internal fun parseFlagValues(input: InputStream): Map<Flag, Boolean> {
162 val parsedFlags = Aconfig.parsed_flags.parseFrom(input).getParsedFlagList()
163 return parsedFlags.associateBy(
164 { Flag("${it.getPackage()}.${it.getName()}") },
165 { it.getState() == Aconfig.flag_state.ENABLED })
166}
167
Mårten Kongstadb673d3b2024-04-16 18:34:20 +0200168internal fun parseApiVersions(input: InputStream): Set<Symbol> {
169 fun Node.getAttribute(name: String): String? = getAttributes()?.getNamedItem(name)?.getNodeValue()
170
171 val output = mutableSetOf<Symbol>()
172 val factory = DocumentBuilderFactory.newInstance()
173 val parser = factory.newDocumentBuilder()
174 val document = parser.parse(input)
175 val fields = document.getElementsByTagName("field")
176 // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
177 for (i in 0.rangeUntil(fields.getLength())) {
178 val field = fields.item(i)
179 val fieldName = field.getAttribute("name")
180 val className =
181 requireNotNull(field.getParentNode()) { "Bad XML: top level <field> element" }
182 .getAttribute("name")
183 output.add(Symbol.create("$className.$fieldName"))
184 }
185 return output
186}
187
Mårten Kongstadacfeb112024-04-16 10:30:26 +0200188fun main(args: Array<String>) = CheckCommand().main(args)