Merge changes Ia6dfcfa8,I8d93c230,I4db7ff47,I003535c7,I8c0619fa into main

* changes:
  check-flagged-apis: consider interfaces when looking up symbol
  check-flagged-apis: skip self-referential interfaces
  check-flagged-apis: record interfaces when parsing classes
  check-flagged-apis: add more details to Symbol class
  check-flagged-apis: api-versions.xml: correctly parse nested class ctor
This commit is contained in:
Treehugger Robot
2024-05-07 13:42:25 +00:00
committed by Gerrit Code Review
2 changed files with 178 additions and 54 deletions

View File

@@ -95,20 +95,20 @@ class CheckFlaggedApisTest {
fun testParseApiSignature() { fun testParseApiSignature() {
val expected = val expected =
setOf( setOf(
Pair(Symbol("android/Clazz"), Flag("android.flag.foo")), Pair(Symbol.createClass("android/Clazz", setOf()), Flag("android.flag.foo")),
Pair(Symbol("android/Clazz/Clazz()"), Flag("android.flag.foo")), Pair(Symbol.createMethod("android/Clazz", "Clazz()"), Flag("android.flag.foo")),
Pair(Symbol("android/Clazz/FOO"), Flag("android.flag.foo")), Pair(Symbol.createField("android/Clazz", "FOO"), Flag("android.flag.foo")),
Pair(Symbol("android/Clazz/getErrorCode()"), Flag("android.flag.foo")), Pair(Symbol.createMethod("android/Clazz", "getErrorCode()"), Flag("android.flag.foo")),
Pair( Pair(
Symbol("android/Clazz/setData(I[[ILandroid/util/Utility;)"), Symbol.createMethod("android/Clazz", "setData(I[[ILandroid/util/Utility;)"),
Flag("android.flag.foo")), Flag("android.flag.foo")),
Pair( Pair(
Symbol("android/Clazz/setVariableData(I[Landroid/util/Atom;)"), Symbol.createMethod("android/Clazz", "setVariableData(I[Landroid/util/Atom;)"),
Flag("android.flag.foo")), Flag("android.flag.foo")),
Pair( Pair(
Symbol("android/Clazz/innerClassArg(Landroid/Clazz/Builder;)"), Symbol.createMethod("android/Clazz", "innerClassArg(Landroid/Clazz/Builder;)"),
Flag("android.flag.foo")), Flag("android.flag.foo")),
Pair(Symbol("android/Clazz/Builder"), Flag("android.flag.bar")), Pair(Symbol.createClass("android/Clazz/Builder", setOf()), Flag("android.flag.bar")),
) )
val actual = parseApiSignature("in-memory", API_SIGNATURE.byteInputStream()) val actual = parseApiSignature("in-memory", API_SIGNATURE.byteInputStream())
assertEquals(expected, actual) assertEquals(expected, actual)
@@ -126,19 +126,40 @@ class CheckFlaggedApisTest {
fun testParseApiVersions() { fun testParseApiVersions() {
val expected: Set<Symbol> = val expected: Set<Symbol> =
setOf( setOf(
Symbol("android/Clazz"), Symbol.createClass("android/Clazz", setOf()),
Symbol("android/Clazz/Clazz()"), Symbol.createMethod("android/Clazz", "Clazz()"),
Symbol("android/Clazz/FOO"), Symbol.createField("android/Clazz", "FOO"),
Symbol("android/Clazz/getErrorCode()"), Symbol.createMethod("android/Clazz", "getErrorCode()"),
Symbol("android/Clazz/setData(I[[ILandroid/util/Utility;)"), Symbol.createMethod("android/Clazz", "setData(I[[ILandroid/util/Utility;)"),
Symbol("android/Clazz/setVariableData(I[Landroid/util/Atom;)"), Symbol.createMethod("android/Clazz", "setVariableData(I[Landroid/util/Atom;)"),
Symbol("android/Clazz/innerClassArg(Landroid/Clazz/Builder;)"), Symbol.createMethod("android/Clazz", "innerClassArg(Landroid/Clazz/Builder;)"),
Symbol("android/Clazz/Builder"), Symbol.createClass("android/Clazz/Builder", setOf()),
) )
val actual = parseApiVersions(API_VERSIONS.byteInputStream()) val actual = parseApiVersions(API_VERSIONS.byteInputStream())
assertEquals(expected, actual) assertEquals(expected, actual)
} }
@Test
fun testParseApiVersionsNestedClasses() {
val apiVersions =
"""
<?xml version="1.0" encoding="utf-8"?>
<api version="3">
<class name="android/Clazz${'$'}Foo${'$'}Bar" since="1">
<method name="&lt;init>()V"/>
</class>
</api>
"""
.trim()
val expected: Set<Symbol> =
setOf(
Symbol.createClass("android/Clazz/Foo/Bar", setOf()),
Symbol.createMethod("android/Clazz/Foo/Bar", "Bar()"),
)
val actual = parseApiVersions(apiVersions.byteInputStream())
assertEquals(expected, actual)
}
@Test @Test
fun testFindErrorsNoErrors() { fun testFindErrorsNoErrors() {
val expected = setOf<ApiError>() val expected = setOf<ApiError>()
@@ -150,27 +171,70 @@ class CheckFlaggedApisTest {
assertEquals(expected, actual) assertEquals(expected, actual)
} }
@Test
fun testFindErrorsVerifyImplements() {
val apiSignature =
"""
// Signature format: 2.0
package android {
@FlaggedApi("android.flag.foo") public final class Clazz implements android.Interface {
method @FlaggedApi("android.flag.foo") public boolean foo();
method @FlaggedApi("android.flag.foo") public boolean bar();
}
public interface Interface {
method public boolean bar();
}
}
"""
.trim()
val apiVersions =
"""
<?xml version="1.0" encoding="utf-8"?>
<api version="3">
<class name="android/Clazz" since="1">
<implements name="android/Interface"/>
<method name="foo()Z"/>
</class>
<class name="android/Interface" since="1">
<method name="bar()Z"/>
</class>
</api>
"""
.trim()
val expected = setOf<ApiError>()
val actual =
findErrors(
parseApiSignature("in-memory", apiSignature.byteInputStream()),
parseFlagValues(generateFlagsProto(ENABLED, ENABLED)),
parseApiVersions(apiVersions.byteInputStream()))
assertEquals(expected, actual)
}
@Test @Test
fun testFindErrorsDisabledFlaggedApiIsPresent() { fun testFindErrorsDisabledFlaggedApiIsPresent() {
val expected = val expected =
setOf<ApiError>( setOf<ApiError>(
DisabledFlaggedApiIsPresentError(Symbol("android/Clazz"), Flag("android.flag.foo")),
DisabledFlaggedApiIsPresentError( DisabledFlaggedApiIsPresentError(
Symbol("android/Clazz/Clazz()"), Flag("android.flag.foo")), Symbol.createClass("android/Clazz", setOf()), Flag("android.flag.foo")),
DisabledFlaggedApiIsPresentError(Symbol("android/Clazz/FOO"), Flag("android.flag.foo")),
DisabledFlaggedApiIsPresentError( DisabledFlaggedApiIsPresentError(
Symbol("android/Clazz/getErrorCode()"), Flag("android.flag.foo")), Symbol.createMethod("android/Clazz", "Clazz()"), Flag("android.flag.foo")),
DisabledFlaggedApiIsPresentError( DisabledFlaggedApiIsPresentError(
Symbol("android/Clazz/setData(I[[ILandroid/util/Utility;)"), Symbol.createField("android/Clazz", "FOO"), Flag("android.flag.foo")),
DisabledFlaggedApiIsPresentError(
Symbol.createMethod("android/Clazz", "getErrorCode()"), Flag("android.flag.foo")),
DisabledFlaggedApiIsPresentError(
Symbol.createMethod("android/Clazz", "setData(I[[ILandroid/util/Utility;)"),
Flag("android.flag.foo")), Flag("android.flag.foo")),
DisabledFlaggedApiIsPresentError( DisabledFlaggedApiIsPresentError(
Symbol("android/Clazz/setVariableData(I[Landroid/util/Atom;)"), Symbol.createMethod("android/Clazz", "setVariableData(I[Landroid/util/Atom;)"),
Flag("android.flag.foo")), Flag("android.flag.foo")),
DisabledFlaggedApiIsPresentError( DisabledFlaggedApiIsPresentError(
Symbol("android/Clazz/innerClassArg(Landroid/Clazz/Builder;)"), Symbol.createMethod("android/Clazz", "innerClassArg(Landroid/Clazz/Builder;)"),
Flag("android.flag.foo")), Flag("android.flag.foo")),
DisabledFlaggedApiIsPresentError( DisabledFlaggedApiIsPresentError(
Symbol("android/Clazz/Builder"), Flag("android.flag.bar")), Symbol.createClass("android/Clazz/Builder", setOf()), Flag("android.flag.bar")),
) )
val actual = val actual =
findErrors( findErrors(

View File

@@ -54,29 +54,41 @@ import org.w3c.dom.Node
* *
* 1. https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3.2 * 1. https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3.2
*/ */
@JvmInline internal sealed class Symbol {
internal value class Symbol(val name: String) {
companion object { companion object {
private val FORBIDDEN_CHARS = listOf('#', '$', '.') private val FORBIDDEN_CHARS = listOf('#', '$', '.')
/** Create a new Symbol from a String that may include delimiters other than dot. */ fun createClass(clazz: String, interfaces: Set<String>): Symbol {
fun create(name: String): Symbol { return ClassSymbol(toInternalFormat(clazz), interfaces.map { toInternalFormat(it) }.toSet())
var sanitizedName = name }
fun createField(clazz: String, field: String): Symbol {
require(!field.contains("(") && !field.contains(")"))
return MemberSymbol(toInternalFormat(clazz), toInternalFormat(field))
}
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) { for (ch in FORBIDDEN_CHARS) {
sanitizedName = sanitizedName.replace(ch, '/') internalName = internalName.replace(ch, '/')
} }
return Symbol(sanitizedName) return internalName
} }
} }
init { abstract fun toPrettyString(): String
require(!name.isEmpty()) { "empty string" } }
for (ch in FORBIDDEN_CHARS) {
require(!name.contains(ch)) { "$name: contains $ch" }
}
}
override fun toString(): String = name.toString() internal data class ClassSymbol(val clazz: 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"
} }
/** /**
@@ -102,7 +114,7 @@ internal data class EnabledFlaggedApiNotPresentError(
override val flag: Flag override val flag: Flag
) : ApiError() { ) : ApiError() {
override fun toString(): String { 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"
} }
} }
@@ -111,14 +123,14 @@ internal data class DisabledFlaggedApiIsPresentError(
override val flag: Flag override val flag: Flag
) : ApiError() { ) : ApiError() {
override fun toString(): String { 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) : internal data class UnknownFlagError(override val symbol: Symbol, override val flag: Flag) :
ApiError() { ApiError() {
override fun toString(): String { override fun toString(): String {
return "error: unknown flag: symbol=$symbol flag=$flag" return "error: unknown flag: symbol=${symbol.toPrettyString()} flag=$flag"
} }
} }
@@ -183,29 +195,34 @@ internal fun parseApiSignature(path: String, input: InputStream): Set<Pair<Symbo
object : BaseItemVisitor() { object : BaseItemVisitor() {
override fun visitClass(cls: ClassItem) { override fun visitClass(cls: ClassItem) {
getFlagOrNull(cls)?.let { flag -> getFlagOrNull(cls)?.let { flag ->
val symbol = Symbol.create(cls.baselineElementId()) val symbol =
Symbol.createClass(
cls.baselineElementId(),
cls.allInterfaces()
.map { it.baselineElementId() }
.filter { it != cls.baselineElementId() }
.toSet())
output.add(Pair(symbol, flag)) output.add(Pair(symbol, flag))
} }
} }
override fun visitField(field: FieldItem) { override fun visitField(field: FieldItem) {
getFlagOrNull(field)?.let { flag -> getFlagOrNull(field)?.let { flag ->
val symbol = Symbol.create(field.baselineElementId()) val symbol =
Symbol.createField(field.containingClass().baselineElementId(), field.name())
output.add(Pair(symbol, flag)) output.add(Pair(symbol, flag))
} }
} }
override fun visitMethod(method: MethodItem) { override fun visitMethod(method: MethodItem) {
getFlagOrNull(method)?.let { flag -> getFlagOrNull(method)?.let { flag ->
val name = buildString { val methodName = buildString {
append(method.containingClass().qualifiedName())
append(".")
append(method.name()) append(method.name())
append("(") append("(")
method.parameters().joinTo(this, separator = "") { it.type().internalName() } method.parameters().joinTo(this, separator = "") { it.type().internalName() }
append(")") append(")")
} }
val symbol = Symbol.create(name) val symbol = Symbol.createMethod(method.containingClass().qualifiedName(), methodName)
output.add(Pair(symbol, flag)) output.add(Pair(symbol, flag))
} }
} }
@@ -246,7 +263,19 @@ internal fun parseApiVersions(input: InputStream): Set<Symbol> {
requireNotNull(cls.getAttribute("name")) { requireNotNull(cls.getAttribute("name")) {
"Bad XML: <class> element without name attribute" "Bad XML: <class> element without name attribute"
} }
output.add(Symbol.create(className.replace("/", "."))) val interfaces = mutableSetOf<String>()
val children = cls.getChildNodes()
for (j in 0.rangeUntil(children.getLength())) {
val child = children.item(j)
if (child.getNodeName() == "implements") {
val interfaceName =
requireNotNull(child.getAttribute("name")) {
"Bad XML: <implements> element without name attribute"
}
interfaces.add(interfaceName)
}
}
output.add(Symbol.createClass(className, interfaces))
} }
val fields = document.getElementsByTagName("field") val fields = document.getElementsByTagName("field")
@@ -261,7 +290,7 @@ internal fun parseApiVersions(input: InputStream): Set<Symbol> {
requireNotNull(field.getParentNode()?.getAttribute("name")) { requireNotNull(field.getParentNode()?.getAttribute("name")) {
"Bad XML: top level <field> element" "Bad XML: top level <field> element"
} }
output.add(Symbol.create("${className.replace("/", ".")}.$fieldName")) output.add(Symbol.createField(className, fieldName))
} }
val methods = document.getElementsByTagName("method") val methods = document.getElementsByTagName("method")
@@ -279,12 +308,13 @@ internal fun parseApiVersions(input: InputStream): Set<Symbol> {
var (methodName, methodArgs, _) = methodSignatureParts var (methodName, methodArgs, _) = methodSignatureParts
val packageAndClassName = val packageAndClassName =
requireNotNull(method.getParentNode()?.getAttribute("name")) { requireNotNull(method.getParentNode()?.getAttribute("name")) {
"Bad XML: top level <method> element, or <class> element missing name attribute" "Bad XML: top level <method> element, or <class> element missing name attribute"
} }
.replace("$", "/")
if (methodName == "<init>") { if (methodName == "<init>") {
methodName = packageAndClassName.split("/").last() methodName = packageAndClassName.split("/").last()
} }
output.add(Symbol.create("${packageAndClassName.replace("/", ".")}.$methodName($methodArgs)")) output.add(Symbol.createMethod(packageAndClassName, "$methodName($methodArgs)"))
} }
return output return output
@@ -303,15 +333,45 @@ internal fun findErrors(
flags: Map<Flag, Boolean>, flags: Map<Flag, Boolean>,
symbolsInOutput: Set<Symbol> symbolsInOutput: Set<Symbol>
): Set<ApiError> { ): 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
}
}
return false
}
val errors = mutableSetOf<ApiError>() val errors = mutableSetOf<ApiError>()
for ((symbol, flag) in flaggedSymbolsInSource) { for ((symbol, flag) in flaggedSymbolsInSource) {
try { try {
if (flags.getValue(flag)) { if (flags.getValue(flag)) {
if (!symbolsInOutput.contains(symbol)) { if (!symbolsInOutput.containsSymbol(symbol)) {
errors.add(EnabledFlaggedApiNotPresentError(symbol, flag)) errors.add(EnabledFlaggedApiNotPresentError(symbol, flag))
} }
} else { } else {
if (symbolsInOutput.contains(symbol)) { if (symbolsInOutput.containsSymbol(symbol)) {
errors.add(DisabledFlaggedApiIsPresentError(symbol, flag)) errors.add(DisabledFlaggedApiIsPresentError(symbol, flag))
} }
} }