aboutsummaryrefslogtreecommitdiff
path: root/tools/check-flagged-apis/src/com/android/checkflaggedapis/Main.kt
diff options
context:
space:
mode:
Diffstat (limited to 'tools/check-flagged-apis/src/com/android/checkflaggedapis/Main.kt')
-rw-r--r--tools/check-flagged-apis/src/com/android/checkflaggedapis/Main.kt354
1 files changed, 302 insertions, 52 deletions
diff --git a/tools/check-flagged-apis/src/com/android/checkflaggedapis/Main.kt b/tools/check-flagged-apis/src/com/android/checkflaggedapis/Main.kt
index 918a5d9bf3..1125d393bf 100644
--- a/tools/check-flagged-apis/src/com/android/checkflaggedapis/Main.kt
+++ b/tools/check-flagged-apis/src/com/android/checkflaggedapis/Main.kt
@@ -22,9 +22,11 @@ import com.android.tools.metalava.model.BaseItemVisitor
import com.android.tools.metalava.model.ClassItem
import com.android.tools.metalava.model.FieldItem
import com.android.tools.metalava.model.Item
+import com.android.tools.metalava.model.MethodItem
import com.android.tools.metalava.model.text.ApiFile
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.ProgramResult
+import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.options.help
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
@@ -40,34 +42,61 @@ import org.w3c.dom.Node
* a Java symbol slightly differently. To keep things consistent, all parsed APIs are converted to
* Symbols.
*
- * All parts of the fully qualified name of the Symbol are separated by a dot, e.g.:
+ * Symbols are encoded using the format similar to the one described in section 4.3.2 of the JVM
+ * spec [1], that is, "package.class.inner-class.method(int, int[], android.util.Clazz)" is
+ * represented as
* <pre>
- * package.class.inner-class.field
- * </pre>
+ * package.class.inner-class.method(II[Landroid/util/Clazz;)
+ * <pre>
+ *
+ * Where possible, the format has been simplified (to make translation of the
+ * various input formats easier): for instance, only / is used as delimiter (#
+ * and $ are never used).
+ *
+ * 1. https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3.2
*/
-@JvmInline
-internal value class Symbol(val name: String) {
+internal sealed class Symbol {
companion object {
- private val FORBIDDEN_CHARS = listOf('/', '#', '$')
+ private val FORBIDDEN_CHARS = listOf('#', '$', '.')
+
+ fun createClass(clazz: String, superclass: String?, interfaces: Set<String>): Symbol {
+ return ClassSymbol(
+ toInternalFormat(clazz),
+ superclass?.let { toInternalFormat(it) },
+ interfaces.map { toInternalFormat(it) }.toSet())
+ }
+
+ fun createField(clazz: String, field: String): Symbol {
+ require(!field.contains("(") && !field.contains(")"))
+ return MemberSymbol(toInternalFormat(clazz), toInternalFormat(field))
+ }
- /** Create a new Symbol from a String that may include delimiters other than dot. */
- fun create(name: String): Symbol {
- var sanitizedName = name
+ fun createMethod(clazz: String, method: String): Symbol {
+ return MemberSymbol(toInternalFormat(clazz), toInternalFormat(method))
+ }
+
+ protected fun toInternalFormat(name: String): String {
+ var internalName = name
for (ch in FORBIDDEN_CHARS) {
- sanitizedName = sanitizedName.replace(ch, '.')
+ internalName = internalName.replace(ch, '/')
}
- return Symbol(sanitizedName)
+ return internalName
}
}
- init {
- require(!name.isEmpty()) { "empty string" }
- for (ch in FORBIDDEN_CHARS) {
- require(!name.contains(ch)) { "$name: contains $ch" }
- }
- }
+ abstract fun toPrettyString(): String
+}
- override fun toString(): String = name.toString()
+internal data class ClassSymbol(
+ val clazz: String,
+ val superclass: String?,
+ val interfaces: Set<String>
+) : Symbol() {
+ override fun toPrettyString(): String = "$clazz"
+}
+
+internal data class MemberSymbol(val clazz: String, val member: String) : Symbol() {
+ override fun toPrettyString(): String = "$clazz/$member"
}
/**
@@ -93,7 +122,7 @@ internal data class EnabledFlaggedApiNotPresentError(
override val flag: Flag
) : ApiError() {
override fun toString(): String {
- return "error: enabled @FlaggedApi not present in built artifact: symbol=$symbol flag=$flag"
+ return "error: enabled @FlaggedApi not present in built artifact: symbol=${symbol.toPrettyString()} flag=$flag"
}
}
@@ -102,17 +131,44 @@ internal data class DisabledFlaggedApiIsPresentError(
override val flag: Flag
) : ApiError() {
override fun toString(): String {
- return "error: disabled @FlaggedApi is present in built artifact: symbol=$symbol flag=$flag"
+ return "error: disabled @FlaggedApi is present in built artifact: symbol=${symbol.toPrettyString()} flag=$flag"
}
}
internal data class UnknownFlagError(override val symbol: Symbol, override val flag: Flag) :
ApiError() {
override fun toString(): String {
- return "error: unknown flag: symbol=$symbol flag=$flag"
+ return "error: unknown flag: symbol=${symbol.toPrettyString()} flag=$flag"
}
}
+val ARG_API_SIGNATURE = "--api-signature"
+val ARG_API_SIGNATURE_HELP =
+ """
+Path to API signature file.
+Usually named *current.txt.
+Tip: `m frameworks-base-api-current.txt` will generate a file that includes all platform and mainline APIs.
+"""
+
+val ARG_FLAG_VALUES = "--flag-values"
+val ARG_FLAG_VALUES_HELP =
+ """
+Path to aconfig parsed_flags binary proto file.
+Tip: `m all_aconfig_declarations` will generate a file that includes all information about all flags.
+"""
+
+val ARG_API_VERSIONS = "--api-versions"
+val ARG_API_VERSIONS_HELP =
+ """
+Path to API versions XML file.
+Usually named xml-versions.xml.
+Tip: `m sdk dist` will generate a file that includes all platform and mainline APIs.
+"""
+
+class MainCommand : CliktCommand() {
+ override fun run() {}
+}
+
class CheckCommand :
CliktCommand(
help =
@@ -124,32 +180,18 @@ This tool reads the API signature file and checks that all flagged APIs are used
The tool will exit with a non-zero exit code if any flagged APIs are found to be used in the incorrect way.
""") {
private val apiSignaturePath by
- option("--api-signature")
- .help(
- """
- Path to API signature file.
- Usually named *current.txt.
- Tip: `m frameworks-base-api-current.txt` will generate a file that includes all platform and mainline APIs.
- """)
+ option(ARG_API_SIGNATURE)
+ .help(ARG_API_SIGNATURE_HELP)
.path(mustExist = true, canBeDir = false, mustBeReadable = true)
.required()
private val flagValuesPath by
- option("--flag-values")
- .help(
- """
- Path to aconfig parsed_flags binary proto file.
- Tip: `m all_aconfig_declarations` will generate a file that includes all information about all flags.
- """)
+ option(ARG_FLAG_VALUES)
+ .help(ARG_FLAG_VALUES_HELP)
.path(mustExist = true, canBeDir = false, mustBeReadable = true)
.required()
private val apiVersionsPath by
- option("--api-versions")
- .help(
- """
- Path to API versions XML file.
- Usually named xml-versions.xml.
- Tip: `m sdk dist` will generate a file that includes all platform and mainline APIs.
- """)
+ option(ARG_API_VERSIONS)
+ .help(ARG_API_VERSIONS_HELP)
.path(mustExist = true, canBeDir = false, mustBeReadable = true)
.required()
@@ -168,21 +210,79 @@ The tool will exit with a non-zero exit code if any flagged APIs are found to be
}
}
+class ListCommand :
+ CliktCommand(
+ help =
+ """
+List all flagged APIs and corresponding flags.
+
+The output format is "<fully-qualified-name-of-flag> <state-of-flag> <API>", one line per API.
+
+The output can be post-processed by e.g. piping it to grep to filter out only enabled APIs, or all APIs guarded by a given flag.
+""") {
+ private val apiSignaturePath by
+ option(ARG_API_SIGNATURE)
+ .help(ARG_API_SIGNATURE_HELP)
+ .path(mustExist = true, canBeDir = false, mustBeReadable = true)
+ .required()
+ private val flagValuesPath by
+ option(ARG_FLAG_VALUES)
+ .help(ARG_FLAG_VALUES_HELP)
+ .path(mustExist = true, canBeDir = false, mustBeReadable = true)
+ .required()
+
+ override fun run() {
+ val flaggedSymbols =
+ apiSignaturePath.toFile().inputStream().use {
+ parseApiSignature(apiSignaturePath.toString(), it)
+ }
+ val flags = flagValuesPath.toFile().inputStream().use { parseFlagValues(it) }
+ val output = listFlaggedApis(flaggedSymbols, flags)
+ if (output.isNotEmpty()) {
+ println(output.joinToString("\n"))
+ }
+ }
+}
+
internal fun parseApiSignature(path: String, input: InputStream): Set<Pair<Symbol, Flag>> {
- // TODO(334870672): add support for metods
val output = mutableSetOf<Pair<Symbol, Flag>>()
val visitor =
object : BaseItemVisitor() {
override fun visitClass(cls: ClassItem) {
getFlagOrNull(cls)?.let { flag ->
- val symbol = Symbol.create(cls.baselineElementId())
+ val symbol =
+ Symbol.createClass(
+ cls.baselineElementId(),
+ if (cls.isInterface()) {
+ "java/lang/Object"
+ } else {
+ cls.superClass()?.baselineElementId()
+ },
+ cls.allInterfaces()
+ .map { it.baselineElementId() }
+ .filter { it != cls.baselineElementId() }
+ .toSet())
output.add(Pair(symbol, flag))
}
}
override fun visitField(field: FieldItem) {
getFlagOrNull(field)?.let { flag ->
- val symbol = Symbol.create(field.baselineElementId())
+ val symbol =
+ Symbol.createField(field.containingClass().baselineElementId(), field.name())
+ output.add(Pair(symbol, flag))
+ }
+ }
+
+ override fun visitMethod(method: MethodItem) {
+ getFlagOrNull(method)?.let { flag ->
+ val methodName = buildString {
+ append(method.name())
+ append("(")
+ method.parameters().joinTo(this, separator = "") { it.type().internalName() }
+ append(")")
+ }
+ val symbol = Symbol.createMethod(method.containingClass().qualifiedName(), methodName)
output.add(Pair(symbol, flag))
}
}
@@ -223,7 +323,28 @@ internal fun parseApiVersions(input: InputStream): Set<Symbol> {
requireNotNull(cls.getAttribute("name")) {
"Bad XML: <class> element without name attribute"
}
- output.add(Symbol.create(className))
+ var superclass: String? = null
+ val interfaces = mutableSetOf<String>()
+ val children = cls.getChildNodes()
+ for (j in 0.rangeUntil(children.getLength())) {
+ val child = children.item(j)
+ when (child.getNodeName()) {
+ "extends" -> {
+ superclass =
+ requireNotNull(child.getAttribute("name")) {
+ "Bad XML: <extends> element without name attribute"
+ }
+ }
+ "implements" -> {
+ val interfaceName =
+ requireNotNull(child.getAttribute("name")) {
+ "Bad XML: <implements> element without name attribute"
+ }
+ interfaces.add(interfaceName)
+ }
+ }
+ }
+ output.add(Symbol.createClass(className, superclass, interfaces))
}
val fields = document.getElementsByTagName("field")
@@ -235,9 +356,34 @@ internal fun parseApiVersions(input: InputStream): Set<Symbol> {
"Bad XML: <field> element without name attribute"
}
val className =
- requireNotNull(field.getParentNode()) { "Bad XML: top level <field> element" }
- .getAttribute("name")
- output.add(Symbol.create("$className.$fieldName"))
+ requireNotNull(field.getParentNode()?.getAttribute("name")) {
+ "Bad XML: top level <field> element"
+ }
+ output.add(Symbol.createField(className, fieldName))
+ }
+
+ val methods = document.getElementsByTagName("method")
+ // ktfmt doesn't understand the `..<` range syntax; explicitly call .rangeUntil instead
+ for (i in 0.rangeUntil(methods.getLength())) {
+ val method = methods.item(i)
+ val methodSignature =
+ requireNotNull(method.getAttribute("name")) {
+ "Bad XML: <method> element without name attribute"
+ }
+ val methodSignatureParts = methodSignature.split(Regex("\\(|\\)"))
+ if (methodSignatureParts.size != 3) {
+ throw Exception("Bad XML: method signature '$methodSignature'")
+ }
+ var (methodName, methodArgs, _) = methodSignatureParts
+ val packageAndClassName =
+ requireNotNull(method.getParentNode()?.getAttribute("name")) {
+ "Bad XML: top level <method> element, or <class> element missing name attribute"
+ }
+ .replace("$", "/")
+ if (methodName == "<init>") {
+ methodName = packageAndClassName.split("/").last()
+ }
+ output.add(Symbol.createMethod(packageAndClassName, "$methodName($methodArgs)"))
}
return output
@@ -256,15 +402,88 @@ internal fun findErrors(
flags: Map<Flag, Boolean>,
symbolsInOutput: Set<Symbol>
): Set<ApiError> {
+ fun Set<Symbol>.containsSymbol(symbol: Symbol): Boolean {
+ // trivial case: the symbol is explicitly listed in api-versions.xml
+ if (contains(symbol)) {
+ return true
+ }
+
+ // non-trivial case: the symbol could be part of the surrounding class'
+ // super class or interfaces
+ val (className, memberName) =
+ when (symbol) {
+ is ClassSymbol -> return false
+ is MemberSymbol -> {
+ Pair(symbol.clazz, symbol.member)
+ }
+ }
+ val clazz = find { it is ClassSymbol && it.clazz == className } as? ClassSymbol?
+ if (clazz == null) {
+ return false
+ }
+
+ for (interfaceName in clazz.interfaces) {
+ // createMethod is the same as createField, except it allows parenthesis
+ val interfaceSymbol = Symbol.createMethod(interfaceName, memberName)
+ if (contains(interfaceSymbol)) {
+ return true
+ }
+ }
+
+ if (clazz.superclass != null) {
+ val superclassSymbol = Symbol.createMethod(clazz.superclass, memberName)
+ return containsSymbol(superclassSymbol)
+ }
+
+ return false
+ }
+
+ /**
+ * Returns whether the given flag is enabled for the given symbol.
+ *
+ * A flagged member inside a flagged class is ignored (and the flag value considered disabled) if
+ * the class' flag is disabled.
+ *
+ * @param symbol the symbol to check
+ * @param flag the flag to check
+ * @return whether the flag is enabled for the given symbol
+ */
+ fun isFlagEnabledForSymbol(symbol: Symbol, flag: Flag): Boolean {
+ when (symbol) {
+ is ClassSymbol -> return flags.getValue(flag)
+ is MemberSymbol -> {
+ val memberFlagValue = flags.getValue(flag)
+ if (!memberFlagValue) {
+ return false
+ }
+ // Special case: if the MemberSymbol's flag is enabled, but the outer
+ // ClassSymbol's flag (if the class is flagged) is disabled, consider
+ // the MemberSymbol's flag as disabled:
+ //
+ // @FlaggedApi(this-flag-is-disabled) Clazz {
+ // @FlaggedApi(this-flag-is-enabled) method(); // The Clazz' flag "wins"
+ // }
+ //
+ // Note: the current implementation does not handle nested classes.
+ val classFlagValue =
+ flaggedSymbolsInSource
+ .find { it.first.toPrettyString() == symbol.clazz }
+ ?.let { flags.getValue(it.second) }
+ ?: true
+ return classFlagValue
+ }
+ }
+ }
+
val errors = mutableSetOf<ApiError>()
for ((symbol, flag) in flaggedSymbolsInSource) {
try {
- if (flags.getValue(flag)) {
- if (!symbolsInOutput.contains(symbol)) {
+ if (isFlagEnabledForSymbol(symbol, flag)) {
+ if (!symbolsInOutput.containsSymbol(symbol)) {
errors.add(EnabledFlaggedApiNotPresentError(symbol, flag))
}
} else {
- if (symbolsInOutput.contains(symbol)) {
+ if (symbolsInOutput.containsSymbol(symbol)) {
errors.add(DisabledFlaggedApiIsPresentError(symbol, flag))
}
}
@@ -275,4 +494,35 @@ internal fun findErrors(
return errors
}
-fun main(args: Array<String>) = CheckCommand().main(args)
+/**
+ * Collect all known info about all @FlaggedApi annotated APIs.
+ *
+ * Each API will be represented as a String, on the format
+ * <pre>
+ * &lt;fully-qualified-name-of-flag&lt; &lt;state-of-flag&lt; &lt;API&lt;
+ * </pre>
+ *
+ * @param flaggedSymbolsInSource the set of symbols that are flagged in the source code
+ * @param flags the set of flags and their values
+ * @return a list of Strings encoding API data using the format described above, sorted
+ * alphabetically
+ */
+internal fun listFlaggedApis(
+ flaggedSymbolsInSource: Set<Pair<Symbol, Flag>>,
+ flags: Map<Flag, Boolean>
+): List<String> {
+ val output = mutableListOf<String>()
+ for ((symbol, flag) in flaggedSymbolsInSource) {
+ val flagState =
+ when (flags.get(flag)) {
+ true -> "ENABLED"
+ false -> "DISABLED"
+ null -> "UNKNOWN"
+ }
+ output.add("$flag $flagState ${symbol.toPrettyString()}")
+ }
+ output.sort()
+ return output
+}
+
+fun main(args: Array<String>) = MainCommand().subcommands(CheckCommand(), ListCommand()).main(args)