diff options
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.kt | 354 |
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> + * <fully-qualified-name-of-flag< <state-of-flag< <API< + * </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) |