Merge commit '4b8af4810daf8d9998f9101ef123f4e91c58b1c3' into aosp/master
Bug: 281035030
Test: ./gradlew
Change-Id: Ic8042ed88c02f1a072562925eceacd4ed9fa2359
diff --git a/Android.bp b/Android.bp
index d573be9..0606bad 100644
--- a/Android.bp
+++ b/Android.bp
@@ -30,7 +30,7 @@
"metalava-tools-common-m2-deps",
"metalava-gradle-plugin-deps",
],
- manifest: "manifest.txt",
+ main_class: "com.android.tools.metalava.Driver",
target: {
host: {
dist: {
diff --git a/OWNERS b/OWNERS
index 29a6ff1..4fdf9f1 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,7 +1,5 @@
-tnorbye@google.com
-jeffrygaston@google.com
-asfalcone@google.com
-alanv@google.com
-aurimas@google.com
-emberrose@google.com
-sjgilbert@google.com
+amhk@google.com
+gurpreetgs@google.com
+hansson@google.com
+paulduffin@google.com
+jham@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/README.md b/README.md
index 397de8d..3fa0401 100644
--- a/README.md
+++ b/README.md
@@ -351,7 +351,7 @@
First, metalava provides an ItemVisitor. This lets you visit the API easily.
For example, here's how you can visit every class:
- coebase.accept(object : ItemVisitor() {
+ codebase.accept(object : ItemVisitor() {
override fun visitClass(cls: ClassItem) {
// code operating on the class here
}
diff --git a/USAGE.md b/USAGE.md
index 9cdc59a..f2fb9fa 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -26,6 +26,5 @@
- XML API signature generation (`--api-xml`) for CTS tests and test coverage infrastructure
- Annotation include, exclude, rewrite, passthrough in stubs (`--include-annotations`, `--exclude-all-annotations`,
`--pass-through-annotation`, `--exclude-annotation`)
-- Annotation extraction (`--extract-annotations`, `--include-annotation-classes`, `--rewrite-annotations`,
-`--copy-annotations`, `--include-source-retention`) for generating SDK
+- Annotation extraction (`--extract-annotations`, `--copy-annotations`) for generating the SDK
- Generating SDK metadata (`--sdk-`values`)
diff --git a/build.gradle.kts b/build.gradle.kts
index 28975d3..55684e4 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -99,8 +99,57 @@
archiveFileName.set("metalava-tests.zip")
}
+fun registerTestPrebuiltsSdkTasks(sourceDir: String, destJar: String): String {
+ val basename = sourceDir.replace("/", "-")
+ val javaCompileTaskName = "$basename.classes"
+ val jarTaskName = "$basename.jar"
+
+ project.tasks.register(javaCompileTaskName, JavaCompile::class) {
+ options.compilerArgs = listOf("--patch-module", "java.base=" + file(sourceDir))
+ source = fileTree(sourceDir)
+ classpath = project.files()
+ destinationDirectory.set(File(getBuildDirectory(), javaCompileTaskName))
+ }
+
+ val dir = destJar.substringBeforeLast("/")
+ val filename = destJar.substringAfterLast("/")
+ if (dir == filename) {
+ throw IllegalArgumentException("bad destJar argument '$destJar'")
+ }
+
+ project.tasks.register(jarTaskName, Jar::class) {
+ from(tasks.named(javaCompileTaskName).get().outputs.files.filter { it.isDirectory })
+ archiveFileName.set(filename)
+ destinationDirectory.set(File(getBuildDirectory(), dir))
+ }
+
+ return jarTaskName
+}
+
+val testPrebuiltsSdkApi30 = registerTestPrebuiltsSdkTasks("src/testdata/prebuilts-sdk-test/30", "prebuilts/sdk/30/public/android.jar")
+val testPrebuiltsSdkApi31 = registerTestPrebuiltsSdkTasks("src/testdata/prebuilts-sdk-test/31", "prebuilts/sdk/31/public/android.jar")
+val testPrebuiltsSdkExt1 = registerTestPrebuiltsSdkTasks("src/testdata/prebuilts-sdk-test/extensions/1", "prebuilts/sdk/extensions/1/public/framework-ext.jar")
+val testPrebuiltsSdkExt2 = registerTestPrebuiltsSdkTasks("src/testdata/prebuilts-sdk-test/extensions/2", "prebuilts/sdk/extensions/2/public/framework-ext.jar")
+val testPrebuiltsSdkExt3 = registerTestPrebuiltsSdkTasks("src/testdata/prebuilts-sdk-test/extensions/3", "prebuilts/sdk/extensions/3/public/framework-ext.jar")
+
+project.tasks.register("test-sdk-extensions-info.xml", Copy::class) {
+ from("src/testdata/prebuilts-sdk-test/sdk-extensions-info.xml")
+ into(File(getBuildDirectory(), "prebuilts/sdk"))
+}
+
+project.tasks.register("test-prebuilts-sdk", Assemble::class) {
+ dependsOn(testPrebuiltsSdkApi30)
+ dependsOn(testPrebuiltsSdkApi31)
+ dependsOn(testPrebuiltsSdkExt1)
+ dependsOn(testPrebuiltsSdkExt2)
+ dependsOn(testPrebuiltsSdkExt3)
+ dependsOn("test-sdk-extensions-info.xml")
+}
+
val testTask = tasks.named("test", Test::class.java)
testTask.configure {
+ dependsOn("test-prebuilts-sdk")
+ setEnvironment("METALAVA_TEST_PREBUILTS_SDK_ROOT" to getBuildDirectory().path + "/prebuilts/sdk")
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).takeIf { it > 0 } ?: 1
testLogging.events = hashSetOf(
TestLogEvent.FAILED,
diff --git a/manifest.txt b/manifest.txt
deleted file mode 100644
index 2128bdd..0000000
--- a/manifest.txt
+++ /dev/null
@@ -1 +0,0 @@
-Main-Class: com.android.tools.metalava.Driver
diff --git a/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt b/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt
index b17091f..0bbabef 100644
--- a/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt
+++ b/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt
@@ -522,7 +522,7 @@
private fun mergeAnnotations(xmlElement: Element, item: Item) {
loop@ for (annotationElement in getChildren(xmlElement)) {
val originalName = getAnnotationName(annotationElement)
- val qualifiedName = AnnotationItem.mapName(codebase, originalName) ?: originalName
+ val qualifiedName = AnnotationItem.mapName(originalName) ?: originalName
if (hasNullnessConflicts(item, qualifiedName)) {
continue@loop
}
@@ -801,10 +801,10 @@
override val originalName: String,
override val attributes: List<XmlBackedAnnotationAttribute> = emptyList()
) : DefaultAnnotationItem(codebase) {
- override val qualifiedName: String? = AnnotationItem.mapName(codebase, originalName)
+ override val qualifiedName: String? = AnnotationItem.mapName(originalName)
override fun toSource(target: AnnotationTarget, showDefaultAttrs: Boolean): String {
- val qualifiedName = AnnotationItem.mapName(codebase, qualifiedName, null, target) ?: return ""
+ val qualifiedName = AnnotationItem.mapName(qualifiedName, target) ?: return ""
if (attributes.isEmpty()) {
return "@$qualifiedName"
diff --git a/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt b/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt
index f1c1753..8c7ecc2 100644
--- a/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt
+++ b/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt
@@ -189,36 +189,22 @@
}
// Find default constructor, if one doesn't exist
- val allConstructors = cls.constructors()
- if (allConstructors.isNotEmpty()) {
+ val constructors = cls.filteredConstructors(filter).toList()
+ if (constructors.isNotEmpty()) {
+ // Try to pick the constructor, select first by fewest throwables,
+ // then fewest parameters, then based on order in listFilter.test(cls)
+ cls.stubConstructor = constructors.reduce { first, second -> pickBest(first, second) }
+ return
+ }
- // Try and use a publicly accessible constructor first.
- val constructors = cls.filteredConstructors(filter).toList()
- if (constructors.isNotEmpty()) {
- // Try to pick the constructor, select first by fewest throwables, then fewest parameters,
- // then based on order in listFilter.test(cls)
- cls.stubConstructor = constructors.reduce { first, second -> pickBest(first, second) }
- return
- }
-
- // No accessible constructors are available so one will have to be created, either a private constructor to
- // prevent instances of the class from being created, or a package private constructor for use by subclasses
- // in the package to use. Subclasses outside the package would need a protected or public constructor which
- // would already be part of the API so should have dropped out above.
- //
- // The visibility levels on the constructors from the source can give a clue as to what is required. e.g.
- // if all constructors are private then it is ok for the generated constructor to be private, otherwise it
- // should be package private.
- val allPrivate = allConstructors.asSequence()
- .map { it.isPrivate }
- .reduce { v1, v2 -> v1 and v2 }
-
- val visibilityLevel = if (allPrivate) VisibilityLevel.PRIVATE else VisibilityLevel.PACKAGE_PRIVATE
-
- // No constructors, yet somebody extends this (or private constructor): we have to invent one, such that
- // subclasses can dispatch to it in the stub files etc
+ // For text based codebase, stub constructor needs to be generated even if
+ // cls.constructors() is empty, so that public default constructor is not created.
+ if (cls.constructors().isNotEmpty() || cls.codebase.preFiltered) {
+ // No accessible constructors are available so a package private constructor is created.
+ // Technically, the stub now has a constructor that isn't available at runtime,
+ // but apps creating subclasses inside the android.* package is not supported.
cls.stubConstructor = cls.createDefaultConstructor().also {
- it.mutableModifiers().setVisibilityLevel(visibilityLevel)
+ it.mutableModifiers().setVisibilityLevel(VisibilityLevel.PACKAGE_PRIVATE)
it.hidden = false
it.superConstructor = superClass?.stubConstructor
}
@@ -870,7 +856,7 @@
if (!method.isConstructor()) {
checkTypeReferencesHidden(
method,
- method.returnType()!!
+ method.returnType()
) // returnType is nullable only for constructors
}
@@ -894,7 +880,7 @@
method.mutableModifiers().removeAnnotation(it)
// Have to also clear the annotation out of the return type itself, if it's a type
// use annotation
- method.returnType()?.scrubAnnotations()
+ method.returnType().scrubAnnotations()
}
}
}
@@ -1001,7 +987,7 @@
}
val returnType = m.returnType()
- if (!m.deprecated && !cl.deprecated && returnType != null && returnType.asClass()?.deprecated == true) {
+ if (!m.deprecated && !cl.deprecated && returnType.asClass()?.deprecated == true) {
reporter.report(
Issues.REFERENCES_DEPRECATED, m,
"Return type of deprecated type $returnType in ${cl.qualifiedName()}.${m.name()}(): this method should also be deprecated"
@@ -1010,7 +996,7 @@
var hiddenClass = findHiddenClasses(returnType, stubImportPackages)
if (hiddenClass != null && !hiddenClass.isFromClassPath()) {
- if (hiddenClass.qualifiedName() == returnType?.asClass()?.qualifiedName()) {
+ if (hiddenClass.qualifiedName() == returnType.asClass()?.qualifiedName()) {
// Return type is hidden
reporter.report(
Issues.UNAVAILABLE_SYMBOL, m,
@@ -1057,7 +1043,7 @@
}
val t = m.returnType()
- if (t != null && !t.primitive && !m.deprecated && !cl.deprecated && t.asClass()?.deprecated == true) {
+ if (!t.primitive && !m.deprecated && !cl.deprecated && t.asClass()?.deprecated == true) {
reporter.report(
Issues.REFERENCES_DEPRECATED, m,
"Returning deprecated type $t from ${cl.qualifiedName()}.${m.name()}(): this method should also be deprecated"
@@ -1248,7 +1234,7 @@
cantStripThis(thrown, filter, notStrippable, stubImportPackages, method, "as exception")
}
val returnType = method.returnType()
- if (returnType != null && !returnType.primitive) {
+ if (!returnType.primitive) {
val returnTypeClass = returnType.asClass()
if (returnTypeClass != null) {
cantStripThis(returnTypeClass, filter, notStrippable, stubImportPackages, method, "as return type")
diff --git a/src/main/java/com/android/tools/metalava/ApiLint.kt b/src/main/java/com/android/tools/metalava/ApiLint.kt
index 2c96b74..c0df42f 100644
--- a/src/main/java/com/android/tools/metalava/ApiLint.kt
+++ b/src/main/java/com/android/tools/metalava/ApiLint.kt
@@ -243,11 +243,9 @@
override fun visitMethod(method: MethodItem) {
checkMethod(method, filterReference)
val returnType = method.returnType()
- if (returnType != null) {
- checkType(returnType, method)
- checkNullableCollections(returnType, method)
- checkMethodSuffixListenableFutureReturn(returnType, method)
- }
+ checkType(returnType, method)
+ checkNullableCollections(returnType, method)
+ checkMethodSuffixListenableFutureReturn(returnType, method)
for (parameter in method.parameters()) {
checkType(parameter.type(), parameter)
}
@@ -579,7 +577,7 @@
method.parameters().size == 1 &&
method.name().startsWith("on") &&
!method.parameters().first().type().primitive &&
- method.returnType()?.toTypeString() == Void.TYPE.name
+ method.returnType().toTypeString() == Void.TYPE.name
if (!methods.all(::isSingleParamCallbackMethod)) return
@@ -624,7 +622,6 @@
val prefix = when (className) {
"android.content.Intent" -> "android.intent.action"
"android.provider.Settings" -> "android.settings"
- "android.app.admin.DevicePolicyManager", "android.app.admin.DeviceAdminReceiver" -> "android.app.action"
else -> field.containingClass().containingPackage().qualifiedName() + ".action"
}
val expected = prefix + "." + name.substring(7)
@@ -661,7 +658,6 @@
val packageName = field.containingClass().containingPackage().qualifiedName()
val prefix = when {
className == "android.content.Intent" -> "android.intent.extra"
- packageName == "android.app.admin" -> "android.app.extra"
else -> "$packageName.extra"
}
val expected = prefix + "." + name.substring(6)
@@ -919,7 +915,7 @@
}
private fun checkIntentBuilder(method: MethodItem) {
- if (method.returnType()?.toTypeString() == "android.content.Intent") {
+ if (method.returnType().toTypeString() == "android.content.Intent") {
val name = method.name()
if (name.startsWith("create") && name.endsWith("Intent")) {
return
@@ -1056,27 +1052,25 @@
)
} else if (name.startsWith("set") || name.startsWith("add") || name.startsWith("clear")) {
val returnType = method.returnType()
- if (returnType != null) {
- val returnsClassType = if (
- returnType is PsiTypeItem && clsType is PsiTypeItem
- ) {
- clsType.isAssignableFromWithoutUnboxing(returnType)
- } else {
- // fallback to a limited text based check
- val returnTypeBounds = returnType
- .asTypeParameter(context = method)
- ?.typeBounds()?.map {
- it.toTypeString()
- } ?: emptyList()
- returnTypeBounds.contains(clsType.toTypeString()) || returnType == clsType
- }
- if (!returnsClassType) {
- report(
- SETTER_RETURNS_THIS, method,
- "Methods must return the builder object (return type " +
- "$clsType instead of $returnType): ${method.describe()}"
- )
- }
+ val returnsClassType = if (
+ returnType is PsiTypeItem && clsType is PsiTypeItem
+ ) {
+ clsType.isAssignableFromWithoutUnboxing(returnType)
+ } else {
+ // fallback to a limited text based check
+ val returnTypeBounds = returnType
+ .asTypeParameter(context = method)
+ ?.typeBounds()?.map {
+ it.toTypeString()
+ } ?: emptyList()
+ returnTypeBounds.contains(clsType.toTypeString()) || returnType == clsType
+ }
+ if (!returnsClassType) {
+ report(
+ SETTER_RETURNS_THIS, method,
+ "Methods must return the builder object (return type " +
+ "$clsType instead of $returnType): ${method.describe()}"
+ )
}
if (method.modifiers.isNullable()) {
@@ -1249,16 +1243,14 @@
for (method in methodsAndConstructors) {
val returnType = method.returnType()
- if (returnType != null) { // not a constructor
- val returnTypeRank = getTypeRank(returnType)
- if (returnTypeRank != -1 && returnTypeRank < classRank) {
- report(
- PACKAGE_LAYERING, cls,
- "Method return type `${returnType.toTypeString()}` violates package layering: nothing in `$classPackage` should depend on `${getTypePackage(
- returnType
- )}`"
- )
- }
+ val returnTypeRank = getTypeRank(returnType)
+ if (returnTypeRank != -1 && returnTypeRank < classRank) {
+ report(
+ PACKAGE_LAYERING, cls,
+ "Method return type `${returnType.toTypeString()}` violates package layering: nothing in `$classPackage` should depend on `${getTypePackage(
+ returnType
+ )}`"
+ )
}
for (parameter in method.parameters()) {
@@ -1307,7 +1299,7 @@
}
fun isGetter(method: MethodItem): Boolean {
- val returnType = method.returnType() ?: return false
+ val returnType = method.returnType()
return method.parameters().isEmpty() && returnType.primitive && returnType.toTypeString() == "boolean"
}
@@ -1549,7 +1541,7 @@
)
}
for (method in methods) {
- if (method.returnType()?.asClass() == cls) {
+ if (method.returnType().asClass() == cls) {
report(
MANAGER_LOOKUP, method,
"Managers must always be obtained from Context (`${method.name()}`)"
@@ -1602,7 +1594,7 @@
is MethodItem -> {
// For methods requiresNullnessInfo and hasNullnessInfo considers both parameters and return,
// only warn about non-annotated returns here as parameters will get visited individually.
- if (item.isConstructor() || item.returnType()?.primitive == true) return
+ if (item.isConstructor() || item.returnType().primitive) return
if (item.modifiers.hasNullnessInfo()) return
"method `${item.name()}` return"
}
@@ -1639,9 +1631,8 @@
private fun anySuperMethodIsNonNull(method: MethodItem): Boolean {
return method.superMethods().any { superMethod ->
- superMethod.modifiers.isNonNull() &&
- // Disable check for generics
- superMethod.returnType()?.isTypeParameter() != true
+ // Disable check for generics
+ superMethod.modifiers.isNonNull() && !superMethod.returnType().isTypeParameter()
}
}
@@ -1661,9 +1652,8 @@
private fun anySuperMethodLacksNullnessInfo(method: MethodItem): Boolean {
return method.superMethods().any { superMethod ->
- !superMethod.hasNullnessInfo() &&
- // Disable check for generics
- superMethod.returnType()?.isTypeParameter() != true
+ // Disable check for generics
+ !superMethod.hasNullnessInfo() && !superMethod.returnType().isTypeParameter()
}
}
@@ -2131,7 +2121,7 @@
return
}
for (method in methods) {
- val returnType = method.returnType() ?: continue
+ val returnType = method.returnType()
if (returnType.primitive) {
return
}
@@ -2171,17 +2161,23 @@
}
private fun checkUnits(method: MethodItem) {
- val returnType = method.returnType() ?: return
+ val returnType = method.returnType()
var type = returnType.toTypeString()
val name = method.name()
if (type == "int" || type == "long" || type == "short") {
if (badUnits.any { name.endsWith(it.key) }) {
- val badUnit = badUnits.keys.find { name.endsWith(it) }
- val value = badUnits[badUnit]
- report(
- METHOD_NAME_UNITS, method,
- "Expected method name units to be `$value`, was `$badUnit` in `$name`"
- )
+ val typeIsTypeDef = method.modifiers.annotations().any { annotation ->
+ val annotationClass = annotation.resolve() ?: return@any false
+ annotationClass.modifiers.annotations().any { it.isTypeDefAnnotation() }
+ }
+ if (!typeIsTypeDef) {
+ val badUnit = badUnits.keys.find { name.endsWith(it) }
+ val value = badUnits[badUnit]
+ report(
+ METHOD_NAME_UNITS, method,
+ "Expected method name units to be `$value`, was `$badUnit` in `$name`"
+ )
+ }
}
} else if (type == "void") {
if (method.parameters().size != 1) {
@@ -2203,7 +2199,7 @@
}
private fun checkCloseable(cls: ClassItem, methods: Sequence<MethodItem>) {
- // AutoClosable has been added in API 19, so libraries with minSdkVersion <19 cannot use it. If the version
+ // AutoCloseable has been added in API 19, so libraries with minSdkVersion <19 cannot use it. If the version
// is not set, then keep the check enabled.
val minSdkVersion = codebase.getMinSdkVersion()
if (minSdkVersion is SetMinSdkVersion && minSdkVersion.value < 19) {
@@ -2220,7 +2216,7 @@
val foundMethodsDescriptions = foundMethods.joinToString { method -> "${method.name()}()" }
report(
NOT_CLOSEABLE, cls,
- "Classes that release resources ($foundMethodsDescriptions) should implement AutoClosable and CloseGuard: ${cls.describe()}"
+ "Classes that release resources ($foundMethodsDescriptions) should implement AutoCloseable and CloseGuard: ${cls.describe()}"
)
}
}
@@ -2255,7 +2251,7 @@
}
// https://kotlinlang.org/docs/reference/operator-overloading.html#increments-and-decrements
"inc", "dec" -> {
- if (method.parameters().isEmpty() && method.returnType()?.toTypeString() != "void") {
+ if (method.parameters().isEmpty() && method.returnType().toTypeString() != "void") {
flagKotlinOperator(
method, "Method can be invoked as a pre/postfix inc/decrement operator from Kotlin: `$name`"
)
@@ -2273,7 +2269,7 @@
if (methods.any {
it.name() == assignName &&
it.parameters().size == 1 &&
- it.returnType()?.toTypeString() == "void"
+ it.returnType().toTypeString() == "void"
}
) {
report(
@@ -2284,7 +2280,7 @@
}
// https://kotlinlang.org/docs/reference/operator-overloading.html#in
"contains" -> {
- if (method.parameters().size == 1 && method.returnType()?.toTypeString() == "boolean") {
+ if (method.parameters().size == 1 && method.returnType().toTypeString() == "boolean") {
flagKotlinOperator(
method, "Method can be invoked as a \"in\" operator from Kotlin: `$name`"
)
@@ -2316,7 +2312,7 @@
}
// https://kotlinlang.org/docs/reference/operator-overloading.html#assignments
"plusAssign", "minusAssign", "timesAssign", "divAssign", "remAssign", "modAssign" -> {
- if (method.parameters().size == 1 && method.returnType()?.toTypeString() == "void") {
+ if (method.parameters().size == 1 && method.returnType().toTypeString() == "void") {
flagKotlinOperator(
method, "Method can be invoked as a compound assignment operator from Kotlin: `$name`"
)
@@ -2369,8 +2365,7 @@
private fun checkUserHandle(cls: ClassItem, methods: Sequence<MethodItem>) {
val qualifiedName = cls.qualifiedName()
- if (qualifiedName == "android.app.admin.DeviceAdminReceiver" ||
- qualifiedName == "android.content.pm.LauncherApps" ||
+ if (qualifiedName == "android.content.pm.LauncherApps" ||
qualifiedName == "android.os.UserHandle" ||
qualifiedName == "android.os.UserManager"
) {
diff --git a/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt b/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt
index a127c59..89d3645 100644
--- a/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt
+++ b/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt
@@ -56,14 +56,11 @@
* Request for compatibility checks.
* [file] represents the signature file to be checked. [apiType] represents which
* part of the API should be checked, [releaseType] represents what kind of codebase
- * we are comparing it against. If [codebase] is specified, compare the signature file
- * against the codebase instead of metalava's current source tree configured via the
- * normal source path flags.
+ * we are comparing it against.
*/
data class CheckRequest(
val file: File,
- val apiType: ApiType,
- val codebase: File? = null
+ val apiType: ApiType
) {
override fun toString(): String {
return "--check-compatibility:${apiType.flagName}:released $file"
@@ -357,7 +354,7 @@
val oldReturnType = old.returnType()
val newReturnType = new.returnType()
- if (!new.isConstructor() && oldReturnType != null && newReturnType != null) {
+ if (!new.isConstructor()) {
val oldTypeParameter = oldReturnType.asTypeParameter(old)
val newTypeParameter = newReturnType.asTypeParameter(new)
var compatible = true
@@ -510,6 +507,16 @@
new,
"${describe(new, capitalize = true)} has added 'final' qualifier"
)
+ } else if (old.isEffectivelyFinal() && !new.isEffectivelyFinal()) {
+ // Disallowed removing final: If an app inherits the class and starts overriding
+ // the method it's going to crash on earlier versions where the method is final
+ // It doesn't break compatibility in the strict sense, but does make it very
+ // difficult to extend this method in practice.
+ report(
+ Issues.REMOVED_FINAL,
+ new,
+ "${describe(new, capitalize = true)} has removed 'final' qualifier"
+ )
}
}
}
@@ -920,26 +927,31 @@
companion object {
fun checkCompatibility(
- codebase: Codebase,
- previous: Codebase,
+ newCodebase: Codebase,
+ oldCodebase: Codebase,
apiType: ApiType,
- oldBase: Codebase? = null,
- newBase: Codebase? = null
+ baseApi: Codebase? = null,
) {
val filter = apiType.getReferenceFilter()
.or(apiType.getEmitFilter())
.or(ApiType.PUBLIC_API.getReferenceFilter())
.or(ApiType.PUBLIC_API.getEmitFilter())
- val checker = CompatibilityCheck(filter, previous, apiType, newBase, options.reporterCompatibilityReleased)
- // newBase is considered part of the current codebase
- val currentFullCodebase = MergedCodebase(listOf(newBase, codebase).filterNotNull())
- // oldBase is considered part of the previous codebase
- val previousFullCodebase = MergedCodebase(listOf(oldBase, previous).filterNotNull())
- CodebaseComparator().compare(checker, previousFullCodebase, currentFullCodebase, filter)
+ val checker = CompatibilityCheck(filter, oldCodebase, apiType, baseApi, options.reporterCompatibilityReleased)
+
+ val oldFullCodebase = if (options.showUnannotated && apiType == ApiType.PUBLIC_API) {
+ MergedCodebase(listOfNotNull(oldCodebase, baseApi))
+ } else {
+ // To avoid issues with partial oldCodeBase we fill gaps with newCodebase, the
+ // first parameter is master, so we don't change values of oldCodeBase
+ MergedCodebase(listOfNotNull(oldCodebase, newCodebase))
+ }
+ val newFullCodebase = MergedCodebase(listOfNotNull(newCodebase, baseApi))
+
+ CodebaseComparator().compare(checker, oldFullCodebase, newFullCodebase, filter)
val message = "Found compatibility problems checking " +
- "the ${apiType.displayName} API (${codebase.location}) against the API in ${previous.location}"
+ "the ${apiType.displayName} API (${newCodebase.location}) against the API in ${oldCodebase.location}"
if (checker.foundProblems) {
throw DriverException(exitCode = -1, stderr = message)
diff --git a/src/main/java/com/android/tools/metalava/DexApiWriter.kt b/src/main/java/com/android/tools/metalava/DexApiWriter.kt
index 1142b2e..c2e844e 100644
--- a/src/main/java/com/android/tools/metalava/DexApiWriter.kt
+++ b/src/main/java/com/android/tools/metalava/DexApiWriter.kt
@@ -59,7 +59,7 @@
writer.print("V")
} else {
val returnType = method.returnType()
- writer.print(returnType?.internalName(method) ?: "V")
+ writer.print(returnType.internalName(method))
}
writer.print("\n")
}
diff --git a/src/main/java/com/android/tools/metalava/DocAnalyzer.kt b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt
index 7e357c0..66f1d32 100644
--- a/src/main/java/com/android/tools/metalava/DocAnalyzer.kt
+++ b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt
@@ -6,6 +6,7 @@
import com.android.tools.lint.checks.ApiLookup
import com.android.tools.lint.detector.api.editDistance
import com.android.tools.lint.helpers.DefaultJavaEvaluator
+import com.android.tools.metalava.apilevels.ApiToExtensionsMap
import com.android.tools.metalava.model.AnnotationAttributeValue
import com.android.tools.metalava.model.AnnotationItem
import com.android.tools.metalava.model.ClassItem
@@ -21,9 +22,14 @@
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiField
import com.intellij.psi.PsiMethod
+import org.w3c.dom.Node
+import org.w3c.dom.NodeList
+import org.xml.sax.Attributes
+import org.xml.sax.helpers.DefaultHandler
import java.io.File
import java.nio.file.Files
import java.util.regex.Pattern
+import javax.xml.parsers.SAXParserFactory
import kotlin.math.min
/**
@@ -659,12 +665,16 @@
fun applyApiLevels(applyApiLevelsXml: File) {
val apiLookup = getApiLookup(applyApiLevelsXml)
+ val elementToSdkExtSinceMap = createSymbolToSdkExtSinceMap(applyApiLevelsXml)
val pkgApi = HashMap<PackageItem, Int?>(300)
codebase.accept(object : ApiVisitor(visitConstructorsAsMethods = true) {
override fun visitMethod(method: MethodItem) {
val psiMethod = method.psi() as? PsiMethod ?: return
addApiLevelDocumentation(apiLookup.getMethodVersion(psiMethod), method)
+ elementToSdkExtSinceMap["${psiMethod.containingClass!!.qualifiedName}#${psiMethod.name}"]?.let {
+ addApiExtensionsDocumentation(it, method)
+ }
addDeprecatedDocumentation(apiLookup.getMethodDeprecatedIn(psiMethod), method)
}
@@ -678,12 +688,18 @@
val pkg = cls.containingPackage()
pkgApi[pkg] = min(pkgApi[pkg] ?: Integer.MAX_VALUE, since)
}
+ elementToSdkExtSinceMap["${psiClass.qualifiedName}"]?.let {
+ addApiExtensionsDocumentation(it, cls)
+ }
addDeprecatedDocumentation(apiLookup.getClassDeprecatedIn(psiClass), cls)
}
override fun visitField(field: FieldItem) {
val psiField = field.psi() as PsiField
addApiLevelDocumentation(apiLookup.getFieldVersion(psiField), field)
+ elementToSdkExtSinceMap["${psiField.containingClass!!.qualifiedName}#${psiField.name}"]?.let {
+ addApiExtensionsDocumentation(it, field)
+ }
addDeprecatedDocumentation(apiLookup.getFieldDeprecatedIn(psiField), field)
}
})
@@ -703,6 +719,13 @@
// @SystemApi, @TestApi etc -- don't apply API levels here since we don't have accurate historical data
return
}
+ if (!options.isDeveloperPreviewBuild() && options.currentApiLevel != -1 && level > options.currentApiLevel) {
+ // api-versions.xml currently assigns api+1 to APIs that have not yet been finalized
+ // in a dessert (only in an extension), but for release builds, we don't want to
+ // include a "future" SDK_INT
+ return
+ }
+
val currentCodeName = options.currentCodeName
val code: String = if (currentCodeName != null && level > options.currentApiLevel) {
currentCodeName
@@ -731,6 +754,24 @@
}
}
+ private fun addApiExtensionsDocumentation(sdkExtSince: List<SdkAndVersion>, item: Item) {
+ if (item.documentation.contains("@sdkExtSince")) {
+ reporter.report(
+ Issues.FORBIDDEN_TAG, item,
+ "Documentation should not specify @sdkExtSince " +
+ "manually; it's computed and injected at build time by $PROGRAM_NAME"
+ )
+ }
+ // Don't emit an @sdkExtSince for every item in sdkExtSince; instead, limit output to the
+ // first non-Android SDK listed for the symbol in sdk-extensions-info.txt (the Android SDK
+ // is already covered by @apiSince and doesn't have to be repeated)
+ sdkExtSince.find {
+ it.sdk != ApiToExtensionsMap.ANDROID_PLATFORM_SDK_ID
+ }?.let {
+ item.appendDocumentation("${it.name} ${it.version}", "@sdkExtSince")
+ }
+ }
+
private fun addDeprecatedDocumentation(level: Int, item: Item) {
if (level > 0) {
if (item.originallyHidden) {
@@ -868,3 +909,101 @@
}
}
}
+
+/**
+ * Generate a map of symbol -> (list of SDKs and corresponding versions the symbol first appeared)
+ * in by parsing an api-versions.xml file. This will be used when injecting @sdkExtSince annotations,
+ * which convey the same information, in a format documentation tools can consume.
+ *
+ * A symbol is either of a class, method or field.
+ *
+ * The symbols are Strings on the format "com.pkg.Foo#MethodOrField", with no method signature.
+ */
+private fun createSymbolToSdkExtSinceMap(xmlFile: File): Map<String, List<SdkAndVersion>> {
+ data class OuterClass(val name: String, val idAndVersionList: List<IdAndVersion>?)
+
+ val sdkIdentifiers = mutableMapOf<Int, SdkIdentifier>(
+ ApiToExtensionsMap.ANDROID_PLATFORM_SDK_ID to SdkIdentifier(ApiToExtensionsMap.ANDROID_PLATFORM_SDK_ID, "Android", "Android", "null")
+ )
+ var lastSeenClass: OuterClass? = null
+ val elementToIdAndVersionMap = mutableMapOf<String, List<IdAndVersion>>()
+ val memberTags = listOf("class", "method", "field")
+ val parser = SAXParserFactory.newDefaultInstance().newSAXParser()
+ parser.parse(
+ xmlFile,
+ object : DefaultHandler() {
+ override fun startElement(uri: String, localName: String, qualifiedName: String, attributes: Attributes) {
+ if (qualifiedName == "sdk") {
+ val id: Int = attributes.getValue("id")?.toIntOrNull() ?: throw IllegalArgumentException("<sdk>: missing or non-integer id attribute")
+ val shortname: String = attributes.getValue("shortname") ?: throw IllegalArgumentException("<sdk>: missing shortname attribute")
+ val name: String = attributes.getValue("name") ?: throw IllegalArgumentException("<sdk>: missing name attribute")
+ val reference: String = attributes.getValue("reference") ?: throw IllegalArgumentException("<sdk>: missing reference attribute")
+ sdkIdentifiers.put(id, SdkIdentifier(id, shortname, name, reference))
+ } else if (memberTags.contains(qualifiedName)) {
+ val name: String = attributes.getValue("name") ?: throw IllegalArgumentException("<$qualifiedName>: missing name attribute")
+ val idAndVersionList: List<IdAndVersion>? = attributes.getValue("sdks")?.split(",")?.map {
+ val (sdk, version) = it.split(":")
+ IdAndVersion(sdk.toInt(), version.toInt())
+ }?.toList()
+
+ // Populate elementToIdAndVersionMap. The keys constructed here are derived from
+ // api-versions.xml; when used elsewhere in DocAnalyzer, the keys will be
+ // derived from PsiItems. The two sources use slightly different nomenclature,
+ // so change "api-versions.xml nomenclature" to "PsiItems nomenclature" before
+ // inserting items in the map.
+ //
+ // Nomenclature differences:
+ // - constructors are named "<init>()V" in api-versions.xml, but
+ // "ClassName()V" in PsiItems
+ // - inner classes are named "Outer#Inner" in api-versions.xml, but
+ // "Outer.Inner" in PsiItems
+ when (qualifiedName) {
+ "class" -> {
+ lastSeenClass = OuterClass(name.replace('/', '.').replace('$', '.'), idAndVersionList)
+ if (idAndVersionList != null) {
+ elementToIdAndVersionMap["${lastSeenClass!!.name}"] = idAndVersionList
+ }
+ }
+ "method", "field" -> {
+ val shortName = if (name.startsWith("<init>")) {
+ // constructors in api-versions.xml are named '<init>': rename to
+ // name of class instead, and strip signature: '<init>()V' -> 'Foo'
+ lastSeenClass!!.name.substringAfterLast('.')
+ } else {
+ // strip signature: 'foo()V' -> 'foo'
+ name.substringBefore('(')
+ }
+ val element = "${lastSeenClass!!.name}#$shortName"
+ if (idAndVersionList != null) {
+ elementToIdAndVersionMap[element] = idAndVersionList
+ } else if (lastSeenClass!!.idAndVersionList != null) {
+ elementToIdAndVersionMap[element] = lastSeenClass!!.idAndVersionList!!
+ }
+ }
+ }
+ }
+ }
+
+ override fun endElement(uri: String, localName: String, qualifiedName: String) {
+ if (qualifiedName == "class") {
+ lastSeenClass = null
+ }
+ }
+ }
+ )
+
+ val elementToSdkExtSinceMap = mutableMapOf<String, List<SdkAndVersion>>()
+ for (entry in elementToIdAndVersionMap.entries) {
+ elementToSdkExtSinceMap[entry.key] = entry.value.map {
+ val name = sdkIdentifiers.get(it.first)?.name ?: throw IllegalArgumentException("SDK reference to unknown <sdk> with id ${it.first}")
+ SdkAndVersion(it.first, name, it.second)
+ }
+ }
+ return elementToSdkExtSinceMap
+}
+
+private fun NodeList.firstOrNull(): Node? = if (length > 0) { item(0) } else { null }
+
+private typealias IdAndVersion = Pair<Int, Int>
+
+private data class SdkAndVersion(val sdk: Int, val name: String, val version: Int)
diff --git a/src/main/java/com/android/tools/metalava/Driver.kt b/src/main/java/com/android/tools/metalava/Driver.kt
index 57942ec..c4a23a1 100644
--- a/src/main/java/com/android/tools/metalava/Driver.kt
+++ b/src/main/java/com/android/tools/metalava/Driver.kt
@@ -250,8 +250,6 @@
} else {
return
}
- codebase.apiLevel = options.currentApiLevel +
- if (options.currentCodeName != null && "REL" != options.currentCodeName) 1 else 0
options.manifest?.let { codebase.manifest = it }
if (options.verbose) {
@@ -266,8 +264,13 @@
val androidApiLevelXml = options.generateApiLevelXml
val apiLevelJars = options.apiLevelJars
if (androidApiLevelXml != null && apiLevelJars != null) {
+ assert(options.currentApiLevel != -1)
+
progress("Generating API levels XML descriptor file, ${androidApiLevelXml.name}: ")
- ApiGenerator.generate(apiLevelJars, options.firstApiLevel, androidApiLevelXml, codebase)
+ ApiGenerator.generate(
+ apiLevelJars, options.firstApiLevel, options.currentApiLevel, options.isDeveloperPreviewBuild(),
+ androidApiLevelXml, codebase, options.sdkJarRoot, options.sdkInfoFile, options.removeMissingClassesInApiLevels
+ )
}
if (options.docStubsDir != null || options.enhanceDocumentation) {
@@ -385,16 +388,6 @@
it, codebase, docStubs = false,
writeStubList = options.stubsSourceList != null
)
-
- val stubAnnotations = options.copyStubAnnotationsFrom
- if (stubAnnotations != null) {
- // Support pointing to both stub-annotations and stub-annotations/src/main/java
- val src = File(stubAnnotations, "src${File.separator}main${File.separator}java")
- val source = if (src.isDirectory) src else stubAnnotations
- source.listFiles()?.forEach { file ->
- RewriteAnnotations().copyAnnotations(codebase, file, File(it, file.name))
- }
- }
}
if (options.docStubsDir == null && options.stubsDir == null) {
@@ -455,9 +448,6 @@
}
}
- // --rewrite-annotations?
- options.rewriteAnnotations?.let { RewriteAnnotations().rewriteAnnotations(it) }
-
// Convert android.jar files?
options.androidJarSignatureFiles?.let { root ->
// Generate API signature files for all the historical JAR files
@@ -501,13 +491,13 @@
* signature file.
*/
fun checkCompatibility(
- codebase: Codebase,
+ newCodebase: Codebase,
check: CheckRequest
) {
progress("Checking API compatibility ($check): ")
val signatureFile = check.file
- val current =
+ val oldCodebase =
if (signatureFile.path.endsWith(DOT_JAR)) {
loadFromJarFile(signatureFile)
} else {
@@ -517,64 +507,39 @@
)
}
- if (current is TextCodebase && current.format > FileFormat.V1 && options.outputFormat == FileFormat.V1) {
- throw DriverException("Cannot perform compatibility check of signature file $signatureFile in format ${current.format} without analyzing current codebase with $ARG_FORMAT=${current.format}")
+ if (oldCodebase is TextCodebase && oldCodebase.format > FileFormat.V1 && options.outputFormat == FileFormat.V1) {
+ throw DriverException("Cannot perform compatibility check of signature file $signatureFile in format ${oldCodebase.format} without analyzing current codebase with $ARG_FORMAT=${oldCodebase.format}")
}
- var newBase: Codebase? = null
- var oldBase: Codebase? = null
+ var baseApi: Codebase? = null
+
val apiType = check.apiType
- // If diffing with a system-api or test-api (or other signature-based codebase
- // generated from --show-annotations), the API is partial: it's only listing
- // the API that is *different* from the base API. This really confuses the
- // codebase comparison when diffing with a complete codebase, since it looks like
- // many classes and members have been added and removed. Therefore, the comparison
- // is simpler if we just make the comparison with the same generated signature
- // file. If we've only emitted one for the new API, use it directly, if not, generate
- // it first
- val new =
- if (check.codebase != null) {
- SignatureFileLoader.load(
- file = check.codebase,
+ if (options.showUnannotated && apiType == ApiType.PUBLIC_API) {
+ // Fast path: if we've already generated a signature file, and it's identical, we're good!
+ val apiFile = options.apiFile
+ if (apiFile != null && apiFile.readText(UTF_8) == signatureFile.readText(UTF_8)) {
+ return
+ }
+ val baseApiFile = options.baseApiForCompatCheck
+ if (baseApiFile != null) {
+ baseApi = SignatureFileLoader.load(
+ file = baseApiFile,
kotlinStyleNulls = options.inputKotlinStyleNulls
)
- } else if (!options.showUnannotated || apiType != ApiType.PUBLIC_API) {
- if (options.baseApiForCompatCheck != null) {
- // This option does not make sense with showAnnotation, as the "base" in that case
- // is the non-annotated APIs.
- throw DriverException(
- ARG_CHECK_COMPATIBILITY_BASE_API +
- " is not compatible with --showAnnotation."
- )
- }
-
- newBase = codebase
- oldBase = newBase
-
- codebase
- } else {
- // Fast path: if we've already generated a signature file and it's identical, we're good!
- val apiFile = options.apiFile
- if (apiFile != null && apiFile.readText(UTF_8) == signatureFile.readText(UTF_8)) {
- return
- }
-
- val baseApiFile = options.baseApiForCompatCheck
- if (baseApiFile != null) {
- oldBase = SignatureFileLoader.load(
- file = baseApiFile,
- kotlinStyleNulls = options.inputKotlinStyleNulls
- )
- newBase = oldBase
- }
-
- codebase
}
+ } else if (options.baseApiForCompatCheck != null) {
+ // This option does not make sense with showAnnotation, as the "base" in that case
+ // is the non-annotated APIs.
+ throw DriverException(
+ ARG_CHECK_COMPATIBILITY_BASE_API +
+ " is not compatible with --showAnnotation."
+ )
+ }
// If configured, compares the new API with the previous API and reports
// any incompatibilities.
- CompatibilityCheck.checkCompatibility(new, current, apiType, oldBase, newBase)
+ CompatibilityCheck.checkCompatibility(newCodebase, oldCodebase, apiType, baseApi)
}
fun createTempFile(namePrefix: String, nameSuffix: String): File {
@@ -819,8 +784,15 @@
}
private fun createStubFiles(stubDir: File, codebase: Codebase, docStubs: Boolean, writeStubList: Boolean) {
- // Generating stubs from a sig-file-based codebase is problematic
- assert(codebase.supportsDocumentation())
+ if (codebase is TextCodebase) {
+ if (options.verbose) {
+ options.stdout.println(
+ "Generating stubs from text based codebase is an experimental feature. " +
+ "It is not guaranteed that stubs generated from text based codebase are " +
+ "class level equivalent to the stubs generated from source files. "
+ )
+ }
+ }
// Temporary bug workaround for org.chromium.arc
if (options.sourcePath.firstOrNull()?.path?.endsWith("org.chromium.arc") == true) {
diff --git a/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt b/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt
index c5c7408..4dd3b36 100644
--- a/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt
+++ b/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt
@@ -193,9 +193,7 @@
if (annotation.isTypeDefAnnotation()) {
// Imported typedef
addItem(item, AnnotationHolder(null, annotation, null))
- } else if (annotation.targets.contains(AnnotationTarget.EXTERNAL_ANNOTATIONS_FILE) &&
- !options.includeSourceRetentionAnnotations
- ) {
+ } else if (annotation.targets.contains(AnnotationTarget.EXTERNAL_ANNOTATIONS_FILE)) {
addItem(item, AnnotationHolder(null, annotation, null))
}
@@ -457,8 +455,8 @@
if (isConstructor()) {
sb.append(escapeXml(containingClass().simpleName()))
- } else if (returnType() != null) {
- sb.append(escapeXml(returnType()!!.toTypeString()))
+ } else {
+ sb.append(escapeXml(returnType().toTypeString()))
sb.append(' ')
sb.append(escapeXml(name()))
}
diff --git a/src/main/java/com/android/tools/metalava/FileReadSandbox.kt b/src/main/java/com/android/tools/metalava/FileReadSandbox.kt
index 7c5542b..3f88d22 100644
--- a/src/main/java/com/android/tools/metalava/FileReadSandbox.kt
+++ b/src/main/java/com/android/tools/metalava/FileReadSandbox.kt
@@ -13,6 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+// Suppress "SecurityManager is deprecated" warnings: FileReadSandboxTest verifies that the class
+// still works as expected
+@file:Suppress("DEPRECATION")
+
package com.android.tools.metalava
import java.io.File
diff --git a/src/main/java/com/android/tools/metalava/Issues.kt b/src/main/java/com/android/tools/metalava/Issues.kt
index 1c356fa..903bd62 100644
--- a/src/main/java/com/android/tools/metalava/Issues.kt
+++ b/src/main/java/com/android/tools/metalava/Issues.kt
@@ -127,8 +127,9 @@
val COMPILE_TIME_CONSTANT = Issue(Severity.ERROR, Category.API_LINT)
val SINGULAR_CALLBACK = Issue(Severity.ERROR, Category.API_LINT, "callback-class-singular")
val CALLBACK_NAME = Issue(Severity.WARNING, Category.API_LINT, "observer-should-be-callback")
+ // Obsolete per https://s.android.com/api-guidelines.
val CALLBACK_INTERFACE =
- Issue(Severity.ERROR, Category.API_LINT, "callback-abstract-instead-of-interface")
+ Issue(Severity.HIDDEN, Category.API_LINT, "callback-abstract-instead-of-interface")
val CALLBACK_METHOD_NAME = Issue(Severity.ERROR, Category.API_LINT, "callback-method-naming")
val LISTENER_INTERFACE = Issue(Severity.ERROR, Category.API_LINT, "callbacks-listener")
val SINGLE_METHOD_INTERFACE = Issue(Severity.ERROR, Category.API_LINT, "callbacks-listener")
diff --git a/src/main/java/com/android/tools/metalava/JDiffXmlWriter.kt b/src/main/java/com/android/tools/metalava/JDiffXmlWriter.kt
index cb6a114..607b21c 100644
--- a/src/main/java/com/android/tools/metalava/JDiffXmlWriter.kt
+++ b/src/main/java/com/android/tools/metalava/JDiffXmlWriter.kt
@@ -207,7 +207,7 @@
writer.print("<method name=\"")
writer.print(method.name())
- method.returnType()?.let {
+ method.returnType().let {
writer.print("\"\n return=\"")
writer.print(XmlUtils.toXmlAttributeValue(formatType(it)))
}
diff --git a/src/main/java/com/android/tools/metalava/NullnessMigration.kt b/src/main/java/com/android/tools/metalava/NullnessMigration.kt
index 77c9a83..3cccb11 100644
--- a/src/main/java/com/android/tools/metalava/NullnessMigration.kt
+++ b/src/main/java/com/android/tools/metalava/NullnessMigration.kt
@@ -50,8 +50,8 @@
override fun compare(old: MethodItem, new: MethodItem) {
@Suppress("ConstantConditionIf")
if (SUPPORT_TYPE_USE_ANNOTATIONS) {
- val newType = new.returnType() ?: return
- val oldType = old.returnType() ?: return
+ val newType = new.returnType()
+ val oldType = old.returnType()
checkType(oldType, newType)
}
}
diff --git a/src/main/java/com/android/tools/metalava/Options.kt b/src/main/java/com/android/tools/metalava/Options.kt
index a65542e..4e16fed 100644
--- a/src/main/java/com/android/tools/metalava/Options.kt
+++ b/src/main/java/com/android/tools/metalava/Options.kt
@@ -83,7 +83,6 @@
const val ARG_HIDE_PACKAGE = "--hide-package"
const val ARG_MANIFEST = "--manifest"
const val ARG_MIGRATE_NULLNESS = "--migrate-nullness"
-const val ARG_CHECK_COMPATIBILITY = "--check-compatibility"
const val ARG_CHECK_COMPATIBILITY_API_RELEASED = "--check-compatibility:api:released"
const val ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED = "--check-compatibility:removed:released"
const val ARG_CHECK_COMPATIBILITY_BASE_API = "--check-compatibility:base"
@@ -108,6 +107,7 @@
const val ARG_HIDE = "--hide"
const val ARG_APPLY_API_LEVELS = "--apply-api-levels"
const val ARG_GENERATE_API_LEVELS = "--generate-api-levels"
+const val ARG_REMOVE_MISSING_CLASS_REFERENCES_IN_API_LEVELS = "--remove-missing-class-references-in-api-levels"
const val ARG_ANDROID_JAR_PATTERN = "--android-jar-pattern"
const val ARG_CURRENT_VERSION = "--current-version"
const val ARG_FIRST_VERSION = "--first-version"
@@ -127,8 +127,6 @@
const val ARG_COMPILE_SDK_VERSION = "--compile-sdk-version"
const val ARG_INCLUDE_ANNOTATIONS = "--include-annotations"
const val ARG_COPY_ANNOTATIONS = "--copy-annotations"
-const val ARG_INCLUDE_ANNOTATION_CLASSES = "--include-annotation-classes"
-const val ARG_REWRITE_ANNOTATIONS = "--rewrite-annotations"
const val ARG_INCLUDE_SOURCE_RETENTION = "--include-source-retention"
const val ARG_PASS_THROUGH_ANNOTATION = "--pass-through-annotation"
const val ARG_EXCLUDE_ANNOTATION = "--exclude-annotation"
@@ -160,6 +158,8 @@
const val ARG_STRICT_INPUT_FILES_WARN = "--strict-input-files:warn"
const val ARG_STRICT_INPUT_FILES_EXEMPT = "--strict-input-files-exempt"
const val ARG_REPEAT_ERRORS_MAX = "--repeat-errors-max"
+const val ARG_SDK_JAR_ROOT = "--sdk-extensions-root"
+const val ARG_SDK_INFO_FILE = "--sdk-extensions-info"
class Options(
private val args: Array<String>,
@@ -252,9 +252,9 @@
/**
* Whether metalava is invoked as part of updating the API files. When this is true, metalava
- * should *cancel* various other flags that are also being passed in, such as --check-compatibility.
+ * should *cancel* various other flags that are also being passed in, such as --check-compatibility:*.
* This is there to ease integration in the build system: for a given target, the build system will
- * pass all the applicable flags (--stubs, --api, --check-compatibility, --generate-documentation, etc),
+ * pass all the applicable flags (--stubs, --api, --check-compatibility:*, --generate-documentation, etc),
* and this integration is re-used for the update-api facility where we *only* want to generate the
* signature files. This avoids having duplicate metalava invocation logic where potentially newly
* added flags are missing in one of the invocations etc.
@@ -267,7 +267,7 @@
* files.
*
* This is there to ease integration in the build system: for a given target, the build system will
- * pass all the applicable flags (--stubs, --api, --check-compatibility, --generate-documentation, etc),
+ * pass all the applicable flags (--stubs, --api, --check-compatibility:*, --generate-documentation, etc),
* and this integration is re-used for the checkapi facility where we *only* want to run compatibility
* checks. This avoids having duplicate metalava invocation logic where potentially newly
* added flags are missing in one of the invocations etc.
@@ -356,8 +356,8 @@
/**
* Annotations that defines APIs that are implicitly included in the API surface. These APIs
- * will be included in included in certain kinds of output such as stubs, but others (e.g.
- * API lint and the API signature file) ignore them.
+ * will be included in certain kinds of output such as stubs, but others (e.g. API lint and the
+ * API signature file) ignore them.
*/
var showForStubPurposesAnnotations: AnnotationFilter = mutableShowForStubPurposesAnnotation
@@ -416,22 +416,6 @@
/** For [ARG_COPY_ANNOTATIONS], the target directory to write converted stub annotations from */
var privateAnnotationsTarget: File? = null
- /**
- * For [ARG_INCLUDE_ANNOTATION_CLASSES], the directory to copy stub annotation source files into the
- * stubs folder from
- */
- var copyStubAnnotationsFrom: File? = null
-
- /**
- * For [ARG_INCLUDE_SOURCE_RETENTION], true if we want to include source-retention annotations
- * both in the set of files emitted by [ARG_INCLUDE_ANNOTATION_CLASSES] and into the stubs
- * themselves
- */
- var includeSourceRetentionAnnotations = false
-
- /** For [ARG_REWRITE_ANNOTATIONS], the jar or bytecode folder to rewrite annotations in */
- var rewriteAnnotations: List<File>? = null
-
/** A manifest file to read to for example look up available permissions */
var manifest: File? = null
@@ -498,15 +482,30 @@
*/
var firstApiLevel = 1
- /** The codename of the codebase, if it's a preview, or null if not specified */
+ /**
+ * The codename of the codebase: non-null string if this is a developer preview build, null if
+ * this is a release build.
+ */
var currentCodeName: String? = null
/** API level XML file to generate */
var generateApiLevelXml: File? = null
+ /** Whether references to missing classes should be removed from the api levels file. */
+ var removeMissingClassesInApiLevels: Boolean = false
+
/** Reads API XML file to apply into documentation */
var applyApiLevelsXml: File? = null
+ /** Directory of prebuilt extension SDK jars that contribute to the API */
+ var sdkJarRoot: File? = null
+
+ /**
+ * Rules to filter out some of the extension SDK APIs from the API, and assign extensions to
+ * the APIs that are kept
+ */
+ var sdkInfoFile: File? = null
+
/** Level to include for javadoc */
var docLevel = DocLevel.PROTECTED
@@ -711,7 +710,6 @@
var androidJarPatterns: MutableList<String>? = null
var currentJar: File? = null
- var delayedCheckApiFiles = false
var skipGenerateAnnotations = false
reporter = Reporter(null, null)
@@ -977,9 +975,6 @@
privateAnnotationsSource = stringToExistingDir(getValue(args, ++index))
privateAnnotationsTarget = stringToNewDir(getValue(args, ++index))
}
- ARG_REWRITE_ANNOTATIONS -> rewriteAnnotations = stringToExistingDirsOrJars(getValue(args, ++index))
- ARG_INCLUDE_ANNOTATION_CLASSES -> copyStubAnnotationsFrom = stringToExistingDir(getValue(args, ++index))
- ARG_INCLUDE_SOURCE_RETENTION -> includeSourceRetentionAnnotations = true
"--previous-api" -> {
migrateNullsFrom = stringToExistingFile(getValue(args, ++index))
@@ -1004,7 +999,7 @@
}
}
- ARG_CHECK_COMPATIBILITY, ARG_CHECK_COMPATIBILITY_API_RELEASED -> {
+ ARG_CHECK_COMPATIBILITY_API_RELEASED -> {
val file = stringToExistingFile(getValue(args, ++index))
mutableCompatibilityChecks.add(CheckRequest(file, ApiType.PUBLIC_API))
}
@@ -1021,35 +1016,6 @@
ARG_NO_NATIVE_DIFF -> noNativeDiff = true
- // Compat flag for the old API check command, invoked from build/make/core/definitions.mk:
- "--check-api-files" -> {
- if (index < args.size - 1 && args[index + 1].startsWith("-")) {
- // Work around bug where --check-api-files is invoked with all
- // the other metalava args before the 4 files; this will be
- // fixed by https://android-review.googlesource.com/c/platform/build/+/874473
- delayedCheckApiFiles = true
- } else {
- val stableApiFile = stringToExistingFile(getValue(args, ++index))
- val apiFileToBeTested = stringToExistingFile(getValue(args, ++index))
- val stableRemovedApiFile = stringToExistingFile(getValue(args, ++index))
- val removedApiFileToBeTested = stringToExistingFile(getValue(args, ++index))
- mutableCompatibilityChecks.add(
- CheckRequest(
- stableApiFile,
- ApiType.PUBLIC_API,
- apiFileToBeTested
- )
- )
- mutableCompatibilityChecks.add(
- CheckRequest(
- stableRemovedApiFile,
- ApiType.REMOVED,
- removedApiFileToBeTested
- )
- )
- }
- }
-
ARG_ERROR, "-error" -> setIssueSeverity(
getValue(args, ++index),
Severity.ERROR,
@@ -1118,7 +1084,10 @@
firstApiLevel = Integer.parseInt(getValue(args, ++index))
}
ARG_CURRENT_CODENAME -> {
- currentCodeName = getValue(args, ++index)
+ val codeName = getValue(args, ++index)
+ if (codeName != "REL") {
+ currentCodeName = codeName
+ }
}
ARG_CURRENT_JAR -> {
currentJar = stringToExistingFile(getValue(args, ++index))
@@ -1135,6 +1104,7 @@
stringToExistingFile(getValue(args, ++index))
}
}
+ ARG_REMOVE_MISSING_CLASS_REFERENCES_IN_API_LEVELS -> removeMissingClassesInApiLevels = true
ARG_UPDATE_API, "--update-api" -> onlyUpdateApi = true
ARG_CHECK_API -> onlyCheckApi = true
@@ -1234,6 +1204,14 @@
repeatErrorsMax = Integer.parseInt(getValue(args, ++index))
}
+ ARG_SDK_JAR_ROOT -> {
+ sdkJarRoot = stringToExistingDir(getValue(args, ++index))
+ }
+
+ ARG_SDK_INFO_FILE -> {
+ sdkInfoFile = stringToExistingFile(getValue(args, ++index))
+ }
+
"--temp-folder" -> {
tempFolder = stringToNewOrExistingDir(getValue(args, ++index))
}
@@ -1391,35 +1369,13 @@
val usage = getUsage(includeHeader = false, colorize = color)
throw DriverException(stderr = "Invalid argument $arg\n\n$usage")
} else {
- if (delayedCheckApiFiles) {
- delayedCheckApiFiles = false
- val stableApiFile = stringToExistingFile(arg)
- val apiFileToBeTested = stringToExistingFile(getValue(args, ++index))
- val stableRemovedApiFile = stringToExistingFile(getValue(args, ++index))
- val removedApiFileToBeTested = stringToExistingFile(getValue(args, ++index))
- mutableCompatibilityChecks.add(
- CheckRequest(
- stableApiFile,
- ApiType.PUBLIC_API,
- apiFileToBeTested
- )
- )
- mutableCompatibilityChecks.add(
- CheckRequest(
- stableRemovedApiFile,
- ApiType.REMOVED,
- removedApiFileToBeTested
- )
- )
- } else {
- // All args that don't start with "-" are taken to be filenames
- mutableSources.addAll(stringToExistingFiles(arg))
+ // All args that don't start with "-" are taken to be filenames
+ mutableSources.addAll(stringToExistingFiles(arg))
- // Temporary workaround for
- // aosp/I73ff403bfc3d9dfec71789a3e90f9f4ea95eabe3
- if (arg.endsWith("hwbinder-stubs-docs-stubs.srcjar.rsp")) {
- skipGenerateAnnotations = true
- }
+ // Temporary workaround for
+ // aosp/I73ff403bfc3d9dfec71789a3e90f9f4ea95eabe3
+ if (arg.endsWith("hwbinder-stubs-docs-stubs.srcjar.rsp")) {
+ skipGenerateAnnotations = true
}
}
}
@@ -1429,6 +1385,10 @@
}
if (generateApiLevelXml != null) {
+ if (currentApiLevel == -1) {
+ throw DriverException(stderr = "$ARG_GENERATE_API_LEVELS requires $ARG_CURRENT_VERSION")
+ }
+
// <String> is redundant here but while IDE (with newer type inference engine
// understands that) the current 1.3.x compiler does not
@Suppress("RemoveExplicitTypeArguments")
@@ -1441,12 +1401,15 @@
apiLevelJars = findAndroidJars(
patterns,
firstApiLevel,
- currentApiLevel,
- currentCodeName,
+ currentApiLevel + if (isDeveloperPreviewBuild()) 1 else 0,
currentJar
)
}
+ if ((sdkJarRoot == null) != (sdkInfoFile == null)) {
+ throw DriverException(stderr = "$ARG_SDK_JAR_ROOT and $ARG_SDK_INFO_FILE must both be supplied")
+ }
+
// outputKotlinStyleNulls implies at least format=v3
if (outputKotlinStyleNulls) {
if (outputFormat < FileFormat.V3) {
@@ -1473,6 +1436,8 @@
// flags count
apiLevelJars = null
generateApiLevelXml = null
+ sdkJarRoot = null
+ sdkInfoFile = null
applyApiLevelsXml = null
androidJarSignatureFiles = null
stubsDir = null
@@ -1493,6 +1458,8 @@
} else if (onlyCheckApi) {
apiLevelJars = null
generateApiLevelXml = null
+ sdkJarRoot = null
+ sdkInfoFile = null
applyApiLevelsXml = null
androidJarSignatureFiles = null
stubsDir = null
@@ -1562,6 +1529,8 @@
checkFlagConsistency()
}
+ public fun isDeveloperPreviewBuild(): Boolean = currentCodeName != null
+
/** Update the classpath to insert android.jar or JDK classpath elements if necessary */
private fun updateClassPath() {
val sdkHome = sdkHome
@@ -1637,17 +1606,8 @@
androidJarPatterns: List<String>,
minApi: Int,
currentApiLevel: Int,
- currentCodeName: String?,
currentJar: File?
): Array<File> {
-
- @Suppress("NAME_SHADOWING")
- val currentApiLevel = if (currentCodeName != null && "REL" != currentCodeName) {
- currentApiLevel + 1
- } else {
- currentApiLevel
- }
-
val apiLevelFiles = mutableListOf<File>()
// api level 0: placeholder, should not be processed.
// (This is here because we want the array index to match
@@ -2160,13 +2120,9 @@
"Whether the signature file being read should be " +
"interpreted as having encoded its types using Kotlin style types: a suffix of \"?\" for nullable " +
"types, no suffix for non nullable types, and \"!\" for unknown. The default is no.",
- "$ARG_CHECK_COMPATIBILITY:type:state <file>",
+ "--check-compatibility:type:released <file>",
"Check compatibility. Type is one of 'api' " +
- "and 'removed', which checks either the public api or the removed api. State is one of " +
- "'current' and 'released', to check either the currently in development API or the last publicly " +
- "released API, respectively. Different compatibility checks apply in the two scenarios. " +
- "For example, to check the code base against the current public API, use " +
- "$ARG_CHECK_COMPATIBILITY:api:current.",
+ "and 'removed', which checks either the public api or the removed api.",
"$ARG_CHECK_COMPATIBILITY_BASE_API <file>",
"When performing a compat check, use the provided signature " +
"file as a base api, which is treated as part of the API being checked. This allows us to compute the " +
@@ -2235,12 +2191,6 @@
"$ARG_EXTRACT_ANNOTATIONS <zipfile>",
"Extracts source annotations from the source files and writes " +
"them into the given zip file",
- "$ARG_INCLUDE_ANNOTATION_CLASSES <dir>",
- "Copies the given stub annotation source files into the " +
- "generated stub sources; <dir> is typically $PROGRAM_NAME/stub-annotations/src/main/java/.",
- "$ARG_REWRITE_ANNOTATIONS <dir/jar>",
- "For a bytecode folder or output jar, rewrites the " +
- "androidx annotations to be package private",
"$ARG_FORCE_CONVERT_TO_WARNING_NULLABILITY_ANNOTATIONS <package1:-package2:...>",
"On every API declared " +
"in a class referenced by the given filter, makes nullability issues appear to callers as warnings " +
@@ -2262,6 +2212,11 @@
"$ARG_GENERATE_API_LEVELS <xmlfile>",
"Reads android.jar SDK files and generates an XML file recording " +
"the API level for each class, method and field",
+ "$ARG_REMOVE_MISSING_CLASS_REFERENCES_IN_API_LEVELS",
+ "Removes references to missing classes when generating the API levels XML file. " +
+ "This can happen when generating the XML file for the non-updatable portions of " +
+ "the module-lib sdk, as those non-updatable portions can reference classes that are " +
+ "part of an updatable apex.",
"$ARG_ANDROID_JAR_PATTERN <pattern>",
"Patterns to use to locate Android JAR files. The default " +
"is \$ANDROID_HOME/platforms/android-%/android.jar.",
@@ -2269,6 +2224,28 @@
ARG_CURRENT_VERSION, "Sets the current API level of the current source code",
ARG_CURRENT_CODENAME, "Sets the code name for the current source code",
ARG_CURRENT_JAR, "Points to the current API jar, if any",
+ ARG_SDK_JAR_ROOT,
+ "Points to root of prebuilt extension SDK jars, if any. This directory is expected to " +
+ "contain snapshots of historical extension SDK versions in the form of stub jars. " +
+ "The paths should be on the format \"<int>/public/<module-name>.jar\", where <int> " +
+ "corresponds to the extension SDK version, and <module-name> to the name of the mainline module.",
+ ARG_SDK_INFO_FILE,
+ "Points to map of extension SDK APIs to include, if any. The file is a plain text file " +
+ "and describes, per extension SDK, what APIs from that extension to include in the " +
+ "file created via $ARG_GENERATE_API_LEVELS. The format of each line is one of the following: " +
+ "\"<module-name> <pattern> <ext-name> [<ext-name> [...]]\", where <module-name> is the " +
+ "name of the mainline module this line refers to, <pattern> is a common Java name prefix " +
+ "of the APIs this line refers to, and <ext-name> is a list of extension SDK names " +
+ "in which these SDKs first appeared, or \"<ext-name> <ext-id> <type>\", where " +
+ "<ext-name> is the name of an SDK, " +
+ "<ext-id> its numerical ID and <type> is one of " +
+ "\"platform\" (the Android platform SDK), " +
+ "\"platform-ext\" (an extension to the Android platform SDK), " +
+ "\"standalone\" (a separate SDK). " +
+ "Fields are separated by whitespace. " +
+ "A mainline module may be listed multiple times. " +
+ "The special pattern \"*\" refers to all APIs in the given mainline module. " +
+ "Lines beginning with # are comments.",
"", "\nSandboxing:",
ARG_NO_IMPLICIT_ROOT,
diff --git a/src/main/java/com/android/tools/metalava/RewriteAnnotations.kt b/src/main/java/com/android/tools/metalava/RewriteAnnotations.kt
index a34c162..a251b8e 100644
--- a/src/main/java/com/android/tools/metalava/RewriteAnnotations.kt
+++ b/src/main/java/com/android/tools/metalava/RewriteAnnotations.kt
@@ -17,26 +17,9 @@
package com.android.tools.metalava
import com.android.SdkConstants
-import com.android.SdkConstants.DOT_CLASS
-import com.android.SdkConstants.DOT_JAR
import com.android.tools.metalava.model.AnnotationRetention
import com.android.tools.metalava.model.Codebase
-import com.google.common.io.Closer
-import org.objectweb.asm.ClassReader
-import org.objectweb.asm.ClassVisitor
-import org.objectweb.asm.ClassWriter
-import org.objectweb.asm.Opcodes
-import org.objectweb.asm.Opcodes.ASM6
-import java.io.BufferedInputStream
-import java.io.BufferedOutputStream
import java.io.File
-import java.io.FileInputStream
-import java.io.FileOutputStream
-import java.io.IOException
-import java.nio.file.attribute.FileTime
-import java.util.jar.JarEntry
-import java.util.zip.ZipInputStream
-import java.util.zip.ZipOutputStream
import kotlin.text.Charsets.UTF_8
/**
@@ -51,12 +34,10 @@
fun modifyAnnotationSources(codebase: Codebase?, source: File, target: File, pkg: String = "") {
val fileName = source.name
if (fileName.endsWith(SdkConstants.DOT_JAVA)) {
- if (!options.includeSourceRetentionAnnotations) {
- // Only copy non-source retention annotation classes
- val qualifiedName = pkg + "." + fileName.substring(0, fileName.indexOf('.'))
- if (hasSourceRetention(codebase, qualifiedName)) {
- return
- }
+ // Only copy non-source retention annotation classes
+ val qualifiedName = pkg + "." + fileName.substring(0, fileName.indexOf('.'))
+ if (hasSourceRetention(codebase, qualifiedName)) {
+ return
}
// Copy and convert
@@ -75,49 +56,6 @@
}
}
- /** Copies annotation source files from [source] to [target] */
- fun copyAnnotations(codebase: Codebase, source: File, target: File, pkg: String = "") {
- val fileName = source.name
- if (fileName.endsWith(SdkConstants.DOT_JAVA)) {
- if (!options.includeSourceRetentionAnnotations) {
- // Only copy non-source retention annotation classes
- val qualifiedName = pkg + "." + fileName.substring(0, fileName.indexOf('.'))
- if (hasSourceRetention(codebase, qualifiedName)) {
- return
- }
- }
-
- // Copy and convert
- target.parentFile.mkdirs()
- source.copyTo(target)
- } else if (source.isDirectory) {
- val newPackage = if (pkg.isEmpty()) fileName else "$pkg.$fileName"
- source.listFiles()?.forEach {
- copyAnnotations(codebase, it, File(target, it.name), newPackage)
- }
- }
- }
-
- /** Writes the bytecode for the compiled annotations in the given file list such that they are package private */
- fun rewriteAnnotations(files: List<File>) {
- for (file in files) {
- // Jump directly into androidx/annotation if it appears we were invoked at the top level
- if (file.isDirectory) {
- val android = File(file, "android${File.separator}annotation/")
- if (android.isDirectory) {
- rewriteAnnotations(android)
- val androidx = File(file, "androidx${File.separator}annotation/")
- if (androidx.isDirectory) {
- rewriteAnnotations(androidx)
- }
- continue
- }
- }
-
- rewriteAnnotations(file)
- }
- }
-
/** Returns true if the given annotation class name has source retention as far as the stub
* annotations are concerned.
*/
@@ -139,129 +77,4 @@
error("Found annotation with unknown desired retention: " + qualifiedName)
}
}
-
- /** Writes the bytecode for the compiled annotations in the given file such that they are package private */
- private fun rewriteAnnotations(file: File) {
- when {
- file.isDirectory -> file.listFiles()?.forEach { rewriteAnnotations(it) }
- file.path.endsWith(DOT_CLASS) -> rewriteClassFile(file)
- file.path.endsWith(DOT_JAR) -> rewriteJar(file)
- }
- }
-
- private fun rewriteClassFile(file: File) {
- if (file.name.contains("$")) {
- return // Not worrying about inner classes
- }
- val bytes = file.readBytes()
- val rewritten = rewriteClass(bytes, file.path) ?: return
- file.writeBytes(rewritten)
- }
-
- private fun rewriteClass(bytes: ByteArray, path: String): ByteArray? {
- return try {
- val reader = ClassReader(bytes)
- rewriteOuterClass(reader)
- } catch (ioe: IOException) {
- error("Could not process " + path + ": " + ioe.localizedMessage)
- }
- }
-
- private fun rewriteOuterClass(reader: ClassReader): ByteArray? {
- val classWriter = ClassWriter(ASM6)
- var skip = true
- val classVisitor = object : ClassVisitor(ASM6, classWriter) {
- override fun visit(
- version: Int,
- access: Int,
- name: String,
- signature: String?,
- superName: String?,
- interfaces: Array<out String>?
- ) {
- // Only process public annotations in android.annotation and androidx.annotation
- if (access and Opcodes.ACC_PUBLIC != 0 &&
- access and Opcodes.ACC_ANNOTATION != 0 &&
- (name.startsWith("android/annotation/") || name.startsWith("androidx/annotation/"))
- ) {
- skip = false
- val flagsWithoutPublic = access and Opcodes.ACC_PUBLIC.inv()
- super.visit(version, flagsWithoutPublic, name, signature, superName, interfaces)
- }
- }
- }
-
- reader.accept(classVisitor, 0)
- return if (skip) {
- null
- } else {
- classWriter.toByteArray()
- }
- }
-
- private fun rewriteJar(file: File) {
- val temp = File(file.path + ".temp-$PROGRAM_NAME")
- rewriteJar(file, temp)
- file.delete()
- temp.renameTo(file)
- }
-
- private val zeroTime = FileTime.fromMillis(0)
-
- private fun rewriteJar(from: File, to: File/*, filter: Predicate<String>?*/) {
- Closer.create().use { closer ->
- val fos = closer.register(FileOutputStream(to))
- val bos = closer.register(BufferedOutputStream(fos))
- val zos = closer.register(ZipOutputStream(bos))
-
- val fis = closer.register(FileInputStream(from))
- val bis = closer.register(BufferedInputStream(fis))
- val zis = closer.register(ZipInputStream(bis))
-
- while (true) {
- val entry = zis.nextEntry ?: break
- val name = entry.name
- val newEntry: JarEntry
-
- // Preserve the STORED method of the input entry.
- newEntry = if (entry.method == JarEntry.STORED) {
- val jarEntry = JarEntry(entry)
- jarEntry.size = entry.size
- jarEntry.compressedSize = entry.compressedSize
- jarEntry.crc = entry.crc
- jarEntry
- } else {
- // Create a new entry so that the compressed len is recomputed.
- JarEntry(name)
- }
-
- newEntry.lastAccessTime = zeroTime
- newEntry.creationTime = zeroTime
- newEntry.lastModifiedTime = entry.lastModifiedTime
-
- // add the entry to the jar archive
- zos.putNextEntry(newEntry)
-
- // read the content of the entry from the input stream, and write it into the archive.
- if (name.endsWith(DOT_CLASS) &&
- (name.startsWith("android/annotation/") || name.startsWith("androidx/annotation/")) &&
- name.indexOf("$") == -1 &&
- !entry.isDirectory
- ) {
- val bytes = zis.readBytes()
- val rewritten = rewriteClass(bytes, name)
- if (rewritten != null) {
- zos.write(rewritten)
- } else {
- zos.write(bytes)
- }
- } else {
- zis.copyTo(zos)
- }
-
- zos.closeEntry()
- zis.closeEntry()
- }
- }
- }
}
diff --git a/src/main/java/com/android/tools/metalava/SdkIdentifier.kt b/src/main/java/com/android/tools/metalava/SdkIdentifier.kt
new file mode 100644
index 0000000..7363bbb
--- /dev/null
+++ b/src/main/java/com/android/tools/metalava/SdkIdentifier.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.metalava
+
+/**
+ * ID and aliases for a given SDK.
+ *
+ * SDKs include the Android SDK and SDK extensions (e.g. the T extensions).
+ *
+ * @param id: numerical ID of the SDK, primarily used in generated artifacts and consumed by tools
+ * @param shortname: short name for the SDK, primarily used in configuration files
+ * @param name: human readable name for the SDK; used in the official documentation
+ * @param reference: Java symbol in the Android SDK with the same numerical value as the id, using a
+ * JVM signature like syntax: "some/clazz$INNER$FIELD"
+ */
+data class SdkIdentifier(val id: Int, val shortname: String, val name: String, val reference: String)
diff --git a/src/main/java/com/android/tools/metalava/SignatureFileLoader.kt b/src/main/java/com/android/tools/metalava/SignatureFileLoader.kt
index 67e75ec..55d434d 100644
--- a/src/main/java/com/android/tools/metalava/SignatureFileLoader.kt
+++ b/src/main/java/com/android/tools/metalava/SignatureFileLoader.kt
@@ -24,40 +24,27 @@
object SignatureFileLoader {
private val map = mutableMapOf<File, Codebase>()
- fun load(
- file: File,
- kotlinStyleNulls: Boolean? = null
- ): Codebase {
+ fun load(file: File, kotlinStyleNulls: Boolean = false): Codebase {
return map[file] ?: run {
- val loaded = loadFromSignatureFiles(file, kotlinStyleNulls)
+ val loaded = loadFiles(listOf(file), kotlinStyleNulls)
map[file] = loaded
loaded
}
}
- private fun loadFromSignatureFiles(
- file: File,
- kotlinStyleNulls: Boolean? = null
- ): Codebase {
+ fun loadFiles(files: List<File>, kotlinStyleNulls: Boolean = false): Codebase {
+ require(files.isNotEmpty()) { "files must not be empty" }
+
try {
- val codebase = ApiFile.parseApi(File(file.path), kotlinStyleNulls ?: false)
- codebase.description = "Codebase loaded from ${file.path}"
+ val codebase = ApiFile.parseApi(files, kotlinStyleNulls)
+
+ // Unlike loadFromSources, analyzer methods are not required for text based codebase
+ // because all methods in the API text file belong to an API surface.
+ val analyzer = ApiAnalyzer(codebase)
+ analyzer.addConstructors { _ -> true }
return codebase
} catch (ex: ApiParseException) {
- val message = "Unable to parse signature file $file: ${ex.message}"
- throw DriverException(message)
- }
- }
-
- fun loadFiles(files: List<File>, kotlinStyleNulls: Boolean? = null): Codebase {
- if (files.isEmpty()) {
- throw IllegalArgumentException("files must not be empty")
- }
- try {
- return ApiFile.parseApi(files, kotlinStyleNulls ?: false)
- } catch (ex: ApiParseException) {
- val message = "Unable to parse signature file: ${ex.message}"
- throw DriverException(message)
+ throw DriverException("Unable to parse signature file: ${ex.message}")
}
}
}
diff --git a/src/main/java/com/android/tools/metalava/SignatureWriter.kt b/src/main/java/com/android/tools/metalava/SignatureWriter.kt
index 0a04a5f..0310322 100644
--- a/src/main/java/com/android/tools/metalava/SignatureWriter.kt
+++ b/src/main/java/com/android/tools/metalava/SignatureWriter.kt
@@ -77,8 +77,7 @@
override fun visitConstructor(constructor: ConstructorItem) {
write(" ctor ")
writeModifiers(constructor)
- // Note - we don't write out the type parameter list (constructor.typeParameterList()) in signature files!
- // writeTypeParameterList(constructor.typeParameterList(), addSpace = true)
+ writeTypeParameterList(constructor.typeParameterList(), addSpace = true)
write(constructor.containingClass().fullName())
writeParameterList(constructor)
writeThrowsList(constructor)
diff --git a/src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java b/src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java
deleted file mode 100644
index 67ba3b6..0000000
--- a/src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tools.metalava.apilevels;
-
-import com.android.SdkConstants;
-import com.android.tools.metalava.model.Codebase;
-import com.google.common.io.ByteStreams;
-import com.google.common.io.Closeables;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.objectweb.asm.ClassReader;
-import org.objectweb.asm.Opcodes;
-import org.objectweb.asm.tree.ClassNode;
-import org.objectweb.asm.tree.FieldNode;
-import org.objectweb.asm.tree.MethodNode;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.util.List;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
-
-/**
- * Reads all the android.jar files found in an SDK and generate a map of {@link ApiClass}.
- */
-class AndroidJarReader {
- private int mMinApi;
- private int mCurrentApi;
- private File mCurrentJar;
- private List<String> mPatterns;
- private File[] mApiLevels;
- private final Codebase mCodebase;
-
- AndroidJarReader(@NotNull List<String> patterns,
- int minApi,
- @NotNull File currentJar,
- int currentApi,
- @Nullable Codebase codebase) {
- mPatterns = patterns;
- mMinApi = minApi;
- mCurrentJar = currentJar;
- mCurrentApi = currentApi;
- mCodebase = codebase;
- }
-
- AndroidJarReader(@NotNull File[] apiLevels, int firstApiLevel, @Nullable Codebase codebase) {
- mApiLevels = apiLevels;
- mCodebase = codebase;
- mMinApi = firstApiLevel;
- }
-
- public Api getApi() throws IOException {
- Api api;
- if (mApiLevels != null) {
- int max = mApiLevels.length - 1;
- if (mCodebase != null) {
- max = mCodebase.getApiLevel();
- }
-
- api = new Api(mMinApi, max);
- for (int apiLevel = mMinApi; apiLevel < mApiLevels.length; apiLevel++) {
- File jar = getAndroidJarFile(apiLevel);
- readJar(api, apiLevel, jar);
- }
- if (mCodebase != null) {
- int apiLevel = mCodebase.getApiLevel();
- if (apiLevel != -1) {
- processCodebase(api, apiLevel);
- }
- }
- } else {
- api = new Api(mMinApi, mCurrentApi);
- // Get all the android.jar. They are in platforms-#
- int apiLevel = mMinApi - 1;
- while (true) {
- apiLevel++;
- File jar = null;
- if (apiLevel == mCurrentApi) {
- jar = mCurrentJar;
- }
- if (jar == null) {
- jar = getAndroidJarFile(apiLevel);
- }
- if (jar == null || !jar.isFile()) {
- if (mCodebase != null) {
- processCodebase(api, apiLevel);
- }
- break;
- }
- readJar(api, apiLevel, jar);
- }
- }
-
- api.inlineFromHiddenSuperClasses();
- api.removeImplicitInterfaces();
- api.removeOverridingMethods();
- api.prunePackagePrivateClasses();
-
- return api;
- }
-
- private void processCodebase(Api api, int apiLevel) {
- if (mCodebase == null) {
- return;
- }
- AddApisFromCodebaseKt.addApisFromCodebase(api, apiLevel, mCodebase);
- }
-
- private void readJar(Api api, int apiLevel, File jar) throws IOException {
- api.update(apiLevel);
-
- FileInputStream fis = new FileInputStream(jar);
- ZipInputStream zis = new ZipInputStream(fis);
- ZipEntry entry = zis.getNextEntry();
- while (entry != null) {
- String name = entry.getName();
-
- if (name.endsWith(SdkConstants.DOT_CLASS)) {
- byte[] bytes = ByteStreams.toByteArray(zis);
- ClassReader reader = new ClassReader(bytes);
- ClassNode classNode = new ClassNode(Opcodes.ASM5);
- reader.accept(classNode, 0 /*flags*/);
-
- ApiClass theClass = api.addClass(classNode.name, apiLevel,
- (classNode.access & Opcodes.ACC_DEPRECATED) != 0);
-
- theClass.updateHidden(apiLevel, (classNode.access & Opcodes.ACC_PUBLIC) == 0);
-
- // super class
- if (classNode.superName != null) {
- theClass.addSuperClass(classNode.superName, apiLevel);
- }
-
- // interfaces
- for (Object interfaceName : classNode.interfaces) {
- theClass.addInterface((String) interfaceName, apiLevel);
- }
-
- // fields
- for (Object field : classNode.fields) {
- FieldNode fieldNode = (FieldNode) field;
- if (((fieldNode.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) == 0)) {
- continue;
- }
- if (!fieldNode.name.startsWith("this$") &&
- !fieldNode.name.equals("$VALUES")) {
- boolean deprecated = (fieldNode.access & Opcodes.ACC_DEPRECATED) != 0;
- theClass.addField(fieldNode.name, apiLevel, deprecated);
- }
- }
-
- // methods
- for (Object method : classNode.methods) {
- MethodNode methodNode = (MethodNode) method;
- if (((methodNode.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) == 0)) {
- continue;
- }
- if (!methodNode.name.equals("<clinit>")) {
- boolean deprecated = (methodNode.access & Opcodes.ACC_DEPRECATED) != 0;
- theClass.addMethod(methodNode.name + methodNode.desc, apiLevel, deprecated);
- }
- }
- }
- entry = zis.getNextEntry();
- }
-
- Closeables.close(fis, true);
- }
-
- private File getAndroidJarFile(int apiLevel) {
- if (mApiLevels != null) {
- return mApiLevels[apiLevel];
- }
- for (String pattern : mPatterns) {
- File f = new File(pattern.replace("%", Integer.toString(apiLevel)));
- if (f.isFile()) {
- return f;
- }
- }
- return null;
- }
-}
diff --git a/src/main/java/com/android/tools/metalava/apilevels/Api.java b/src/main/java/com/android/tools/metalava/apilevels/Api.java
index de08cf5..a79af6a 100644
--- a/src/main/java/com/android/tools/metalava/apilevels/Api.java
+++ b/src/main/java/com/android/tools/metalava/apilevels/Api.java
@@ -15,10 +15,16 @@
*/
package com.android.tools.metalava.apilevels;
+import com.android.tools.metalava.SdkIdentifier;
+
import java.io.PrintStream;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
/**
* Represents the whole Android API.
@@ -26,12 +32,10 @@
public class Api extends ApiElement {
private final Map<String, ApiClass> mClasses = new HashMap<>();
private final int mMin;
- private final int mMax;
- public Api(int min, int max) {
+ public Api(int min) {
super("Android API");
mMin = min;
- mMax = max;
}
/**
@@ -39,12 +43,16 @@
*
* @param stream the stream to print the XML elements to
*/
- public void print(PrintStream stream) {
- stream.print("<api version=\"2\"");
+ public void print(PrintStream stream, Set<SdkIdentifier> sdkIdentifiers) {
+ stream.print("<api version=\"3\"");
if (mMin > 1) {
stream.print(" min=\"" + mMin + "\"");
}
stream.println(">");
+ for (SdkIdentifier sdk : sdkIdentifiers) {
+ stream.println(String.format("\t<sdk id=\"%d\" shortname=\"%s\" name=\"%s\" reference=\"%s\"/>",
+ sdk.getId(), sdk.getShortname(), sdk.getName(), sdk.getReference()));
+ }
print(mClasses.values(), "class", "\t", stream);
printClosingTag("api", "", stream);
}
@@ -75,6 +83,37 @@
return mClasses.get(name);
}
+ public Collection<ApiClass> getClasses() {
+ return Collections.unmodifiableCollection(mClasses.values());
+ }
+
+ public void backfillHistoricalFixes() {
+ backfillSdkExtensions();
+ }
+
+ private void backfillSdkExtensions() {
+ // SdkExtensions.getExtensionVersion was added in 30/R, but was a SystemApi
+ // to avoid publishing the versioning API publicly before there was any
+ // valid use for it.
+ // getAllExtensionsVersions was added as part of 31/S
+ // The class and its APIs were made public between S and T, but we pretend
+ // here like it was always public, for maximum backward compatibility.
+ ApiClass sdkExtensions = findClass("android/os/ext/SdkExtensions");
+
+ if (sdkExtensions != null && sdkExtensions.getSince() != 30
+ && sdkExtensions.getSince() != 33) {
+ throw new AssertionError("Received unexpected historical data");
+ } else if (sdkExtensions == null || sdkExtensions.getSince() == 30) {
+ // This is the system API db (30), or module-lib/system-server dbs (null)
+ // They don't need patching.
+ return;
+ }
+ sdkExtensions.update(30, false);
+ sdkExtensions.addSuperClass("java/lang/Object", 30);
+ sdkExtensions.getMethod("getExtensionVersion(I)I").update(30, false);
+ sdkExtensions.getMethod("getAllExtensionVersions()Ljava/util/Map;").update(31, false);
+ }
+
/**
* The bytecode visitor registers interfaces listed for a class. However,
* a class will <b>also</b> implement interfaces implemented by the super classes.
@@ -115,4 +154,40 @@
cls.removeHiddenSuperClasses(mClasses);
}
}
+
+ public void removeMissingClasses() {
+ for (ApiClass cls : mClasses.values()) {
+ cls.removeMissingClasses(mClasses);
+ }
+ }
+
+ public void verifyNoMissingClasses() {
+ Map<String, Set<String>> results = new TreeMap<>();
+ for (ApiClass cls : mClasses.values()) {
+ Set<ApiElement> missing = cls.findMissingClasses(mClasses);
+ // Have the missing classes as keys, and the referencing classes as values.
+ for (ApiElement missingClass : missing) {
+ String missingName = missingClass.getName();
+ if (!results.containsKey(missingName)) {
+ results.put(missingName, new TreeSet<>());
+ }
+ results.get(missingName).add(cls.getName());
+ }
+ }
+ if (!results.isEmpty()) {
+ String message = "";
+ for (Map.Entry<String, Set<String>> entry : results.entrySet()) {
+ message += "\n " + entry.getKey() + " referenced by:";
+ for (String referencer : entry.getValue()) {
+ message += "\n " + referencer;
+ }
+ }
+ throw new IllegalStateException("There are classes in this API that reference other "+
+ "classes that do not exist in this API. "+
+ "This can happen when an api is provided by an apex, but referenced "+
+ "from non-updatable platform code. Use --remove-missing-classes-in-api-levels to "+
+ "make metalava remove these references instead of erroring out."+
+ message);
+ }
+ }
}
diff --git a/src/main/java/com/android/tools/metalava/apilevels/ApiClass.java b/src/main/java/com/android/tools/metalava/apilevels/ApiClass.java
index a40cd72..1184358 100644
--- a/src/main/java/com/android/tools/metalava/apilevels/ApiClass.java
+++ b/src/main/java/com/android/tools/metalava/apilevels/ApiClass.java
@@ -21,11 +21,15 @@
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
/**
* Represents a class or an interface and its methods/fields.
@@ -43,23 +47,23 @@
*/
private int mPrivateUntil; // Package private class?
- private final Map<String, ApiElement> mFields = new HashMap<>();
- private final Map<String, ApiElement> mMethods = new HashMap<>();
+ private final Map<String, ApiElement> mFields = new ConcurrentHashMap<>();
+ private final Map<String, ApiElement> mMethods = new ConcurrentHashMap<>();
public ApiClass(String name, int version, boolean deprecated) {
super(name, version, deprecated);
}
- public void addField(String name, int version, boolean deprecated) {
- addToMap(mFields, name, version, deprecated);
+ public ApiElement addField(String name, int version, boolean deprecated) {
+ return addToMap(mFields, name, version, deprecated);
}
- public void addMethod(String name, int version, boolean deprecated) {
+ public ApiElement addMethod(String name, int version, boolean deprecated) {
// Correct historical mistake in android.jar files
if (name.endsWith(")Ljava/lang/AbstractStringBuilder;")) {
name = name.substring(0, name.length() - ")Ljava/lang/AbstractStringBuilder;".length()) + ")L" + getName() + ";";
}
- addToMap(mMethods, name, version, deprecated);
+ return addToMap(mMethods, name, version, deprecated);
}
public ApiElement addSuperClass(String superClass, int since) {
@@ -107,7 +111,7 @@
return mInterfaces;
}
- private void addToMap(Map<String, ApiElement> elements, String name, int version, boolean deprecated) {
+ private ApiElement addToMap(Map<String, ApiElement> elements, String name, int version, boolean deprecated) {
ApiElement element = elements.get(name);
if (element == null) {
element = new ApiElement(name, version, deprecated);
@@ -115,6 +119,7 @@
} else {
element.update(version, deprecated);
}
+ return element;
}
private ApiElement addToArray(Collection<ApiElement> elements, String name, int version) {
@@ -311,4 +316,62 @@
}
}
}
+
+ // Ensure this class doesn't extend/implement any other classes/interfaces that are
+ // not in the provided api. This can happen when a class in an android.jar file
+ // encodes the inheritance, but the class that is inherited is not present in any
+ // android.jar file. The class would instead be present in an apex's stub jar file.
+ // An example of this is the QosSessionAttributes interface being provided by the
+ // Connectivity apex, but being implemented by NrQosSessionAttributes from
+ // frameworks/base/telephony.
+ public void removeMissingClasses(Map<String, ApiClass> api) {
+ Iterator<ApiElement> superClassIter = mSuperClasses.iterator();
+ while (superClassIter.hasNext()) {
+ ApiElement scls = superClassIter.next();
+ if (!api.containsKey(scls.getName())) {
+ superClassIter.remove();
+ }
+ }
+
+ Iterator<ApiElement> interfacesIter = mInterfaces.iterator();
+ while (interfacesIter.hasNext()) {
+ ApiElement intf = interfacesIter.next();
+ if (!api.containsKey(intf.getName())) {
+ interfacesIter.remove();
+ }
+ }
+ }
+
+ // Returns the set of superclasses or interfaces are not present in the provided api map
+ public Set<ApiElement> findMissingClasses(Map<String, ApiClass> api) {
+ Set<ApiElement> result = new HashSet<>();
+ for (ApiElement scls : mSuperClasses) {
+ if (!api.containsKey(scls.getName())) {
+ result.add(scls);
+ }
+ }
+
+ for (ApiElement intf : mInterfaces) {
+ if (!api.containsKey(intf.getName())) {
+ result.add(intf);
+ }
+ }
+ return result;
+ }
+
+ public Iterator<ApiElement> getFieldIterator() {
+ return mFields.values().iterator();
+ }
+
+ public Iterator<ApiElement> getMethodIterator() {
+ return mMethods.values().iterator();
+ }
+
+ public ApiElement getField(String name) {
+ return mFields.get(name);
+ }
+
+ public ApiElement getMethod(String name) {
+ return mMethods.get(name);
+ }
}
diff --git a/src/main/java/com/android/tools/metalava/apilevels/ApiElement.java b/src/main/java/com/android/tools/metalava/apilevels/ApiElement.java
index ec58ffc..eee7d9b 100644
--- a/src/main/java/com/android/tools/metalava/apilevels/ApiElement.java
+++ b/src/main/java/com/android/tools/metalava/apilevels/ApiElement.java
@@ -22,24 +22,49 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
/**
* Represents an API element, e.g. class, method or field.
*/
public class ApiElement implements Comparable<ApiElement> {
+ public static final int NEVER = Integer.MAX_VALUE;
+
private final String mName;
+
+ /**
+ * The Android platform SDK version this API was first introduced in.
+ */
private int mSince;
+
+ /**
+ * The Android extension SDK version this API was first introduced in.
+ */
+ private int mSinceExtension = NEVER;
+
+ /**
+ * The SDKs and their versions this API was first introduced in.
+ *
+ * The value is a comma-separated list of <int>:<int> values, where the first
+ * <int> is the integer ID of an SDK, and the second <int> the version of that SDK,
+ * in which this API first appeared.
+ *
+ * This field is a super-set of mSince, and if non-null/non-empty, should be preferred.
+ */
+ private String mSdks;
+
+ private String mMainlineModule;
private int mDeprecatedIn;
private int mLastPresentIn;
/**
* @param name the name of the API element
- * @param version an API version for which the API element existed
+ * @param version an API version for which the API element existed, or -1 if the class does
+ * not yet exist in the Android SDK (only in extension SDKs)
* @param deprecated whether the API element was deprecated in the API version in question
*/
ApiElement(String name, int version, boolean deprecated) {
assert name != null;
- assert version > 0;
mName = name;
mSince = version;
mLastPresentIn = version;
@@ -68,11 +93,21 @@
return mName;
}
+ /**
+ * The Android API level of this ApiElement.
+ */
public int getSince() {
return mSince;
}
/**
+ * The extension version of this ApiElement.
+ */
+ public int getSinceExtension() {
+ return mSinceExtension;
+ }
+
+ /**
* Checks if this API element was introduced not later than another API element.
*
* @param other the API element to compare to
@@ -113,6 +148,24 @@
}
/**
+ * Analoguous to update(), but for extensions sdk versions.
+ *
+ * @param version an extension SDK version for which the API element existed
+ */
+ public void updateExtension(int version) {
+ assert version > 0;
+ if (mSinceExtension > version) {
+ mSinceExtension = version;
+ }
+ }
+
+ public void updateSdks(String sdks) { mSdks = sdks; }
+
+ public void updateMainlineModule(String module) { mMainlineModule = module; }
+
+ public String getMainlineModule() { return mMainlineModule; }
+
+ /**
* Checks whether the API element is deprecated or not.
*/
public final boolean isDeprecated() {
@@ -151,10 +204,18 @@
stream.print(tag);
stream.print(" name=\"");
stream.print(encodeAttribute(mName));
+ if (!isEmpty(mMainlineModule) && !isEmpty(mSdks)) {
+ stream.print("\" module=\"");
+ stream.print(encodeAttribute(mMainlineModule));
+ }
if (mSince > parentElement.mSince) {
stream.print("\" since=\"");
stream.print(mSince);
}
+ if (!isEmpty(mSdks) && !Objects.equals(mSdks, parentElement.mSdks)) {
+ stream.print("\" sdks=\"");
+ stream.print(mSdks);
+ }
if (mDeprecatedIn != 0) {
stream.print("\" deprecated=\"");
stream.print(mDeprecatedIn);
@@ -170,6 +231,10 @@
stream.println('>');
}
+ private boolean isEmpty(String s) {
+ return s == null || s.isEmpty();
+ }
+
/**
* Prints homogeneous XML elements to a stream. Each element is printed on a separate line.
* Attributes with values matching the parent API element are omitted.
diff --git a/src/main/java/com/android/tools/metalava/apilevels/ApiGenerator.java b/src/main/java/com/android/tools/metalava/apilevels/ApiGenerator.java
index 28dec48..bfbdf9d 100644
--- a/src/main/java/com/android/tools/metalava/apilevels/ApiGenerator.java
+++ b/src/main/java/com/android/tools/metalava/apilevels/ApiGenerator.java
@@ -17,177 +17,151 @@
package com.android.tools.metalava.apilevels;
import com.android.tools.metalava.model.Codebase;
+import com.android.tools.metalava.SdkIdentifier;
+
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
+import java.nio.file.Files;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
/**
* Main class for command line command to convert the existing API XML/TXT files into diff-based
* simple text files.
*/
public class ApiGenerator {
- public static void main(String[] args) {
- boolean error = false;
- int minApi = 1;
- int currentApi = -1;
- String currentCodename = null;
- File currentJar = null;
- List<String> patterns = new ArrayList<>();
- String outPath = null;
-
- for (int i = 0; i < args.length && !error; i++) {
- String arg = args[i];
-
- if (arg.equals("--pattern")) {
- i++;
- if (i < args.length) {
- patterns.add(args[i]);
- } else {
- System.err.println("Missing argument after " + arg);
- error = true;
- }
- } else if (arg.equals("--current-version")) {
- i++;
- if (i < args.length) {
- currentApi = Integer.parseInt(args[i]);
- if (currentApi <= 22) {
- System.err.println("Suspicious currentApi=" + currentApi + ", expected at least 23");
- error = true;
- }
- } else {
- System.err.println("Missing number >= 1 after " + arg);
- error = true;
- }
- } else if (arg.equals("--current-codename")) {
- i++;
- if (i < args.length) {
- currentCodename = args[i];
- } else {
- System.err.println("Missing codename after " + arg);
- error = true;
- }
- } else if (arg.equals("--current-jar")) {
- i++;
- if (i < args.length) {
- if (currentJar != null) {
- System.err.println("--current-jar should only be specified once");
- error = true;
- }
- String path = args[i];
- currentJar = new File(path);
- } else {
- System.err.println("Missing argument after " + arg);
- error = true;
- }
- } else if (arg.equals("--min-api")) {
- i++;
- if (i < args.length) {
- minApi = Integer.parseInt(args[i]);
- } else {
- System.err.println("Missing number >= 1 after " + arg);
- error = true;
- }
- } else if (arg.length() >= 2 && arg.startsWith("--")) {
- System.err.println("Unknown argument: " + arg);
- error = true;
- } else if (outPath == null) {
- outPath = arg;
- } else if (new File(arg).isDirectory()) {
- String pattern = arg;
- if (!pattern.endsWith(File.separator)) {
- pattern += File.separator;
- }
- pattern += "platforms" + File.separator + "android-%" + File.separator + "android.jar";
- patterns.add(pattern);
- } else {
- System.err.println("Unknown argument: " + arg);
- error = true;
- }
- }
-
- if (!error && outPath == null) {
- System.err.println("Missing out file path");
- error = true;
- }
-
- if (!error && patterns.isEmpty()) {
- System.err.println("Missing SdkFolder or --pattern.");
- error = true;
- }
-
- if (currentJar != null && currentApi == -1 || currentJar == null && currentApi != -1) {
- System.err.println("You must specify both --current-jar and --current-version (or neither one)");
- error = true;
- }
-
- // The SDK version number
- if (currentCodename != null && !"REL".equals(currentCodename)) {
- currentApi++;
- }
-
- if (error) {
- printUsage();
- System.exit(1);
- }
-
- try {
- if (!generate(minApi, currentApi, currentJar, patterns, outPath, null)) {
- System.exit(1);
- }
- } catch (IOException e) {
- e.printStackTrace();
- System.exit(-1);
- }
- }
-
- private static boolean generate(int minApi,
- int currentApi,
- @NotNull File currentJar,
- @NotNull List<String> patterns,
- @NotNull String outPath,
- @Nullable Codebase codebase) throws IOException {
- AndroidJarReader reader = new AndroidJarReader(patterns, minApi, currentJar, currentApi, codebase);
- Api api = reader.getApi();
- return createApiFile(new File(outPath), api);
- }
-
public static boolean generate(@NotNull File[] apiLevels,
int firstApiLevel,
+ int currentApiLevel,
+ boolean isDeveloperPreviewBuild,
@NotNull File outputFile,
- @Nullable Codebase codebase) throws IOException {
- AndroidJarReader reader = new AndroidJarReader(apiLevels, firstApiLevel, codebase);
- Api api = reader.getApi();
- return createApiFile(outputFile, api);
+ @NotNull Codebase codebase,
+ @Nullable File sdkJarRoot,
+ @Nullable File sdkFilterFile,
+ boolean removeMissingClasses) throws IOException, IllegalArgumentException {
+ if ((sdkJarRoot == null) != (sdkFilterFile == null)) {
+ throw new IllegalArgumentException("sdkJarRoot and sdkFilterFile must both be null, or non-null");
+ }
+
+ int notFinalizedApiLevel = currentApiLevel + 1;
+ Api api = readAndroidJars(apiLevels, firstApiLevel);
+ if (isDeveloperPreviewBuild || apiLevels.length - 1 < currentApiLevel) {
+ // Only include codebase if we don't have a prebuilt, finalized jar for it.
+ int apiLevel = isDeveloperPreviewBuild ? notFinalizedApiLevel : currentApiLevel;
+ AddApisFromCodebaseKt.addApisFromCodebase(api, apiLevel, codebase);
+ }
+ api.backfillHistoricalFixes();
+
+ Set<SdkIdentifier> sdkIdentifiers = Collections.emptySet();
+ if (sdkJarRoot != null && sdkFilterFile != null) {
+ sdkIdentifiers = processExtensionSdkApis(api, notFinalizedApiLevel, sdkJarRoot, sdkFilterFile);
+ }
+ api.inlineFromHiddenSuperClasses();
+ api.removeImplicitInterfaces();
+ api.removeOverridingMethods();
+ api.prunePackagePrivateClasses();
+ if (removeMissingClasses) {
+ api.removeMissingClasses();
+ } else {
+ api.verifyNoMissingClasses();
+ }
+ return createApiFile(outputFile, api, sdkIdentifiers);
}
- private static void printUsage() {
- System.err.println("\nGenerates a single API file from the content of an SDK.");
- System.err.println("Usage:");
- System.err.println("\tApiCheck [--min-api=1] OutFile [SdkFolder | --pattern sdk/%/public/android.jar]+");
- System.err.println("Options:");
- System.err.println("--min-api <int> : The first API level to consider (>=1).");
- System.err.println("--pattern <pattern>: Path pattern to find per-API android.jar files, where\n" +
- " '%' is replaced by the API level.");
- System.err.println("--current-jar <path>: Path pattern to find the current android.jar");
- System.err.println("--current-version <int>: The API level for the current API");
- System.err.println("--current-codename <name>: REL, if a release, or codename for previews");
- System.err.println("SdkFolder: if given, this adds the pattern\n" +
- " '$SdkFolder/platforms/android-%/android.jar'");
- System.err.println("If multiple --pattern are specified, they are tried in the order given.\n");
+ private static Api readAndroidJars(File[] apiLevels, int firstApiLevel) {
+ Api api = new Api(firstApiLevel);
+ for (int apiLevel = firstApiLevel; apiLevel < apiLevels.length; apiLevel++) {
+ File jar = apiLevels[apiLevel];
+ JarReaderUtilsKt.readAndroidJar(api, apiLevel, jar);
+ }
+ return api;
+ }
+
+ /**
+ * Modify the extension SDK API parts of an API as dictated by a filter.
+ *
+ * - remove APIs not listed in the filter
+ * - assign APIs listed in the filter their corresponding extensions
+ *
+ * Some APIs only exist in extension SDKs and not in the Android SDK, but for backwards
+ * compatibility with tools that expect the Android SDK to be the only SDK, metalava needs to
+ * assign such APIs some Android SDK API level. The recommended value is current-api-level + 1,
+ * which is what non-finalized APIs use.
+ *
+ * @param api the api to modify
+ * @param apiLevelNotInAndroidSdk fallback API level for APIs not in the Android SDK
+ * @param sdkJarRoot path to directory containing extension SDK jars (usually $ANDROID_ROOT/prebuilts/sdk/extensions)
+ * @param filterPath: path to the filter file. @see ApiToExtensionsMap
+ * @throws IOException if the filter file can not be read
+ * @throws IllegalArgumentException if an error is detected in the filter file, or if no jar files were found
+ */
+ private static Set<SdkIdentifier> processExtensionSdkApis(
+ @NotNull Api api,
+ int apiLevelNotInAndroidSdk,
+ @NotNull File sdkJarRoot,
+ @NotNull File filterPath) throws IOException, IllegalArgumentException {
+ String rules = new String(Files.readAllBytes(filterPath.toPath()));
+
+ Map<String, List<VersionAndPath>> map = ExtensionSdkJarReader.Companion.findExtensionSdkJarFiles(sdkJarRoot);
+ if (map.isEmpty()) {
+ throw new IllegalArgumentException("no extension sdk jar files found in " + sdkJarRoot);
+ }
+ Map<String, ApiToExtensionsMap> moduleMaps = new HashMap<>();
+ for (Map.Entry<String, List<VersionAndPath>> entry : map.entrySet()) {
+ String mainlineModule = entry.getKey();
+ ApiToExtensionsMap moduleMap = ApiToExtensionsMap.Companion.fromXml(mainlineModule, rules);
+ if (moduleMap.isEmpty()) continue; // TODO(b/259115852): remove this (though it is an optimization too).
+
+ moduleMaps.put(mainlineModule, moduleMap);
+ for (VersionAndPath f : entry.getValue()) {
+ JarReaderUtilsKt.readExtensionJar(api, f.version, mainlineModule, f.path, apiLevelNotInAndroidSdk);
+ }
+ }
+ for (ApiClass clazz : api.getClasses()) {
+ String module = clazz.getMainlineModule();
+ if (module == null) continue;
+ ApiToExtensionsMap extensionsMap = moduleMaps.get(module);
+ String sdks = extensionsMap.calculateSdksAttr(clazz.getSince(), apiLevelNotInAndroidSdk,
+ extensionsMap.getExtensions(clazz), clazz.getSinceExtension());
+ clazz.updateSdks(sdks);
+
+ Iterator<ApiElement> iter = clazz.getFieldIterator();
+ while (iter.hasNext()) {
+ ApiElement field = iter.next();
+ sdks = extensionsMap.calculateSdksAttr(field.getSince(), apiLevelNotInAndroidSdk,
+ extensionsMap.getExtensions(clazz, field), field.getSinceExtension());
+ field.updateSdks(sdks);
+ }
+
+ iter = clazz.getMethodIterator();
+ while (iter.hasNext()) {
+ ApiElement method = iter.next();
+ sdks = extensionsMap.calculateSdksAttr(method.getSince(), apiLevelNotInAndroidSdk,
+ extensionsMap.getExtensions(clazz, method), method.getSinceExtension());
+ method.updateSdks(sdks);
+ }
+ }
+ return ApiToExtensionsMap.Companion.fromXml("", rules).getSdkIdentifiers();
}
/**
* Creates the simplified diff-based API level.
*
- * @param outFile the output file
- * @param api the api to write
+ * @param outFile the output file
+ * @param api the api to write
+ * @param sdkIdentifiers SDKs referenced by the api
*/
- private static boolean createApiFile(File outFile, Api api) {
+ private static boolean createApiFile(@NotNull File outFile, @NotNull Api api, @NotNull Set<SdkIdentifier> sdkIdentifiers) {
File parentFile = outFile.getParentFile();
if (!parentFile.exists()) {
boolean ok = parentFile.mkdirs();
@@ -198,7 +172,7 @@
}
try (PrintStream stream = new PrintStream(outFile, "UTF-8")) {
stream.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
- api.print(stream);
+ api.print(stream, sdkIdentifiers);
} catch (Exception e) {
e.printStackTrace();
return false;
diff --git a/src/main/java/com/android/tools/metalava/apilevels/ApiToExtensionsMap.kt b/src/main/java/com/android/tools/metalava/apilevels/ApiToExtensionsMap.kt
new file mode 100644
index 0000000..087d690
--- /dev/null
+++ b/src/main/java/com/android/tools/metalava/apilevels/ApiToExtensionsMap.kt
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.metalava.apilevels
+
+import com.android.tools.metalava.SdkIdentifier
+import org.xml.sax.Attributes
+import org.xml.sax.helpers.DefaultHandler
+import javax.xml.parsers.SAXParserFactory
+
+/**
+ * A filter of classes, fields and methods that are allowed in and extension SDK, and for each item,
+ * what extension SDK it first appeared in. Also, a mapping between SDK name and numerical ID.
+ *
+ * Internally, the filers are represented as a tree, where each node in the tree matches a part of a
+ * package, class or member name. For example, given the patterns
+ *
+ * com.example.Foo -> [A]
+ * com.example.Foo#someMethod -> [B]
+ * com.example.Bar -> [A, C]
+ *
+ * (anything prefixed with com.example.Foo is allowed and part of the A extension, except for
+ * com.example.Foo#someMethod which is part of B; anything prefixed with com.example.Bar is part
+ * of both A and C), the internal tree looks like
+ *
+ * root -> null
+ * com -> null
+ * example -> null
+ * Foo -> [A]
+ * someMethod -> [B]
+ * Bar -> [A, C]
+ */
+class ApiToExtensionsMap private constructor(
+ private val sdkIdentifiers: Set<SdkIdentifier>,
+ private val root: Node,
+) {
+ fun isEmpty(): Boolean = root.children.isEmpty() && root.extensions.isEmpty()
+
+ fun getExtensions(clazz: ApiClass): List<String> = getExtensions(clazz.name.toDotNotation())
+
+ fun getExtensions(clazz: ApiClass, member: ApiElement): List<String> =
+ getExtensions(clazz.name.toDotNotation() + "#" + member.name.toDotNotation())
+
+ fun getExtensions(what: String): List<String> {
+ // Special case: getExtensionVersion is not part of an extension
+ val sdkExtensions = "android.os.ext.SdkExtensions"
+ if (what == sdkExtensions || what == "$sdkExtensions#getExtensionVersion") {
+ return listOf()
+ }
+
+ val parts = what.split(REGEX_DELIMITERS)
+
+ var lastSeenExtensions = root.extensions
+ var node = root.children.findNode(parts[0]) ?: return lastSeenExtensions
+ if (node.extensions.isNotEmpty()) {
+ lastSeenExtensions = node.extensions
+ }
+
+ for (part in parts.stream().skip(1)) {
+ node = node.children.findNode(part) ?: break
+ if (node.extensions.isNotEmpty()) {
+ lastSeenExtensions = node.extensions
+ }
+ }
+ return lastSeenExtensions
+ }
+
+ fun getSdkIdentifiers(): Set<SdkIdentifier> = sdkIdentifiers.toSet()
+
+ /**
+ * Construct a `sdks` attribute value
+ *
+ * `sdks` is an XML attribute on class, method and fields in the XML generated by ARG_GENERATE_API_LEVELS.
+ * It expresses in what SDKs an API exist, and in which version of each SDK it was first introduced;
+ * `sdks` replaces the `since` attribute.
+ *
+ * The format of `sdks` is
+ *
+ * sdks="ext:version[,ext:version[,...]]
+ *
+ * where <ext> is the numerical ID of the SDK, and <version> is the version in which the API was introduced.
+ *
+ * The returned string is guaranteed to be one of
+ *
+ * - list of (extensions,finalized_version) pairs + ANDROID_SDK:finalized_dessert
+ * - list of (extensions,finalized_version) pairs
+ * - ANDROID_SDK:finalized_dessert
+ * - ANDROID_SDK:next_dessert_int (for symbols not finalized anywhere)
+ *
+ * See go/mainline-sdk-api-versions-xml for more information.
+ *
+ * @param androidSince Android dessert version in which this symbol was finalized, or notFinalizedValue
+ * if this symbol has not been finalized in an Android dessert
+ * @param notFinalizedValue value used together with the Android SDK ID to indicate that this symbol
+ * has not been finalized at all
+ * @param extensions names of the SDK extensions in which this symbol has been finalized (may be non-empty
+ * even if extensionsSince is ApiElement.NEVER)
+ * @param extensionsSince the version of the SDK extensions in which this API was initially introduced
+ * (same value for all SDK extensions), or ApiElement.NEVER if this symbol
+ * has not been finalized in any SDK extension (regardless of the extensions argument)
+ * @return an `sdks` value suitable for including verbatim in XML
+ */
+ fun calculateSdksAttr(
+ androidSince: Int,
+ notFinalizedValue: Int,
+ extensions: List<String>,
+ extensionsSince: Int
+ ): String {
+ // Special case: symbol not finalized anywhere -> "ANDROID_SDK:next_dessert_int"
+ if (androidSince == notFinalizedValue && extensionsSince == ApiElement.NEVER) {
+ return "$ANDROID_PLATFORM_SDK_ID:$notFinalizedValue"
+ }
+
+ val versions = mutableSetOf<String>()
+ // Only include SDK extensions if the symbol has been finalized in at least one
+ if (extensionsSince != ApiElement.NEVER) {
+ for (ext in extensions) {
+ val ident = sdkIdentifiers.find {
+ it.shortname == ext
+ } ?: throw IllegalStateException("unknown extension SDK \"$ext\"")
+ assert(ident.id != ANDROID_PLATFORM_SDK_ID) // invariant
+ versions.add("${ident.id}:$extensionsSince")
+ }
+ }
+
+ // Only include the Android SDK in `sdks` if
+ // - the symbol has been finalized in an Android dessert, and
+ // - the symbol has been finalized in at least one SDK extension
+ if (androidSince != notFinalizedValue && versions.isNotEmpty()) {
+ versions.add("$ANDROID_PLATFORM_SDK_ID:$androidSince")
+ }
+ return versions.joinToString(",")
+ }
+
+ companion object {
+ // Hard-coded ID for the Android platform SDK. Used identically as the extension SDK IDs
+ // to express when an API first appeared in an SDK.
+ const val ANDROID_PLATFORM_SDK_ID = 0
+
+ private val REGEX_DELIMITERS = Regex("[.#$]")
+
+ /*
+ * Create an ApiToExtensionsMap from a list of text based rules.
+ *
+ * The input is XML:
+ *
+ * <?xml version="1.0" encoding="utf-8"?>
+ * <sdk-extensions-info version="1">
+ * <sdk name="<name>" shortname="<short-name>" id="<int>" reference="<constant>" />
+ * <symbol jar="<jar>" pattern="<pattern>" sdks="<sdks>" />
+ * </sdk-extensions-info>
+ *
+ * The <sdk> and <symbol> tags may be repeated.
+ *
+ * - <name> is a long name for the SDK, e.g. "R Extensions".
+ *
+ * - <short-name> is a short name for the SDK, e.g. "R-ext".
+ *
+ * - <id> is the numerical identifier for the SDK, e.g. 30. It is an error to use the
+ * Android SDK ID (0).
+ *
+ * - <jar> is the jar file symbol belongs to, named after the jar file in
+ * prebuilts/sdk/extensions/<int>/public, e.g. "framework-sdkextensions".
+ *
+ * - <constant> is a Java symbol that can be passed to `SdkExtensions.getExtensionVersion`
+ * to look up the version of the corresponding SDK, e.g.
+ * "android/os/Build$VERSION_CODES$R"
+ *
+ * - <pattern> is either '*', which matches everything, or a 'com.foo.Bar$Inner#member'
+ * string (or prefix thereof terminated before . or $), which matches anything with that
+ * prefix. Note that arguments and return values of methods are omitted (and there is no
+ * way to distinguish overloaded methods).
+ *
+ * - <sdks> is a comma separated list of SDKs in which the symbol defined by <jar> and
+ * <pattern> appears; the list items are <name> attributes of SDKs defined in the XML.
+ *
+ * It is an error to specify the same <jar> and <pattern> pair twice.
+ *
+ * A more specific <symbol> rule has higher precedence than a less specific rule.
+ *
+ * @param jar jar file to limit lookups to: ignore symbols not present in this jar file
+ * @param xml XML as described above
+ * @throws IllegalArgumentException if the XML is malformed
+ */
+ fun fromXml(filterByJar: String, xml: String): ApiToExtensionsMap {
+ val root = Node("<root>")
+ val sdkIdentifiers = mutableSetOf<SdkIdentifier>()
+ val allSeenExtensions = mutableSetOf<String>()
+
+ val parser = SAXParserFactory.newDefaultInstance().newSAXParser()
+ try {
+ parser.parse(
+ xml.byteInputStream(),
+ object : DefaultHandler() {
+ override fun startElement(uri: String, localName: String, qualifiedName: String, attributes: Attributes) {
+ when (qualifiedName) {
+ "sdk" -> {
+ val id = attributes.getIntOrThrow(qualifiedName, "id")
+ val shortname = attributes.getStringOrThrow(qualifiedName, "shortname")
+ val name = attributes.getStringOrThrow(qualifiedName, "name")
+ val reference = attributes.getStringOrThrow(qualifiedName, "reference")
+ sdkIdentifiers.add(SdkIdentifier(id, shortname, name, reference))
+ }
+ "symbol" -> {
+ val jar = attributes.getStringOrThrow(qualifiedName, "jar")
+ if (jar != filterByJar) {
+ return
+ }
+ val sdks = attributes.getStringOrThrow(qualifiedName, "sdks").split(',')
+ if (sdks != sdks.distinct()) {
+ throw IllegalArgumentException("symbol lists the same SDK multiple times: '$sdks'")
+ }
+ allSeenExtensions.addAll(sdks)
+ val pattern = attributes.getStringOrThrow(qualifiedName, "pattern")
+ if (pattern == "*") {
+ root.extensions = sdks
+ return
+ }
+ // add each part of the pattern as separate nodes, e.g. if pattern is
+ // com.example.Foo, add nodes, "com" -> "example" -> "Foo"
+ val parts = pattern.split(REGEX_DELIMITERS)
+ var node = root.children.addNode(parts[0])
+ for (name in parts.stream().skip(1)) {
+ node = node.children.addNode(name)
+ }
+ if (node.extensions.isNotEmpty()) {
+ throw IllegalArgumentException("duplicate pattern: $pattern")
+ }
+ node.extensions = sdks
+ }
+ }
+ }
+ }
+ )
+ } catch (e: Throwable) {
+ throw IllegalArgumentException("failed to parse xml", e)
+ }
+
+ // verify: the predefined Android platform SDK ID is not reused as an extension SDK ID
+ if (sdkIdentifiers.any { it.id == ANDROID_PLATFORM_SDK_ID }) {
+ throw IllegalArgumentException("bad SDK definition: the ID $ANDROID_PLATFORM_SDK_ID is reserved for the Android platform SDK")
+ }
+
+ // verify: all rules refer to declared SDKs
+ val allSdkNames = sdkIdentifiers.map { it.shortname }.toList()
+ for (ext in allSeenExtensions) {
+ if (!allSdkNames.contains(ext)) {
+ throw IllegalArgumentException("bad SDK definitions: undefined SDK $ext")
+ }
+ }
+
+ // verify: no duplicate SDK IDs
+ if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.id }.size) {
+ throw IllegalArgumentException("bad SDK definitions: duplicate SDK IDs")
+ }
+
+ // verify: no duplicate SDK names
+ if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.shortname }.size) {
+ throw IllegalArgumentException("bad SDK definitions: duplicate SDK short names")
+ }
+
+ // verify: no duplicate SDK names
+ if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.name }.size) {
+ throw IllegalArgumentException("bad SDK definitions: duplicate SDK names")
+ }
+
+ // verify: no duplicate SDK references
+ if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.reference }.size) {
+ throw IllegalArgumentException("bad SDK definitions: duplicate SDK references")
+ }
+
+ return ApiToExtensionsMap(sdkIdentifiers, root)
+ }
+ }
+}
+
+private fun MutableSet<Node>.addNode(name: String): Node {
+ findNode(name)?.let {
+ return it
+ }
+ val node = Node(name)
+ add(node)
+ return node
+}
+
+private fun Attributes.getStringOrThrow(tag: String, attr: String): String = getValue(attr) ?: throw IllegalArgumentException("<$tag>: missing attribute: $attr")
+
+private fun Attributes.getIntOrThrow(tag: String, attr: String): Int = getStringOrThrow(tag, attr).toIntOrNull() ?: throw IllegalArgumentException("<$tag>: attribute $attr: not an integer")
+
+private fun Set<Node>.findNode(breadcrumb: String): Node? = find { it.breadcrumb == breadcrumb }
+
+private fun String.toDotNotation(): String = split('(')[0].replace('/', '.')
+
+private class Node(val breadcrumb: String) {
+ var extensions: List<String> = emptyList()
+ val children: MutableSet<Node> = mutableSetOf()
+}
diff --git a/src/main/java/com/android/tools/metalava/apilevels/ExtensionSdkJarReader.kt b/src/main/java/com/android/tools/metalava/apilevels/ExtensionSdkJarReader.kt
new file mode 100644
index 0000000..dc3b360
--- /dev/null
+++ b/src/main/java/com/android/tools/metalava/apilevels/ExtensionSdkJarReader.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.metalava.apilevels
+
+import com.android.SdkConstants
+import com.android.SdkConstants.PLATFORM_WINDOWS
+import java.io.File
+
+class ExtensionSdkJarReader() {
+
+ companion object {
+ private val REGEX_JAR_PATH = run {
+ var pattern = ".*/(\\d+)/public/(.*)\\.jar$"
+ if (SdkConstants.currentPlatform() == PLATFORM_WINDOWS) {
+ pattern = pattern.replace("/", "\\\\")
+ }
+ Regex(pattern)
+ }
+
+ /**
+ * Find extension SDK jar files in an extension SDK tree.
+ *
+ * @return a mapping SDK jar file -> list of VersionAndPath objects, sorted from earliest
+ * to last version
+ */
+ fun findExtensionSdkJarFiles(root: File): Map<String, List<VersionAndPath>> {
+ val map = mutableMapOf<String, MutableList<VersionAndPath>>()
+ root.walk().maxDepth(3).mapNotNull { file ->
+ REGEX_JAR_PATH.matchEntire(file.path)?.groups?.let { groups ->
+ Triple(groups[2]!!.value, groups[1]!!.value.toInt(), file)
+ }
+ }.sortedBy {
+ it.second
+ }.forEach {
+ map.getOrPut(it.first) {
+ mutableListOf()
+ }.add(VersionAndPath(it.second, it.third))
+ }
+ return map
+ }
+ }
+}
+
+data class VersionAndPath(@JvmField val version: Int, @JvmField val path: File)
diff --git a/src/main/java/com/android/tools/metalava/apilevels/JarReaderUtils.kt b/src/main/java/com/android/tools/metalava/apilevels/JarReaderUtils.kt
new file mode 100644
index 0000000..58307cb
--- /dev/null
+++ b/src/main/java/com/android/tools/metalava/apilevels/JarReaderUtils.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.metalava.apilevels
+
+import com.android.SdkConstants
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.tree.ClassNode
+import org.objectweb.asm.tree.FieldNode
+import org.objectweb.asm.tree.MethodNode
+import java.io.File
+import java.io.FileInputStream
+import java.util.zip.ZipInputStream
+
+fun Api.readAndroidJar(apiLevel: Int, jar: File) {
+ update(apiLevel)
+ readJar(apiLevel, jar)
+}
+
+fun Api.readExtensionJar(extensionVersion: Int, module: String, jar: File, nextApiLevel: Int) {
+ readJar(nextApiLevel, jar, extensionVersion, module)
+}
+
+fun Api.readJar(apiLevel: Int, jar: File, extensionVersion: Int? = null, module: String? = null) {
+ val fis = FileInputStream(jar)
+ ZipInputStream(fis).use { zis ->
+ var entry = zis.nextEntry
+ while (entry != null) {
+ if (!entry.name.endsWith(SdkConstants.DOT_CLASS)) {
+ entry = zis.nextEntry
+ continue
+ }
+ val bytes = zis.readBytes()
+ val reader = ClassReader(bytes)
+ val classNode = ClassNode(Opcodes.ASM5)
+ reader.accept(classNode, 0)
+
+ val theClass = addClass(
+ classNode.name,
+ apiLevel,
+ (classNode.access and Opcodes.ACC_DEPRECATED) != 0
+ )
+ extensionVersion?.let { theClass.updateExtension(extensionVersion) }
+ module?.let { theClass.updateMainlineModule(module) }
+
+ theClass.updateHidden(
+ apiLevel,
+ (classNode.access and Opcodes.ACC_PUBLIC) == 0
+ )
+
+ // super class
+ if (classNode.superName != null) {
+ theClass.addSuperClass(classNode.superName, apiLevel)
+ }
+
+ // interfaces
+ for (interfaceName in classNode.interfaces) {
+ theClass.addInterface(interfaceName, apiLevel)
+ }
+
+ // fields
+ for (field in classNode.fields) {
+ val fieldNode = field as FieldNode
+ if ((fieldNode.access and (Opcodes.ACC_PUBLIC or Opcodes.ACC_PROTECTED)) == 0) {
+ continue
+ }
+ if (!fieldNode.name.startsWith("this\$") && fieldNode.name != "\$VALUES") {
+ val apiField = theClass.addField(
+ fieldNode.name,
+ apiLevel,
+ (fieldNode.access and Opcodes.ACC_DEPRECATED) != 0
+ )
+ extensionVersion?.let { apiField.updateExtension(extensionVersion) }
+ }
+ }
+
+ // methods
+ for (method in classNode.methods) {
+ val methodNode = method as MethodNode
+ if ((methodNode.access and (Opcodes.ACC_PUBLIC or Opcodes.ACC_PROTECTED)) == 0) {
+ continue
+ }
+ if (methodNode.name != "<clinit>") {
+ val apiMethod = theClass.addMethod(
+ methodNode.name + methodNode.desc, apiLevel,
+ (methodNode.access and Opcodes.ACC_DEPRECATED) != 0
+ )
+ extensionVersion?.let { apiMethod.updateExtension(extensionVersion) }
+ }
+ }
+
+ entry = zis.nextEntry
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt b/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt
index f2e1f25..9159224 100644
--- a/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt
@@ -42,7 +42,6 @@
import com.intellij.psi.PsiReference
import org.jetbrains.kotlin.psi.KtObjectDeclaration
import org.jetbrains.uast.UElement
-import java.util.function.Predicate
fun isNullableAnnotation(qualifiedName: String): Boolean {
return qualifiedName.endsWith("Nullable")
@@ -175,18 +174,20 @@
* Annotations that should not be exported are mapped to null.
*/
fun mapName(
- codebase: Codebase,
qualifiedName: String?,
- filter: Predicate<Item>? = null,
target: AnnotationTarget = AnnotationTarget.SIGNATURE_FILE
): String? {
qualifiedName ?: return null
- if (options.passThroughAnnotations.contains(qualifiedName)) {
+ if (options.passThroughAnnotations.contains(qualifiedName) ||
+ options.showAnnotations.matches(qualifiedName) ||
+ options.hideAnnotations.matches(qualifiedName)
+ ) {
return qualifiedName
}
if (options.excludeAnnotations.contains(qualifiedName)) {
return null
}
+
when (qualifiedName) {
// Resource annotations
"android.annotation.AnimRes" -> return "androidx.annotation.AnimRes"
@@ -282,21 +283,7 @@
"android.annotation.Condemned",
"android.annotation.Hide",
- "android.annotation.Widget" -> {
- // Remove, unless (a) public or (b) specifically included in --showAnnotations
- return if (options.showAnnotations.matches(qualifiedName)) {
- qualifiedName
- } else if (filter != null) {
- val cls = codebase.findClass(qualifiedName)
- if (cls != null && filter.test(cls)) {
- qualifiedName
- } else {
- null
- }
- } else {
- qualifiedName
- }
- }
+ "android.annotation.Widget" -> return qualifiedName
// Included for analysis, but should not be exported:
"android.annotation.BroadcastBehavior",
@@ -325,29 +312,10 @@
// Unknown Android platform annotations
qualifiedName.startsWith("android.annotation.") -> {
- // Remove, unless specifically included in --showAnnotations
- return if (options.showAnnotations.matches(qualifiedName)) {
- qualifiedName
- } else {
- null
- }
+ return null
}
- else -> {
- // Remove, unless (a) public or (b) specifically included in --showAnnotations
- return if (options.showAnnotations.matches(qualifiedName)) {
- qualifiedName
- } else if (filter != null) {
- val cls = codebase.findClass(qualifiedName)
- if (cls != null && filter.test(cls)) {
- qualifiedName
- } else {
- null
- }
- } else {
- qualifiedName
- }
- }
+ else -> qualifiedName
}
}
}
@@ -409,6 +377,14 @@
"kotlin.UseExperimental",
"kotlin.OptIn" -> return NO_ANNOTATION_TARGETS
+ // These optimization-related annotations shouldn't be exported.
+ "dalvik.annotation.optimization.CriticalNative",
+ "dalvik.annotation.optimization.FastNative",
+ "dalvik.annotation.optimization.NeverCompile",
+ "dalvik.annotation.optimization.NeverInline",
+ "dalvik.annotation.optimization.ReachabilitySensitive" ->
+ return NO_ANNOTATION_TARGETS
+
// TODO(aurimas): consider using annotation directly instead of modifiers
"kotlin.Deprecated" -> return NO_ANNOTATION_TARGETS // tracked separately as a pseudo-modifier
"android.annotation.DeprecatedForSdk",
@@ -474,10 +450,6 @@
// habit of loading all annotation classes it encounters.)
if (qualifiedName.startsWith("androidx.annotation.")) {
- if (options.includeSourceRetentionAnnotations) {
- return ANNOTATION_IN_ALL_STUBS
- }
-
if (qualifiedName == ANDROIDX_NULLABLE || qualifiedName == ANDROIDX_NONNULL) {
// Right now, nullness annotations (other than @RecentlyNullable and @RecentlyNonNull)
// have to go in external annotations since they aren't in the class path for
@@ -538,10 +510,14 @@
fun unshortenAnnotation(source: String): String {
return when {
source == "@Deprecated" -> "@java.lang.Deprecated"
- // These 3 annotations are in the android.annotation. package, not androidx.annotation
+ // The first 3 annotations are in the android.annotation. package, not androidx.annotation
+ // Nullability annotations are written as @NonNull and @Nullable in API text files,
+ // and these should be linked no android.annotation package when generating stubs.
source.startsWith("@SystemService") ||
source.startsWith("@TargetApi") ||
- source.startsWith("@SuppressLint") ->
+ source.startsWith("@SuppressLint") ||
+ source.startsWith("@Nullable") ||
+ source.startsWith("@NonNull") ->
"@android.annotation." + source.substring(1)
// If the first character of the name (after "@") is lower-case, then
// assume it's a package name, so no need to shorten it.
diff --git a/src/main/java/com/android/tools/metalava/model/ClassItem.kt b/src/main/java/com/android/tools/metalava/model/ClassItem.kt
index 4e11cc3..fa045f1 100644
--- a/src/main/java/com/android/tools/metalava/model/ClassItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/ClassItem.kt
@@ -230,6 +230,10 @@
return qualifiedName() == JAVA_LANG_OBJECT
}
+ fun isAbstractClass(): Boolean {
+ return this.modifiers.isAbstract()
+ }
+
// Mutation APIs: Used to "fix up" the API hierarchy (in [ApiAnalyzer]) to only expose
// visible parts of the API)
diff --git a/src/main/java/com/android/tools/metalava/model/Codebase.kt b/src/main/java/com/android/tools/metalava/model/Codebase.kt
index e8a9a58..2ba461e 100644
--- a/src/main/java/com/android/tools/metalava/model/Codebase.kt
+++ b/src/main/java/com/android/tools/metalava/model/Codebase.kt
@@ -57,9 +57,6 @@
*/
var location: File
- /** The API level of this codebase, or -1 if not known */
- var apiLevel: Int
-
/** The packages in the codebase (may include packages that are not included in the API) */
fun getPackages(): PackageList
@@ -269,7 +266,6 @@
private var minSdkVersion: MinSdkVersion? = null
override var original: Codebase? = null
override var units: List<PsiFile> = emptyList()
- override var apiLevel: Int = -1
@Suppress("LeakingThis")
override val printer = CodePrinter(this)
@Suppress("LeakingThis")
diff --git a/src/main/java/com/android/tools/metalava/model/Item.kt b/src/main/java/com/android/tools/metalava/model/Item.kt
index f309fe4..462403c 100644
--- a/src/main/java/com/android/tools/metalava/model/Item.kt
+++ b/src/main/java/com/android/tools/metalava/model/Item.kt
@@ -299,7 +299,7 @@
}
builder.append(' ')
if (includeReturnValue && !item.isConstructor()) {
- builder.append(item.returnType()?.toSimpleType())
+ builder.append(item.returnType().toSimpleType())
builder.append(' ')
}
appendMethodSignature(builder, item, includeParameterNames, includeParameterTypes)
diff --git a/src/main/java/com/android/tools/metalava/model/MethodItem.kt b/src/main/java/com/android/tools/metalava/model/MethodItem.kt
index e4eb38e..fce301a 100644
--- a/src/main/java/com/android/tools/metalava/model/MethodItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/MethodItem.kt
@@ -32,8 +32,8 @@
/** Whether this method is a constructor */
fun isConstructor(): Boolean
- /** The type of this field, or null for constructors */
- fun returnType(): TypeItem?
+ /** The type of this field. Returns the containing class for constructors */
+ fun returnType(): TypeItem
/** The list of parameters */
fun parameters(): List<ParameterItem>
@@ -71,7 +71,7 @@
}
sb.append(")")
- sb.append(if (voidConstructorTypes && isConstructor()) "V" else returnType()?.internalName() ?: "V")
+ sb.append(if (voidConstructorTypes && isConstructor()) "V" else returnType().internalName())
return sb.toString()
}
@@ -214,9 +214,7 @@
if (!isConstructor()) {
val type = returnType()
- if (type != null) { // always true when not a constructor
- visitor.visitType(type, this)
- }
+ visitor.visitType(type, this)
}
for (parameter in parameters()) {
@@ -229,9 +227,7 @@
if (!isConstructor()) {
val type = returnType()
- if (type != null) {
- visitor.visitType(type, this)
- }
+ visitor.visitType(type, this)
}
}
@@ -369,7 +365,7 @@
return when {
modifiers.hasJvmSyntheticAnnotation() -> false
isConstructor() -> false
- (returnType()?.primitive != true) -> true
+ (!returnType().primitive) -> true
parameters().any { !it.type().primitive } -> true
else -> false
}
@@ -380,7 +376,7 @@
return true
}
- if (!isConstructor() && returnType()?.primitive != true) {
+ if (!isConstructor() && !returnType().primitive) {
if (!modifiers.hasNullnessInfo()) {
return false
}
@@ -479,16 +475,14 @@
}
val returnType = returnType()
- if (returnType != null) {
- val returnTypeClass = returnType.asClass()
- if (returnTypeClass != null && !filterReference.test(returnTypeClass)) {
- return true
- }
- if (returnType.hasTypeArguments()) {
- for (argument in returnType.typeArgumentClasses()) {
- if (!filterReference.test(argument)) {
- return true
- }
+ val returnTypeClass = returnType.asClass()
+ if (returnTypeClass != null && !filterReference.test(returnTypeClass)) {
+ return true
+ }
+ if (returnType.hasTypeArguments()) {
+ for (argument in returnType.typeArgumentClasses()) {
+ if (!filterReference.test(argument)) {
+ return true
}
}
}
diff --git a/src/main/java/com/android/tools/metalava/model/ModifierList.kt b/src/main/java/com/android/tools/metalava/model/ModifierList.kt
index 5b81463..5ea4468 100644
--- a/src/main/java/com/android/tools/metalava/model/ModifierList.kt
+++ b/src/main/java/com/android/tools/metalava/model/ModifierList.kt
@@ -192,7 +192,7 @@
* in this modifier list
*/
fun findAnnotation(qualifiedName: String): AnnotationItem? {
- val mappedName = AnnotationItem.mapName(codebase, qualifiedName)
+ val mappedName = AnnotationItem.mapName(qualifiedName)
return annotations().firstOrNull {
mappedName == it.qualifiedName
}
diff --git a/src/main/java/com/android/tools/metalava/model/TypeItem.kt b/src/main/java/com/android/tools/metalava/model/TypeItem.kt
index 8084dca..2787972 100644
--- a/src/main/java/com/android/tools/metalava/model/TypeItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/TypeItem.kt
@@ -95,6 +95,15 @@
return s
}
+ /**
+ * Returns the element type if the type is an array or contains a vararg.
+ * If the element is not an array or does not contain a vararg,
+ * returns the original type string.
+ */
+ fun toElementType(): String {
+ return toTypeString().replace("...", "").replace("[]", "")
+ }
+
val primitive: Boolean
fun typeArgumentClasses(): List<ClassItem>
@@ -127,13 +136,10 @@
fun defaultValue(): Any? {
return when (toTypeString()) {
"boolean" -> false
+ "char", "int", "float", "double" -> 0
"byte" -> 0.toByte()
- "char" -> '\u0000'
"short" -> 0.toShort()
- "int" -> 0
"long" -> 0L
- "float" -> 0f
- "double" -> 0.0
else -> null
}
}
@@ -143,6 +149,53 @@
fun hasTypeArguments(): Boolean = toTypeString().contains("<")
/**
+ * If the item has type arguments, return a list of type arguments.
+ * If simplified is true, returns the simplified forms of the type arguments.
+ * e.g. when type arguments are <K, V extends some.arbitrary.Class>, [K, V] will be returned.
+ * If the item does not have any type arguments, return an empty list.
+ */
+ fun typeArguments(simplified: Boolean = false): List<String> {
+ if (!hasTypeArguments()) {
+ return emptyList()
+ }
+ val typeString = toTypeString()
+ val bracketRemovedTypeString = typeString.indexOf('<')
+ .let { typeString.substring(it + 1, typeString.length - 1) }
+ val typeArguments = mutableListOf<String>()
+ var builder = StringBuilder()
+ var balance = 0
+ var idx = 0
+ while (idx < bracketRemovedTypeString.length) {
+ when (val s = bracketRemovedTypeString[idx]) {
+ ',' -> {
+ if (balance == 0) {
+ typeArguments.add(builder.toString())
+ builder = StringBuilder()
+ } else {
+ builder.append(s)
+ }
+ }
+ '<' -> {
+ balance += 1
+ builder.append(s)
+ }
+ '>' -> {
+ balance -= 1
+ builder.append(s)
+ }
+ else -> builder.append(s)
+ }
+ idx += 1
+ }
+ typeArguments.add(builder.toString())
+
+ if (simplified) {
+ return typeArguments.map { it.substringBefore(" extends ").trim() }
+ }
+ return typeArguments.map { it.trim() }
+ }
+
+ /**
* If this type is a type parameter, then return the corresponding [TypeParameterItem].
* The optional [context] provides the method or class where this type parameter
* appears, and can be used for example to resolve the bounds for a type variable
diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiAnnotationItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiAnnotationItem.kt
index 4077b2a..5de4137 100644
--- a/src/main/java/com/android/tools/metalava/model/psi/PsiAnnotationItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/psi/PsiAnnotationItem.kt
@@ -51,7 +51,7 @@
val psiAnnotation: PsiAnnotation,
override val originalName: String?
) : DefaultAnnotationItem(codebase) {
- override val qualifiedName: String? = AnnotationItem.mapName(codebase, originalName)
+ override val qualifiedName: String? = AnnotationItem.mapName(originalName)
override fun toString(): String = toSource()
@@ -137,7 +137,7 @@
target: AnnotationTarget,
showDefaultAttrs: Boolean
) {
- val qualifiedName = AnnotationItem.mapName(codebase, originalName, null, target) ?: return
+ val qualifiedName = AnnotationItem.mapName(originalName, target) ?: return
val attributes = getAttributes(psiAnnotation, showDefaultAttrs)
if (attributes.isEmpty()) {
diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt
index 74b35d4..f782d27 100644
--- a/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt
+++ b/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt
@@ -40,6 +40,7 @@
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiClassOwner
import com.intellij.psi.PsiClassType
+import com.intellij.psi.PsiCodeBlock
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiErrorElement
import com.intellij.psi.PsiField
@@ -169,7 +170,9 @@
for (psiFile in psiFiles.asSequence().distinct()) {
tick() // show progress
- psiFile.accept(object : JavaRecursiveElementVisitor() {
+ // Visiting psiFile directly would eagerly load the entire file even though we only need
+ // the importList here.
+ (psiFile as? PsiJavaFile)?.importList?.accept(object : JavaRecursiveElementVisitor() {
override fun visitImportStatement(element: PsiImportStatement) {
super.visitImportStatement(element)
if (element.resolve() == null) {
@@ -227,6 +230,15 @@
"Syntax error: `${element.errorDescription}`"
)
}
+
+ override fun visitCodeBlock(block: PsiCodeBlock?) {
+ // Ignore to avoid eagerly parsing all method bodies.
+ }
+
+ override fun visitDocComment(comment: PsiDocComment?) {
+ // Ignore to avoid eagerly parsing all doc comments.
+ // Doc comments cannot contain error elements.
+ }
})
topLevelClassesFromSource += createClass(psiClass)
diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiItem.kt
index df9fd1a..0277c4f 100644
--- a/src/main/java/com/android/tools/metalava/model/psi/PsiItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/psi/PsiItem.kt
@@ -167,7 +167,7 @@
// API target (there are many), and each time would have involved constructing a full javadoc
// AST with lexical tokens using IntelliJ's javadoc parsing APIs. Instead, we'll just
// do some simple string heuristics.
- if (tagSection == "@apiSince" || tagSection == "@deprecatedSince") {
+ if (tagSection == "@apiSince" || tagSection == "@deprecatedSince" || tagSection == "@sdkExtSince") {
documentation = addUniqueTag(documentation, tagSection, comment)
return
}
diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt
index 167173f..52ce6b1 100644
--- a/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt
@@ -110,7 +110,7 @@
override fun isImplicitConstructor(): Boolean = false
- override fun returnType(): TypeItem? = returnType
+ override fun returnType(): TypeItem = returnType
override fun parameters(): List<PsiParameterItem> = parameters
@@ -316,7 +316,7 @@
}
val returnType = method.returnType()
- sb.append(returnType?.convertTypeString(replacementMap))
+ sb.append(returnType.convertTypeString(replacementMap))
sb.append(' ')
sb.append(method.name())
diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiTypePrinter.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiTypePrinter.kt
index 0b99b35..3c61a88 100644
--- a/src/main/java/com/android/tools/metalava/model/psi/PsiTypePrinter.kt
+++ b/src/main/java/com/android/tools/metalava/model/psi/PsiTypePrinter.kt
@@ -488,7 +488,7 @@
val mapped =
if (mapAnnotations) {
- AnnotationItem.mapName(codebase, qualifiedName) ?: return null
+ AnnotationItem.mapName(qualifiedName) ?: return null
} else {
qualifiedName
}
diff --git a/src/main/java/com/android/tools/metalava/model/psi/UAnnotationItem.kt b/src/main/java/com/android/tools/metalava/model/psi/UAnnotationItem.kt
index c76dfcf..45402e7 100644
--- a/src/main/java/com/android/tools/metalava/model/psi/UAnnotationItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/psi/UAnnotationItem.kt
@@ -51,7 +51,7 @@
val uAnnotation: UAnnotation,
override val originalName: String?
) : DefaultAnnotationItem(codebase) {
- override val qualifiedName: String? = AnnotationItem.mapName(codebase, originalName)
+ override val qualifiedName: String? = AnnotationItem.mapName(originalName)
override fun toString(): String = toSource()
@@ -121,7 +121,7 @@
target: AnnotationTarget,
showDefaultAttrs: Boolean
) {
- val qualifiedName = AnnotationItem.mapName(codebase, originalName, null, target) ?: return
+ val qualifiedName = AnnotationItem.mapName(originalName, target) ?: return
val attributes = getAttributes(uAnnotation, showDefaultAttrs)
if (attributes.isEmpty()) {
diff --git a/src/main/java/com/android/tools/metalava/model/text/ApiFile.kt b/src/main/java/com/android/tools/metalava/model/text/ApiFile.kt
index 5766605..df1bc61 100644
--- a/src/main/java/com/android/tools/metalava/model/text/ApiFile.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/ApiFile.kt
@@ -47,11 +47,7 @@
* Even if false, we'll allow them if the file format supports them/
*/
@Throws(ApiParseException::class)
- fun parseApi(@Nonnull file: File, kotlinStyleNulls: Boolean): TextCodebase {
- val files: MutableList<File> = ArrayList(1)
- files.add(file)
- return parseApi(files, kotlinStyleNulls)
- }
+ fun parseApi(@Nonnull file: File, kotlinStyleNulls: Boolean) = parseApi(listOf(file), kotlinStyleNulls)
/**
* Read API signature files into a [TextCodebase].
@@ -242,9 +238,7 @@
var isInterface = false
var isAnnotation = false
var isEnum = false
- val qualifiedName: String
var ext: String? = null
- val cl: TextClassItem
// Metalava: including annotations in file now
val annotations: List<String> = getAnnotations(tokenizer, token)
@@ -279,10 +273,7 @@
}
assertIdent(tokenizer, token)
val name: String = token
- qualifiedName = qualifiedName(pkg.name(), name)
- if (api.findClass(qualifiedName) != null) {
- throw ApiParseException("Duplicate class found: $qualifiedName", tokenizer)
- }
+ val qualifiedName = qualifiedName(pkg.name(), name)
val typeInfo = api.obtainTypeFromString(qualifiedName)
// Simple type info excludes the package name (but includes enclosing class names)
var rawName = name
@@ -291,11 +282,22 @@
rawName = rawName.substring(0, variableIndex)
}
token = tokenizer.requireToken()
- cl = TextClassItem(
+ val maybeExistingClass = TextClassItem(
api, tokenizer.pos(), modifiers, isInterface, isEnum, isAnnotation,
typeInfo.toErasedTypeString(null), typeInfo.qualifiedTypeName(),
rawName, annotations
)
+ val cl = when (val foundClass = api.findClass(maybeExistingClass.qualifiedName())) {
+ null -> maybeExistingClass
+ else -> {
+ if (!foundClass.isCompatible(maybeExistingClass)) {
+ throw ApiParseException("Incompatible $foundClass definitions")
+ } else {
+ foundClass
+ }
+ }
+ }
+
cl.setContainingPackage(pkg)
cl.setTypeInfo(typeInfo)
cl.deprecated = modifiers.isDeprecated()
@@ -446,12 +448,17 @@
) {
var token = startingToken
val method: TextConstructorItem
+ var typeParameterList = NONE
// Metalava: including annotations in file now
val annotations: List<String> = getAnnotations(tokenizer, token)
token = tokenizer.current
val modifiers = parseModifiers(api, tokenizer, token, annotations)
token = tokenizer.current
+ if ("<" == token) {
+ typeParameterList = parseTypeParameterList(api, tokenizer)
+ token = tokenizer.requireToken()
+ }
assertIdent(tokenizer, token)
val name: String = token.substring(token.lastIndexOf('.') + 1) // For inner classes, strip outer classes from name
token = tokenizer.requireToken()
@@ -461,6 +468,10 @@
method = TextConstructorItem(api, name, cl, modifiers, cl.asTypeInfo(), tokenizer.pos())
method.deprecated = modifiers.isDeprecated()
parseParameterList(api, tokenizer, method)
+ method.setTypeParameterList(typeParameterList)
+ if (typeParameterList is TextTypeParameterList) {
+ typeParameterList.owner = method
+ }
token = tokenizer.requireToken()
if ("throws" == token) {
token = parseThrows(tokenizer, method)
@@ -551,7 +562,9 @@
if (";" != token) {
throw ApiParseException("expected ; found $token", tokenizer)
}
- cl.addMethod(method)
+ if (!cl.methods().contains(method)) {
+ cl.addMethod(method)
+ }
}
private fun mergeAnnotations(
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextBackedAnnotationItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextBackedAnnotationItem.kt
index fce3a9d..b3bd1fc 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextBackedAnnotationItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextBackedAnnotationItem.kt
@@ -40,7 +40,7 @@
else source.substring(1, index)
originalName = annotationClass
- qualifiedName = if (mapName) AnnotationItem.mapName(codebase, annotationClass) else annotationClass
+ qualifiedName = if (mapName) AnnotationItem.mapName(annotationClass) else annotationClass
full = when {
qualifiedName == null -> ""
index == -1 -> "@$qualifiedName"
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt
index dffe723..7c3a121 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt
@@ -24,6 +24,7 @@
import com.android.tools.metalava.model.Item
import com.android.tools.metalava.model.MethodItem
import com.android.tools.metalava.model.PackageItem
+import com.android.tools.metalava.model.ParameterItem
import com.android.tools.metalava.model.PropertyItem
import com.android.tools.metalava.model.TypeItem
import com.android.tools.metalava.model.TypeParameterItem
@@ -225,6 +226,22 @@
innerClasses.add(cls)
}
+ fun isCompatible(cls: TextClassItem): Boolean {
+ if (this === cls) {
+ return true
+ }
+ if (fullName != cls.fullName) {
+ return false
+ }
+
+ return modifiers.toString() == cls.modifiers.toString() &&
+ isInterface == cls.isInterface &&
+ isEnum == cls.isEnum &&
+ isAnnotation == cls.isAnnotation &&
+ superClass == cls.superClass &&
+ allInterfaces().toSet() == cls.allInterfaces().toSet()
+ }
+
override fun filteredSuperClassType(predicate: Predicate<Item>): TypeItem? {
// No filtering in signature files: we assume signature APIs
// have already been filtered and all items should match.
@@ -260,6 +277,69 @@
return emptyMap()
}
+ override fun createDefaultConstructor(): ConstructorItem {
+ return TextConstructorItem.createDefaultConstructor(codebase, this, position)
+ }
+
+ fun containsMethodInClassContext(method: MethodItem): Boolean {
+ return methods.any { equalMethodInClassContext(it, method) }
+ }
+
+ fun getParentAndInterfaces(): List<TextClassItem> {
+ val classes = interfaceTypes().map { it.asClass() as TextClassItem }.toMutableList()
+ superClass()?.let { classes.add(0, it as TextClassItem) }
+ return classes
+ }
+
+ private var allSuperClassesAndInterfaces: List<TextClassItem>? = null
+
+ /** Returns all super classes and interfaces in the class hierarchy the class item inherits.
+ * The returned list is sorted by the proximity of the classes to the class item in the hierarchy chain.
+ * If an interface appears multiple time in the hierarchy chain,
+ * it is ordered based on the furthest distance to the class item.
+ */
+ fun getAllSuperClassesAndInterfaces(): List<TextClassItem> {
+ allSuperClassesAndInterfaces?.let { return it }
+
+ val classLevelMap = mutableMapOf<TextClassItem, Int>()
+
+ // Stores the parent class and interfaces to be iterated.
+ // Since a class can inherit multiple class and interfaces, queue is two-dimensional.
+ // Each inner lists represents all super class and interfaces in the same hierarchy level.
+ val queue = ArrayDeque<List<TextClassItem>>()
+ queue.add(getParentAndInterfaces())
+
+ // We need to visit the hierarchy starting from the greatest ancestor,
+ // but we cannot naively reverse-iterate based on the order the hierarchy is discovered
+ // because a class/interface can appear multiple times in the hierarchy graph
+ // (i.e. a vertex can have multiple outgoing edges).
+ // Thus, we keep track of the furthest distances from each hierarchy vertices to the
+ // destination vertex (cl) and reverse iterate from the vertices that are
+ // farthest from the destination.
+ var hierarchyLevel = 1
+ while (queue.isNotEmpty()) {
+ val superClasses = queue.removeFirst()
+ val parentClasses = ArrayList<TextClassItem>()
+ for (superClass in superClasses) {
+ // Every class extends java.lang.Object and thus not need to be
+ // included in the hierarchy
+ if (!superClass.isJavaLangObject()) {
+ classLevelMap[superClass] = hierarchyLevel
+ parentClasses.addAll(superClass.getParentAndInterfaces())
+ }
+ }
+ if (parentClasses.isNotEmpty()) {
+ queue.add(parentClasses)
+ }
+ hierarchyLevel += 1
+ }
+
+ allSuperClassesAndInterfaces =
+ classLevelMap.toList().sortedWith(compareBy { it.second }).map { it.first }
+
+ return allSuperClassesAndInterfaces!!
+ }
+
companion object {
fun createClassStub(codebase: TextCodebase, name: String): TextClassItem =
createStub(codebase, name, isInterface = false)
@@ -304,5 +384,215 @@
return qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1)
}
+
+ /**
+ * Determines whether if [thisClassType] is covariant type with [otherClassType].
+ * If [thisClassType] does not belong to this class, return false.
+ *
+ * @param thisClass [ClassItem] that [thisClassType] belongs to
+ * @param thisClassType [TypeItem] that belongs to this class
+ * @param otherClassType [TypeItem] that belongs to other class
+ * @return Boolean that indicates whether if the two types are covariant
+ */
+ private fun isCovariantType(
+ thisClass: ClassItem,
+ thisClassType: TypeItem,
+ otherClassType: TypeItem
+ ): Boolean {
+ // TypeItem.asClass() returns null for primitive types.
+ // Since primitive types are not covariant with anything, return false
+ val otherClass = otherClassType.asClass() ?: return false
+
+ val otherSuperClassNames = (otherClass as TextClassItem)
+ .getAllSuperClassesAndInterfaces().map { it.qualifiedName() }
+
+ val thisClassTypeErased = thisClassType.toErasedTypeString()
+ val typeArgIndex = thisClass.toType().typeArguments(simplified = true).indexOf(thisClassTypeErased)
+
+ // thisClassSuperType is the super type of thisClassType retrieved from the type arguments.
+ // e.g. when type arguments are <K, V extends some.arbitrary.Class>,
+ // thisClassSuperType will be "some.arbitrary.Class" when the thisClassType is "V"
+ // If thisClassType is not included in the type arguments or
+ // if thisClassType does not have a super type specified in the type argument,
+ // thisClassSuperType will be thisClassType.
+ val thisClassSuperType = if (typeArgIndex == -1) thisClassTypeErased else
+ thisClass.toType().typeArguments()[typeArgIndex].substringAfterLast(" extends ")
+
+ return thisClassSuperType in otherSuperClassNames
+ }
+
+ private fun hasEqualTypeVar(
+ type1: TypeItem,
+ class1: ClassItem,
+ type2: TypeItem,
+ class2: ClassItem
+ ): Boolean {
+
+ // Given a type and its containing class,
+ // find the interface types that contains the type.
+ // For instance, for a method that looks like:
+ // class SomeClass implements InterfaceA<some.return.Type>, InterfaceB<some.return.Type>
+ // Type foo()
+ // this function will return [InterfaceA, InterfaceB] when Type and SomeClass
+ // are passed as inputs.
+ val typeContainingInterfaces = {
+ t: TypeItem, cl: ClassItem ->
+ val interfaceTypes = cl.interfaceTypes()
+ .plus(cl.toType())
+ .plus(cl.superClassType())
+ .filterNotNull()
+ interfaceTypes.filter {
+ val typeArgs = it.typeArguments(simplified = true)
+ t.toString() in typeArgs ||
+ t.toElementType() in typeArgs ||
+ t.asClass()?.superClass()?.qualifiedName() in typeArgs
+ }
+ }
+
+ val typeContainingInterfaces1 = typeContainingInterfaces(type1, class1)
+ val typeContainingInterfaces2 = typeContainingInterfaces(type2, class2)
+
+ if (typeContainingInterfaces1.isEmpty() || typeContainingInterfaces2.isEmpty()) {
+ return false
+ }
+
+ val interfaceTypesAreCovariant = {
+ t1: TypeItem, t2: TypeItem ->
+ t1.toErasedTypeString() == t2.toErasedTypeString() ||
+ t1.asClass()?.superClass()?.qualifiedName() == t2.asClass()?.qualifiedName() ||
+ t2.asClass()?.superClass()?.qualifiedName() == t1.asClass()?.qualifiedName()
+ }
+
+ // Check if the return type containing interfaces of the two methods have an intersection.
+ return typeContainingInterfaces1.any {
+ typeInterface1 ->
+ typeContainingInterfaces2.any {
+ typeInterface2 ->
+ interfaceTypesAreCovariant(typeInterface1, typeInterface2)
+ }
+ }
+ }
+
+ private fun hasEqualTypeBounds(method1: MethodItem, method2: MethodItem): Boolean {
+ val typeInTypeParams = {
+ t: TypeItem, m: MethodItem ->
+ t in m.typeParameterList().typeParameters().map { it.toType() }
+ }
+
+ val getTypeBounds = {
+ t: TypeItem, m: MethodItem ->
+ m.typeParameterList().typeParameters().single { it.toType() == t }.typeBounds().toSet()
+ }
+
+ val returnType1 = method1.returnType()
+ val returnType2 = method2.returnType()
+
+ // The following two methods are considered equal:
+ // method public <A extends some.package.SomeClass> A foo (Class<A>);
+ // method public <T extends some.package.SomeClass> T foo (Class<T>);
+ // This can be verified by converting return types to bounds ([some.package.SomeClass])
+ // and compare equivalence.
+ return typeInTypeParams(returnType1, method1) && typeInTypeParams(returnType2, method2) &&
+ getTypeBounds(returnType1, method1) == getTypeBounds(returnType2, method2)
+ }
+
+ private fun hasCovariantTypes(
+ type1: TypeItem,
+ class1: ClassItem,
+ type2: TypeItem,
+ class2: ClassItem
+ ): Boolean {
+ val types = listOf(type1, type2)
+
+ val type1Erased = type1.toErasedTypeString()
+ val type2Erased = type2.toErasedTypeString()
+
+ // The return type of the following two methods are considered equal:
+ // when SomeReturnSubClass extends SomeReturnClass:
+ // method SomeReturnClass foo() and method SomeReturnSubClass foo()
+ // Likewise, the return type of the two methods are also considered equal:
+ // method T foo() in SomeClass<T extends SomeReturnClass> and
+ // method SomeReturnSubClass foo() in SomeOtherClass
+ // This can be verified by checking if a method's return type exists in
+ // another method return type's super classes
+ // Since this method is only used to compare methods with same name and parameters count
+ // within same hierarchy tree, it is unlikely that
+ // two methods have same generic return type.
+ // However, not comparing erased type strings may lead to false negatives
+ // (e.g. comparing java.util.Iterator<E> and java.util.Iterator<T>)
+ // Thus erased type strings equivalence must be evaluated.
+ return type1Erased == type2Erased ||
+ isCovariantType(class1, type1, type2) ||
+ isCovariantType(class2, type2, type1) ||
+ types.any { it.isJavaLangObject() }
+ }
+
+ /**
+ * Compares two [MethodItem]s and determines if the two methods have equal return types.
+ * The two methods' return types are considered equal even if the two are not identical,
+ * but are compatible in compiler level. For instance, return types in a same hierarchy tree
+ * are considered equal.
+ *
+ * @param method1 first [MethodItem] to compare the return type
+ * @param method2 second [MethodItem] to compare the return type
+ * @return a [Boolean] value representing if the two methods' return types are equal
+ */
+ fun hasEqualReturnType(method1: MethodItem, method2: MethodItem): Boolean {
+ val returnType1 = method1.returnType()
+ val returnType2 = method2.returnType()
+ val class1 = method1.containingClass()
+ val class2 = method2.containingClass()
+
+ if (returnType1 == returnType2) return true
+
+ if (hasEqualTypeVar(returnType1, class1, returnType2, class2)) return true
+
+ if (hasEqualTypeBounds(method1, method2)) return true
+
+ if (hasCovariantTypes(returnType1, class1, returnType2, class2)) return true
+
+ return false
+ }
+
+ /**
+ * Compares two [MethodItem] and determines if the two are considered equal based on
+ * the context of the containing classes of the methods. To be specific, for the two methods
+ * in which the coexistence in a class would lead to a
+ * method already defined compiler error, this method returns true.
+ *
+ * @param method1 first [MethodItem] to compare
+ * @param method2 second [MethodItem] to compare
+ * @return a [Boolean] value representing if the two methods are equal or not
+ * with respect to the classes contexts.
+ */
+ fun equalMethodInClassContext(method1: MethodItem, method2: MethodItem): Boolean {
+ if (method1 == method2) return true
+
+ if (method1.name() != method2.name()) return false
+ if (method1.parameters().size != method2.parameters().size) return false
+
+ val hasEqualParams = method1.parameters().zip(method2.parameters()).all {
+ (param1, param2): Pair<ParameterItem, ParameterItem> ->
+ val type1 = param1.type()
+ val type2 = param2.type()
+ val class1 = method1.containingClass()
+ val class2 = method2.containingClass()
+
+ // At this point, two methods' return types equivalence would have been checked.
+ // i.e. If hasEqualReturnType(method1, method2) is true,
+ // we know that the two methods return types are equal.
+ // In other words, if the two compared param types are both method return types,
+ // we transitively know that the two param types are equal as well.
+ val bothAreMethodReturnType =
+ type1 == method1.returnType() && type2 == method2.returnType()
+
+ type1 == type2 ||
+ bothAreMethodReturnType ||
+ hasEqualTypeVar(type1, class1, type2, class2) ||
+ hasCovariantTypes(type1, class1, type2, class2)
+ }
+
+ return hasEqualReturnType(method1, method2) && hasEqualParams
+ }
}
}
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextCodebase.kt b/src/main/java/com/android/tools/metalava/model/text/TextCodebase.kt
index 815d688..384f8f3 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextCodebase.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextCodebase.kt
@@ -39,6 +39,7 @@
import com.android.tools.metalava.model.TypeParameterList
import com.android.tools.metalava.model.visitors.ItemVisitor
import com.android.tools.metalava.model.visitors.TypeVisitor
+import com.android.tools.metalava.options
import java.io.File
import java.util.ArrayList
import java.util.HashMap
@@ -104,7 +105,9 @@
if (!mClassToInterface.containsKey(classInfo)) {
mClassToInterface[classInfo] = ArrayList()
}
- mClassToInterface[classInfo]?.add(iface)
+ mClassToInterface[classInfo]?.let {
+ if (!it.contains(iface)) it.add(iface)
+ }
}
fun implementsInterface(classInfo: TextClassItem, iface: String): Boolean {
@@ -206,6 +209,89 @@
}
}
+ /**
+ * Add abstract superclass abstract methods to non-abstract class
+ * when generating from-text stubs.
+ * Iterate through the hierarchy and collect all super abstract methods that need to be added.
+ * These are not included in the signature files but omitting these methods
+ * will lead to compile error.
+ */
+ private fun resolveAbstractMethods(allClasses: List<TextClassItem>) {
+ for (cl in allClasses) {
+
+ // If class is interface, naively iterate through all parent class and interfaces
+ // and resolve inheritance of override equivalent signatures
+ // Find intersection of super class/interface default methods
+ // Resolve conflict by adding signature
+ // https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.4.1.3
+ if (cl.isInterface()) {
+
+ // We only need to track one method item(value) with the signature(key),
+ // since the containing class does not matter if a method to be added is found
+ // as method.duplicate(cl) sets containing class to cl.
+ // Therefore, the value of methodMap can be overwritten.
+ val methodMap = mutableMapOf<String, TextMethodItem>()
+ val methodCount = mutableMapOf<String, Int>()
+ val hasDefault = mutableMapOf<String, Boolean>()
+ for (superInterfaceOrClass in cl.getParentAndInterfaces()) {
+ val methods = superInterfaceOrClass.methods().map { it as TextMethodItem }
+ for (method in methods) {
+ val signature = method.toSignatureString()
+ val isDefault = method.modifiers.isDefault()
+ val newCount = methodCount.getOrDefault(signature, 0) + 1
+ val newHasDefault = hasDefault.getOrDefault(signature, false) || isDefault
+
+ methodMap[signature] = method
+ methodCount[signature] = newCount
+ hasDefault[signature] = newHasDefault
+
+ // If the method has appeared more than once, there may be a potential conflict
+ // thus add the method to the interface
+ if (newHasDefault && newCount == 2 &&
+ !cl.containsMethodInClassContext(method)
+ ) {
+ val m = method.duplicate(cl) as TextMethodItem
+ m.modifiers.setAbstract(true)
+ m.modifiers.setDefault(false)
+ cl.addMethod(m)
+ }
+ }
+ }
+ }
+
+ // If class is a concrete class, iterate through all hierarchy and
+ // find all missing abstract methods.
+ // Only add methods that are not implemented in the hierarchy and not included
+ else if (!cl.isAbstractClass() && !cl.isEnum()) {
+ val superMethodsToBeOverridden = mutableListOf<TextMethodItem>()
+ val hierarchyClassesList = cl.getAllSuperClassesAndInterfaces().toMutableList()
+ while (hierarchyClassesList.isNotEmpty()) {
+ val ancestorClass = hierarchyClassesList.removeLast()
+ val abstractMethods = ancestorClass.methods().filter { it.modifiers.isAbstract() }
+ for (method in abstractMethods) {
+ // We do not compare this against all ancestors of cl,
+ // because an abstract method cannot be overridden at its ancestor class.
+ // Thus, we compare against hierarchyClassesList.
+ if (hierarchyClassesList.all { !it.containsMethodInClassContext(method) } &&
+ !cl.containsMethodInClassContext(method)
+ ) {
+ superMethodsToBeOverridden.add(method as TextMethodItem)
+ }
+ }
+ }
+ for (superMethod in superMethodsToBeOverridden) {
+ // MethodItem.duplicate() sets the containing class of
+ // the duplicated method item as the input parameter.
+ // Thus, the method items to be overridden are duplicated here after the
+ // ancestor classes iteration so that the method items are correctly compared.
+ val m = superMethod.duplicate(cl) as TextMethodItem
+ m.modifiers.setAbstract(false)
+ cl.addMethod(m)
+ }
+ }
+ }
+ }
+
fun registerClass(cls: TextClassItem) {
mAllClasses[cls.qualifiedName] = cls
}
@@ -261,6 +347,12 @@
resolveInterfaces(classes)
resolveThrowsClasses(classes)
resolveInnerClasses(packages)
+
+ // Add overridden methods to the codebase only when the codebase is generated
+ // from text file passed via --source-files
+ if (this.location in options.sources) {
+ resolveAbstractMethods(classes)
+ }
}
override fun findPackage(pkgName: String): TextPackageItem? {
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt
index d854695..4577801 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt
@@ -17,13 +17,14 @@
package com.android.tools.metalava.model.text
import com.android.tools.metalava.model.ConstructorItem
+import com.android.tools.metalava.model.DefaultModifierList
class TextConstructorItem(
codebase: TextCodebase,
name: String,
containingClass: TextClassItem,
modifiers: TextModifiers,
- returnType: TextTypeItem?,
+ returnType: TextTypeItem,
position: SourcePositionInfo
) : TextMethodItem(codebase, name, containingClass, modifiers, returnType, position),
ConstructorItem {
@@ -31,4 +32,26 @@
override var superConstructor: ConstructorItem? = null
override fun isConstructor(): Boolean = true
+
+ companion object {
+ fun createDefaultConstructor(
+ codebase: TextCodebase,
+ containingClass: TextClassItem,
+ position: SourcePositionInfo,
+ ): TextConstructorItem {
+ val name = containingClass.name
+ val modifiers = TextModifiers(codebase, DefaultModifierList.PACKAGE_PRIVATE, null)
+
+ val item = TextConstructorItem(
+ codebase = codebase,
+ name = name,
+ containingClass = containingClass,
+ modifiers = modifiers,
+ returnType = containingClass.asTypeInfo(),
+ position = position,
+ )
+ modifiers.setOwner(item)
+ return item
+ }
+ }
}
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt
index 9dba79a..fcd4063 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt
@@ -31,7 +31,7 @@
name: String,
containingClass: TextClassItem,
modifiers: TextModifiers,
- private val returnType: TextTypeItem?,
+ private val returnType: TextTypeItem,
position: SourcePositionInfo
) : TextMemberItem(
codebase, name, containingClass, position,
@@ -79,7 +79,7 @@
override fun isConstructor(): Boolean = false
- override fun returnType(): TypeItem? = returnType
+ override fun returnType(): TypeItem = returnType
override fun superMethods(): List<MethodItem> {
if (isConstructor()) {
@@ -209,9 +209,10 @@
override var inheritedFrom: ClassItem? = null
override fun toString(): String =
- "${if (isConstructor()) "constructor" else "method"} ${containingClass().qualifiedName()}.${name()}(${parameters().joinToString {
- it.type().toSimpleType()
- }})"
+ "${if (isConstructor()) "constructor" else "method"} ${containingClass().qualifiedName()}.${toSignatureString()}"
+
+ fun toSignatureString(): String =
+ "${name()}(${parameters().joinToString { it.type().toSimpleType() }})"
private var annotationDefault = ""
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextModifiers.kt b/src/main/java/com/android/tools/metalava/model/text/TextModifiers.kt
index 2c72a0d..143ffcd 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextModifiers.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextModifiers.kt
@@ -53,7 +53,7 @@
annotationSources.forEach { source ->
val index = source.indexOf('(')
val originalName = if (index == -1) source.substring(1) else source.substring(1, index)
- val qualifiedName = AnnotationItem.mapName(codebase, originalName)
+ val qualifiedName = AnnotationItem.mapName(originalName)
// @Deprecated is also treated as a "modifier"
if (qualifiedName == JAVA_LANG_DEPRECATED) {
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt
index d3bfc4d..7b126c3 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextPackageItem.kt
@@ -31,10 +31,17 @@
private val classes = ArrayList<TextClassItem>(100)
+ private val classesNames = HashSet<String>(100)
+
fun name() = name
fun addClass(classInfo: TextClassItem) {
+ val classFullName = classInfo.fullName()
+ if (classFullName in classesNames) {
+ return
+ }
classes.add(classInfo)
+ classesNames.add(classFullName)
}
internal fun pruneClassList() {
diff --git a/src/main/java/com/android/tools/metalava/stub/JavaStubWriter.kt b/src/main/java/com/android/tools/metalava/stub/JavaStubWriter.kt
index 08d6d32..41e77ed 100644
--- a/src/main/java/com/android/tools/metalava/stub/JavaStubWriter.kt
+++ b/src/main/java/com/android/tools/metalava/stub/JavaStubWriter.kt
@@ -26,7 +26,6 @@
import com.android.tools.metalava.model.ModifierList
import com.android.tools.metalava.model.PackageItem
import com.android.tools.metalava.model.TypeParameterList
-import com.android.tools.metalava.model.psi.PsiClassItem
import com.android.tools.metalava.model.visitors.ItemVisitor
import com.android.tools.metalava.options
import java.io.PrintWriter
@@ -192,7 +191,6 @@
return
}
}
- (cls as PsiClassItem).psiClass.superClassType
writer.print(qualifiedName)
}
}
@@ -355,7 +353,7 @@
val returnType = method.returnType()
writer.print(
- returnType?.toTypeString(
+ returnType.toTypeString(
outerAnnotations = false,
innerAnnotations = generateAnnotations,
filter = filterReference
diff --git a/src/main/java/com/android/tools/metalava/stub/StubWriter.kt b/src/main/java/com/android/tools/metalava/stub/StubWriter.kt
index e11d8ef..2f27823 100644
--- a/src/main/java/com/android/tools/metalava/stub/StubWriter.kt
+++ b/src/main/java/com/android/tools/metalava/stub/StubWriter.kt
@@ -116,7 +116,9 @@
private fun writePackageInfo(pkg: PackageItem) {
val annotations = pkg.modifiers.annotations()
- if (annotations.isNotEmpty() && generateAnnotations || !pkg.documentation.isBlank()) {
+ val writeAnnotations = annotations.isNotEmpty() && generateAnnotations
+ val writeDocumentation = docStubs && pkg.documentation.isNotBlank()
+ if (writeAnnotations || writeDocumentation) {
val sourceFile = File(getPackageDir(pkg), "package-info.java")
val packageInfoWriter = try {
PrintWriter(BufferedWriter(FileWriter(sourceFile)))
@@ -163,7 +165,10 @@
assert(classItem.containingClass() == null) { "Should only be called on top level classes" }
val packageDir = getPackageDir(classItem.containingPackage())
- return if (classItem.isKotlin() && options.kotlinStubs) {
+ // Kotlin From-text stub generation is not supported.
+ // This method will raise an error if
+ // options.kotlinStubs == true and classItem is TextClassItem.
+ return if (options.kotlinStubs && classItem.isKotlin()) {
File(packageDir, "${classItem.simpleName()}.kt")
} else {
File(packageDir, "${classItem.simpleName()}.java")
diff --git a/src/test/java/com/android/tools/metalava/ApiFileTest.kt b/src/test/java/com/android/tools/metalava/ApiFileTest.kt
index 31aae84..40e0ee7 100644
--- a/src/test/java/com/android/tools/metalava/ApiFileTest.kt
+++ b/src/test/java/com/android/tools/metalava/ApiFileTest.kt
@@ -2289,6 +2289,33 @@
}
@Test
+ fun `Check generic type signature insertion`() {
+ check(
+ format = FileFormat.V2,
+ sourceFiles = arrayOf(
+ java(
+ """
+ package test.pkg;
+ public class MyClass {
+ public <T> MyClass(Class<T> klass) { }
+ public <U> void method1(Function<U> func) { }
+ }
+ """
+ )
+ ),
+ expectedIssues = "",
+ api = """
+ package test.pkg {
+ public class MyClass {
+ ctor public <T> MyClass(Class<T>);
+ method public <U> void method1(Function<U>);
+ }
+ }
+ """
+ )
+ }
+
+ @Test
fun `When implementing rather than extending package private class, inline members instead`() {
// If you implement a package private interface, we just remove it and inline the members into
// the subclass
@@ -3777,7 +3804,7 @@
}
@Test
- fun `Test cannot merging API signature files with duplicate class`() {
+ fun `Test can merge API signature files with duplicate class`() {
val source1 = """
package Test.pkg {
public final class Class1 {
@@ -3792,9 +3819,79 @@
}
}
"""
+ val expected = """
+ package Test.pkg {
+ public final class Class1 {
+ method public void method1();
+ }
+ }
+ """
check(
signatureSources = arrayOf(source1, source2),
- expectedFail = "Aborting: Unable to parse signature file: TESTROOT/project/load-api2.txt:2: Duplicate class found: Test.pkg.Class1"
+ api = expected
+ )
+ }
+
+ @Test
+ fun `Test can merge API signature files with generic type classes`() {
+ val source1 = """
+ package Test.pkg {
+ public class LinkedHashMap<K, V> extends java.util.HashMap<K,V> implements java.util.Map<K,V> {
+ ctor public LinkedHashMap(int, float);
+ ctor public LinkedHashMap(int);
+ ctor public LinkedHashMap();
+ ctor public LinkedHashMap(java.util.Map<? extends K,? extends V>);
+ ctor public LinkedHashMap(int, float, boolean);
+ method protected boolean removeEldestEntry(java.util.Map.Entry<K,V>);
+ }
+ }
+ """
+ val source2 = """
+ package Test.pkg {
+ public class LinkedHashMap<K, V> extends java.util.HashMap<K,V> implements java.util.Map<K,V> {
+ method public java.util.Map.Entry<K,V> eldest();
+ }
+ }
+ """
+ val expected = """
+ package Test.pkg {
+ public class LinkedHashMap<K, V> extends java.util.HashMap<K,V> implements java.util.Map<K,V> {
+ ctor public LinkedHashMap(int, float);
+ ctor public LinkedHashMap(int);
+ ctor public LinkedHashMap();
+ ctor public LinkedHashMap(java.util.Map<? extends K,? extends V>);
+ ctor public LinkedHashMap(int, float, boolean);
+ method public java.util.Map.Entry<K,V> eldest();
+ method protected boolean removeEldestEntry(java.util.Map.Entry<K,V>);
+ }
+ }
+ """
+ check(
+ signatureSources = arrayOf(source1, source2),
+ api = expected,
+ format = FileFormat.V2,
+ )
+ }
+
+ @Test
+ fun `Test cannot merge API signature files with incompatible class definitions`() {
+ val source1 = """
+ package Test.pkg {
+ public class Class1 {
+ method public void method2();
+ }
+ }
+ """
+ val source2 = """
+ package Test.pkg {
+ public final class Class1 {
+ method public void method1();
+ }
+ }
+ """
+ check(
+ signatureSources = arrayOf(source1, source2),
+ expectedFail = "Aborting: Unable to parse signature file: Incompatible class Test.pkg.Class1 definitions"
)
}
diff --git a/src/test/java/com/android/tools/metalava/ApiLintTest.kt b/src/test/java/com/android/tools/metalava/ApiLintTest.kt
index 2ac7c0a..8eec509 100644
--- a/src/test/java/com/android/tools/metalava/ApiLintTest.kt
+++ b/src/test/java/com/android/tools/metalava/ApiLintTest.kt
@@ -274,6 +274,10 @@
@Test
fun `Test callbacks`() {
check(
+ extraArguments = arrayOf(
+ ARG_ERROR,
+ "CallbackInterface"
+ ),
apiLint = "", // enabled
expectedIssues = """
src/android/pkg/MyCallback.java:9: error: Callback method names must follow the on<Something> style: bar [CallbackMethodName] [See https://s.android.com/api-guidelines#callback-method-naming]
@@ -2274,6 +2278,13 @@
@NonNull
public UnitNameTestBuilder setOkPercentage(int i) { return this; }
}
+
+ @NamedDataSpace
+ public int getOkDataSpace() { return 0; }
+
+ /** @hide */
+ @android.annotation.IntDef
+ public @interface NamedDataSpace {}
}
"""
)
@@ -2286,8 +2297,8 @@
check(
apiLint = "", // enabled
expectedIssues = """
- src/android/pkg/MyErrorClass1.java:3: warning: Classes that release resources (close()) should implement AutoClosable and CloseGuard: class android.pkg.MyErrorClass1 [NotCloseable]
- src/android/pkg/MyErrorClass2.java:3: warning: Classes that release resources (finalize(), shutdown()) should implement AutoClosable and CloseGuard: class android.pkg.MyErrorClass2 [NotCloseable]
+ src/android/pkg/MyErrorClass1.java:3: warning: Classes that release resources (close()) should implement AutoCloseable and CloseGuard: class android.pkg.MyErrorClass1 [NotCloseable]
+ src/android/pkg/MyErrorClass2.java:3: warning: Classes that release resources (finalize(), shutdown()) should implement AutoCloseable and CloseGuard: class android.pkg.MyErrorClass2 [NotCloseable]
""",
sourceFiles = arrayOf(
java(
@@ -2346,8 +2357,8 @@
check(
apiLint = "", // enabled
expectedIssues = """
- src/android/pkg/MyErrorClass1.java:3: warning: Classes that release resources (close()) should implement AutoClosable and CloseGuard: class android.pkg.MyErrorClass1 [NotCloseable]
- src/android/pkg/MyErrorClass2.java:3: warning: Classes that release resources (finalize(), shutdown()) should implement AutoClosable and CloseGuard: class android.pkg.MyErrorClass2 [NotCloseable]
+ src/android/pkg/MyErrorClass1.java:3: warning: Classes that release resources (close()) should implement AutoCloseable and CloseGuard: class android.pkg.MyErrorClass1 [NotCloseable]
+ src/android/pkg/MyErrorClass2.java:3: warning: Classes that release resources (finalize(), shutdown()) should implement AutoCloseable and CloseGuard: class android.pkg.MyErrorClass2 [NotCloseable]
""",
manifest = """<?xml version="1.0" encoding="UTF-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
diff --git a/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt
index 1dc846b..4a5c6d1 100644
--- a/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt
+++ b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt
@@ -1200,6 +1200,7 @@
check(
expectedIssues = """
src/test/pkg/Outer.java:7: error: Method test.pkg.Outer.Class1.method1 has added 'final' qualifier [AddedFinal]
+ src/test/pkg/Outer.java:19: error: Method test.pkg.Outer.Class4.method4 has removed 'final' qualifier [RemovedFinal]
""",
checkCompatibilityApiReleased = """
package test.pkg {
@@ -1776,6 +1777,94 @@
}
@Test
+ fun `Incompatible Changes in Released System API `() {
+ // Incompatible changes to a released System API should be detected
+ // In this case removing final and changing value of constant
+ check(
+ includeSystemApiAnnotations = true,
+ expectedIssues = """
+ src/android/rolecontrollerservice/RoleControllerService.java:8: error: Method android.rolecontrollerservice.RoleControllerService.sendNetworkScore has removed 'final' qualifier [RemovedFinal]
+ src/android/rolecontrollerservice/RoleControllerService.java:9: error: Field android.rolecontrollerservice.RoleControllerService.APP_RETURN_UNWANTED has changed value from 1 to 0 [ChangedValue]
+ """,
+ sourceFiles = arrayOf(
+ java(
+ """
+ package android.rolecontrollerservice;
+ import android.annotation.SystemApi;
+
+ /** @hide */
+ @SystemApi
+ public abstract class RoleControllerService {
+ public abstract void onGrantDefaultRoles();
+ public void sendNetworkScore();
+ public static final int APP_RETURN_UNWANTED = 0;
+ }
+ """
+ ),
+ systemApiSource
+ ),
+
+ extraArguments = arrayOf(
+ ARG_SHOW_ANNOTATION, "android.annotation.TestApi",
+ ARG_HIDE_PACKAGE, "android.annotation",
+ ),
+
+ checkCompatibilityApiReleased =
+ """
+ package android.rolecontrollerservice {
+ public abstract class RoleControllerService {
+ ctor public RoleControllerService();
+ method public abstract void onGrantDefaultRoles();
+ method public final void sendNetworkScore();
+ field public static final int APP_RETURN_UNWANTED = 1;
+ }
+ }
+ """
+ )
+ }
+
+ @Test
+ fun `Incompatible changes to released API signature codebase`() {
+ // Incompatible changes to a released System API should be detected
+ // in case of partial files
+ check(
+ expectedIssues = """
+ TESTROOT/released-api.txt:5: error: Removed method test.pkg.Foo.method2() [RemovedMethod]
+ """,
+ signatureSource = """
+ // Signature format: 3.0
+ package test.pkg {
+ public final class Foo {
+ ctor public Foo();
+ method public void method1();
+ }
+ }
+ """,
+
+ checkCompatibilityApiReleased =
+ """
+ package test.pkg {
+ public final class Foo {
+ ctor public Foo();
+ method public void method1();
+ method public void method2();
+ method public void method3();
+ }
+ }
+ """,
+ checkCompatibilityBaseApi =
+ """
+ package test.pkg {
+ public final class Foo {
+ ctor public Foo();
+ method public void method3();
+ }
+ }
+ """,
+ )
+ }
+
+ @Test
fun `Partial text file which adds methods to show-annotation API`() {
// This happens in system and test files where we only include APIs that differ
// from the base IDE. When parsing these code bases we need to gracefully handle
@@ -3266,7 +3355,7 @@
// Regression test for 130567941
check(
expectedIssues = """
- TESTROOT/load-api.txt:7: error: Method test.pkg.sample.SampleClass.convert has changed return type from Number to java.lang.Number [ChangedType]
+ TESTROOT/load-api.txt:7: error: Method test.pkg.sample.SampleClass.convert1 has changed return type from Number to java.lang.Number [ChangedType]
""",
inputKotlinStyleNulls = true,
outputKotlinStyleNulls = true,
@@ -3275,7 +3364,7 @@
package test.pkg.sample {
public abstract class SampleClass {
method public <Number> Number! convert(Number);
- method public <Number> Number! convert(Number);
+ method public <Number> Number! convert1(Number);
}
}
""",
@@ -3286,7 +3375,7 @@
// Here the generic type parameter applies to both the function argument and the function return type
method public <Number> Number! convert(Number);
// Here the generic type parameter applies to the function argument but not the function return type
- method public <Number> java.lang.Number! convert(Number);
+ method public <Number> java.lang.Number! convert1(Number);
}
}
"""
diff --git a/src/test/java/com/android/tools/metalava/CoreApiTest.kt b/src/test/java/com/android/tools/metalava/CoreApiTest.kt
index b3429be..6147db7 100644
--- a/src/test/java/com/android/tools/metalava/CoreApiTest.kt
+++ b/src/test/java/com/android/tools/metalava/CoreApiTest.kt
@@ -209,7 +209,8 @@
extraArguments = arrayOf(
ARG_SHOW_SINGLE_ANNOTATION, "libcore.api.IntraCoreApi",
ARG_HIDE_ANNOTATION, "libcore.api.LibCoreHidden"
- )
+ ),
+ docStubs = true
)
}
diff --git a/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt
index de91122..71d52a4 100644
--- a/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt
+++ b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt
@@ -238,34 +238,27 @@
/**
* Requires {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}
*/
- @androidx.annotation.RequiresPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
public void test1() { throw new RuntimeException("Stub!"); }
/**
* Requires {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}
*/
- @androidx.annotation.RequiresPermission(allOf=android.Manifest.permission.ACCESS_COARSE_LOCATION)
public void test2() { throw new RuntimeException("Stub!"); }
/**
* Requires {@link android.Manifest.permission#ACCESS_COARSE_LOCATION} or {@link android.Manifest.permission#ACCESS_FINE_LOCATION}
*/
- @androidx.annotation.RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION})
public void test3() { throw new RuntimeException("Stub!"); }
/**
* Requires {@link android.Manifest.permission#ACCESS_COARSE_LOCATION} and {@link android.Manifest.permission#ACCOUNT_MANAGER}
*/
- @androidx.annotation.RequiresPermission(allOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCOUNT_MANAGER})
public void test4() { throw new RuntimeException("Stub!"); }
- @androidx.annotation.RequiresPermission(value=android.Manifest.permission.WATCH_APPOPS, conditional=true)
public void test5() { throw new RuntimeException("Stub!"); }
/**
* Requires {@link android.Manifest.permission#ACCESS_COARSE_LOCATION} or {@link android.telephony.TelephonyManager#hasCarrierPrivileges carrier privileges}
*/
- @androidx.annotation.RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, "carrier privileges"})
public void test6() { throw new RuntimeException("Stub!"); }
/**
* Requires {@link android.Manifest.permission#ACCESS_COARSE_LOCATION} or "carier priviliges"
*/
- @androidx.annotation.RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, "carier priviliges"})
public void test6() { throw new RuntimeException("Stub!"); }
}
"""
@@ -315,7 +308,6 @@
@SuppressWarnings({"unchecked", "deprecation", "all"})
public class PermissionTest {
public PermissionTest() { throw new RuntimeException("Stub!"); }
- @androidx.annotation.RequiresPermission(value=android.Manifest.permission.WATCH_APPOPS, conditional=true)
public void test1() { throw new RuntimeException("Stub!"); }
}
"""
@@ -361,17 +353,14 @@
* @param range2 Value is 20 or greater
* @return Value is 10 or greater
*/
- @androidx.annotation.IntRange(from=10)
- public int test1(@androidx.annotation.IntRange(from=20) int range2) { throw new RuntimeException("Stub!"); }
+ public int test1(int range2) { throw new RuntimeException("Stub!"); }
/**
* @return Value is between 10 and 20 inclusive
*/
- @androidx.annotation.IntRange(from=10, to=20)
public int test2() { throw new RuntimeException("Stub!"); }
/**
* @return Value is 100 or less
*/
- @androidx.annotation.IntRange(to=100)
public int test3() { throw new RuntimeException("Stub!"); }
}
"""
@@ -411,14 +400,12 @@
* main thread of your app. *
*/
@SuppressWarnings({"unchecked", "deprecation", "all"})
- @androidx.annotation.UiThread
public class RangeTest {
public RangeTest() { throw new RuntimeException("Stub!"); }
/**
* This method may take several seconds to complete, so it should
* only be called from a worker thread.
*/
- @androidx.annotation.WorkerThread
public int test1() { throw new RuntimeException("Stub!"); }
}
"""
@@ -462,8 +449,6 @@
* This method may take several seconds to complete, so it should
* only be called from a worker thread.
*/
- @androidx.annotation.UiThread
- @androidx.annotation.WorkerThread
public int test1() { throw new RuntimeException("Stub!"); }
}
"""
@@ -536,7 +521,6 @@
* @return blah blah blah
* @apiSince 24
*/
- @androidx.annotation.UiThread
public int getCurrentContentInsetEnd() { throw new RuntimeException("Stub!"); }
/**
* <br>
@@ -544,7 +528,6 @@
* this UI element. This is typically the main thread of your app.
* @apiSince 15
*/
- @androidx.annotation.UiThread
public int getCurrentContentInsetRight() { throw new RuntimeException("Stub!"); }
}
"""
@@ -712,7 +695,6 @@
/**
* Requires {@link test.pkg.RangeTest#ACCESS_COARSE_LOCATION}
*/
- @androidx.annotation.RequiresPermission(test.pkg.RangeTest.ACCESS_COARSE_LOCATION)
public void test1() { throw new RuntimeException("Stub!"); }
public static final java.lang.String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION";
}
@@ -752,7 +734,6 @@
/**
* Requires "MyPermission"
*/
- @androidx.annotation.RequiresPermission("MyPermission")
public void test1() { throw new RuntimeException("Stub!"); }
}
"""
@@ -794,7 +775,6 @@
* <br>
* Requires {@link test.pkg.RangeTest#ACCESS_COARSE_LOCATION}
*/
- @androidx.annotation.RequiresPermission(test.pkg.RangeTest.ACCESS_COARSE_LOCATION)
public int test1() { throw new RuntimeException("Stub!"); }
public static final java.lang.String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION";
}
@@ -841,7 +821,6 @@
* <br>
* Requires {@link test.pkg.RangeTest#ACCESS_COARSE_LOCATION}
*/
- @androidx.annotation.RequiresPermission(test.pkg.RangeTest.ACCESS_COARSE_LOCATION)
public int test1() { throw new RuntimeException("Stub!"); }
public static final java.lang.String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION";
}
@@ -878,7 +857,7 @@
/**
* @param parameter2 Value is 10 or greater
*/
- public int test1(int parameter1, @androidx.annotation.IntRange(from=10) int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
+ public int test1(int parameter1, int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
}
"""
)
@@ -929,7 +908,6 @@
* @param parameter3 docs for parameter2
* @return return value documented here
*/
- @androidx.annotation.RequiresPermission(test.pkg.RangeTest.ACCESS_COARSE_LOCATION)
public int test1(int parameter1, int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
public static final java.lang.String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION";
}
@@ -972,7 +950,7 @@
* @param parameter2 Value is 10 or greater
* @return return value documented here
*/
- public int test1(int parameter1, @androidx.annotation.IntRange(from=10) int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
+ public int test1(int parameter1, int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
}
"""
)
@@ -1017,7 +995,7 @@
* @param parameter2 Value is 10 or greater
* @return return value documented here
*/
- public int test1(int parameter1, @androidx.annotation.IntRange(from=10) int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
+ public int test1(int parameter1, int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
}
"""
)
@@ -1064,7 +1042,7 @@
* @param parameter3 docs for parameter2
* @return return value documented here
*/
- public int test1(int parameter1, @androidx.annotation.IntRange(from=10) int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
+ public int test1(int parameter1, int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
}
"""
)
@@ -1100,7 +1078,6 @@
/**
* @return Value is 10 or greater
*/
- @androidx.annotation.IntRange(from=10)
public int test1(int parameter1, int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
}
"""
@@ -1143,7 +1120,6 @@
* @return return value documented here
* Value is 10 or greater
*/
- @androidx.annotation.IntRange(from=10)
public int test1(int parameter1, int parameter2, int parameter3) { throw new RuntimeException("Stub!"); }
}
"""
@@ -1517,6 +1493,188 @@
)
}
+ object SdkExtSinceConstants {
+ val sourceFiles = arrayOf(
+ java(
+ """
+ package android.pkg;
+ public class Test {
+ public static final String UNIT_TEST_1 = "unit.test.1";
+ public static final String UNIT_TEST_2 = "unit.test.2";
+ public static final String UNIT_TEST_3 = "unit.test.3";
+ public Test() {}
+ public void foo() {}
+ public class Inner {
+ public Inner() {}
+ public static final boolean UNIT_TEST_4 = true;
+ }
+ }
+ """
+ )
+ )
+
+ const val apiVersionsXml = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <api version="3">
+ <sdk id="30" shortname="R-ext" name="R Extensions" reference="android/os/Build${'$'}VERSION_CODES${'$'}R" />
+ <sdk id="31" shortname="S-ext" name="S Extensions" reference="android/os/Build${'$'}VERSION_CODES${'$'}S" />
+ <sdk id="33" shortname="T-ext" name="T Extensions" reference="android/os/Build${'$'}VERSION_CODES${'$'}T" />
+ <sdk id="1000000" shortname="standalone-ext" name="Standalone Extensions" reference="some/other/CONST" />
+ <class name="android/pkg/Test" since="1" sdks="0:1,30:2,31:2,33:2">
+ <method name="foo()V"/>
+ <method name="<init>()V"/>
+ <field name="UNIT_TEST_1"/>
+ <field name="UNIT_TEST_2" since="2" sdks="1000000:3,31:3,33:3,0:2"/>
+ <field name="UNIT_TEST_3" since="31" sdks="1000000:4"/>
+ </class>
+ <class name="android/pkg/Test${'$'}Inner" since="1" sdks="0:1,30:2,31:2,33:2">
+ <method name="<init>()V"/>
+ <field name="UNIT_TEST_4"/>
+ </class>
+ </api>
+ """
+
+ const val docStubsSourceList = """
+ TESTROOT/stubs/android/pkg/package-info.java
+ TESTROOT/stubs/android/pkg/Test.java
+ """
+ }
+
+ @Test
+ fun `@sdkExtSince (finalized, no codename)`() {
+ check(
+ extraArguments = arrayOf(
+ ARG_CURRENT_VERSION,
+ "30",
+ ),
+ sourceFiles = SdkExtSinceConstants.sourceFiles,
+ applyApiLevelsXml = SdkExtSinceConstants.apiVersionsXml,
+ checkCompilation = true,
+ docStubs = true,
+ docStubsSourceList = SdkExtSinceConstants.docStubsSourceList,
+ stubFiles = arrayOf(
+ java(
+ """
+ package android.pkg;
+ /**
+ * @apiSince 1
+ * @sdkExtSince R Extensions 2
+ */
+ @SuppressWarnings({"unchecked", "deprecation", "all"})
+ public class Test {
+ /**
+ * @apiSince 1
+ * @sdkExtSince R Extensions 2
+ */
+ public Test() { throw new RuntimeException("Stub!"); }
+ /**
+ * @apiSince 1
+ * @sdkExtSince R Extensions 2
+ */
+ public void foo() { throw new RuntimeException("Stub!"); }
+ /**
+ * @apiSince 1
+ * @sdkExtSince R Extensions 2
+ */
+ public static final java.lang.String UNIT_TEST_1 = "unit.test.1";
+ /**
+ * @apiSince 2
+ * @sdkExtSince Standalone Extensions 3
+ */
+ public static final java.lang.String UNIT_TEST_2 = "unit.test.2";
+ /** @sdkExtSince Standalone Extensions 4 */
+ public static final java.lang.String UNIT_TEST_3 = "unit.test.3";
+ /**
+ * @apiSince 1
+ * @sdkExtSince R Extensions 2
+ */
+ @SuppressWarnings({"unchecked", "deprecation", "all"})
+ public class Inner {
+ /**
+ * @apiSince 1
+ * @sdkExtSince R Extensions 2
+ */
+ public Inner() { throw new RuntimeException("Stub!"); }
+ /**
+ * @apiSince 1
+ * @sdkExtSince R Extensions 2
+ */
+ public static final boolean UNIT_TEST_4 = true;
+ }
+ }
+ """
+ )
+ )
+ )
+ }
+
+ @Test
+ fun `@sdkExtSince (not finalized)`() {
+ check(
+ sourceFiles = SdkExtSinceConstants.sourceFiles,
+ applyApiLevelsXml = SdkExtSinceConstants.apiVersionsXml,
+ checkCompilation = true,
+ docStubs = true,
+ docStubsSourceList = SdkExtSinceConstants.docStubsSourceList,
+ stubFiles = arrayOf(
+ java(
+ """
+ package android.pkg;
+ /**
+ * @apiSince 1
+ * @sdkExtSince R Extensions 2
+ */
+ @SuppressWarnings({"unchecked", "deprecation", "all"})
+ public class Test {
+ /**
+ * @apiSince 1
+ * @sdkExtSince R Extensions 2
+ */
+ public Test() { throw new RuntimeException("Stub!"); }
+ /**
+ * @apiSince 1
+ * @sdkExtSince R Extensions 2
+ */
+ public void foo() { throw new RuntimeException("Stub!"); }
+ /**
+ * @apiSince 1
+ * @sdkExtSince R Extensions 2
+ */
+ public static final java.lang.String UNIT_TEST_1 = "unit.test.1";
+ /**
+ * @apiSince 2
+ * @sdkExtSince Standalone Extensions 3
+ */
+ public static final java.lang.String UNIT_TEST_2 = "unit.test.2";
+ /**
+ * @apiSince 31
+ * @sdkExtSince Standalone Extensions 4
+ */
+ public static final java.lang.String UNIT_TEST_3 = "unit.test.3";
+ /**
+ * @apiSince 1
+ * @sdkExtSince R Extensions 2
+ */
+ @SuppressWarnings({"unchecked", "deprecation", "all"})
+ public class Inner {
+ /**
+ * @apiSince 1
+ * @sdkExtSince R Extensions 2
+ */
+ public Inner() { throw new RuntimeException("Stub!"); }
+ /**
+ * @apiSince 1
+ * @sdkExtSince R Extensions 2
+ */
+ public static final boolean UNIT_TEST_4 = true;
+ }
+ }
+ """
+ )
+ )
+ )
+ }
+
@Test
fun `Generate overview html docs`() {
// If a codebase provides overview.html files in the a public package,
@@ -1707,7 +1865,6 @@
package test.pkg;
/** @apiSince 21 */
@SuppressWarnings({"unchecked", "deprecation", "all"})
- @androidx.annotation.RequiresApi(21)
public class MyClass1 {
public MyClass1() { throw new RuntimeException("Stub!"); }
}
diff --git a/src/test/java/com/android/tools/metalava/DriverTest.kt b/src/test/java/com/android/tools/metalava/DriverTest.kt
index b8f2f85..e56e041 100644
--- a/src/test/java/com/android/tools/metalava/DriverTest.kt
+++ b/src/test/java/com/android/tools/metalava/DriverTest.kt
@@ -77,7 +77,7 @@
}
protected fun createProject(vararg files: TestFile): File {
- val dir = temporaryFolder.newFolder("project")
+ val dir = newFolder("project")
files
.map { it.createFile(dir) }
@@ -86,11 +86,33 @@
return dir
}
+ private fun newFolder(children: String = ""): File {
+ var dir = File(temporaryFolder.root.path, children)
+ return if (dir.exists()) {
+ dir
+ } else {
+ temporaryFolder.newFolder(children)
+ }
+ }
+
+ private fun newFile(children: String = ""): File {
+ var dir = File(temporaryFolder.root.path, children)
+ return if (dir.exists()) {
+ dir
+ } else {
+ temporaryFolder.newFile(children)
+ }
+ }
+
// Makes a note to fail the test, but still allows the test to complete before failing
protected fun addError(error: String) {
errorCollector.addError(Throwable(error))
}
+ protected fun getApiFile(): File {
+ return File(temporaryFolder.root.path, "public-api.txt")
+ }
+
protected fun runDriver(vararg args: String, expectedFail: String = ""): String {
resetTicker()
@@ -119,7 +141,7 @@
actualFail.replace(".", "").trim()
) {
val reportedCompatError = actualFail.startsWith("Aborting: Found compatibility problems checking the ")
- if (expectedFail == "Aborting: Found compatibility problems with --check-compatibility" &&
+ if (expectedFail == "Aborting: Found compatibility problems" &&
reportedCompatError
) {
// Special case for compat checks; we don't want to force each one of them
@@ -347,11 +369,6 @@
/** Enable nullability validation for the listed classes */
validateNullabilityFromList: String? = null,
/**
- * Whether to include source retention annotations in the stubs (in that case they do not
- * go into the extracted annotations zip file)
- */
- includeSourceRetentionAnnotations: Boolean = true,
- /**
* Whether to include the signature version in signatures
*/
includeSignatureVersion: Boolean = false,
@@ -416,7 +433,7 @@
expectedFail != null -> expectedFail
(checkCompatibilityApiReleased != null || checkCompatibilityRemovedApiReleased != null) &&
expectedIssues != null && expectedIssues.trim().isNotEmpty() -> {
- "Aborting: Found compatibility problems with --check-compatibility"
+ "Aborting: Found compatibility problems"
}
else -> ""
}
@@ -703,13 +720,6 @@
emptyArray()
}
- val includeSourceRetentionAnnotationArgs =
- if (includeSourceRetentionAnnotations) {
- arrayOf(ARG_INCLUDE_SOURCE_RETENTION)
- } else {
- emptyArray()
- }
-
var removedApiFile: File? = null
val removedArgs = if (removedApi != null) {
removedApiFile = temporaryFolder.newFile("removed.txt")
@@ -718,13 +728,9 @@
emptyArray()
}
- var apiFile: File? = null
- val apiArgs = if (api != null) {
- apiFile = temporaryFolder.newFile("public-api.txt")
- arrayOf(ARG_API, apiFile.path)
- } else {
- emptyArray()
- }
+ // Always pass apiArgs and generate API text file in runDriver
+ var apiFile: File = newFile("public-api.txt")
+ val apiArgs = arrayOf(ARG_API, apiFile.path)
var apiXmlFile: File? = null
val apiXmlArgs = if (apiXml != null) {
@@ -796,7 +802,7 @@
var stubsDir: File? = null
val stubsArgs = if (stubFiles.isNotEmpty()) {
- stubsDir = temporaryFolder.newFolder("stubs")
+ stubsDir = newFolder("stubs")
if (docStubs) {
arrayOf(ARG_DOC_STUBS, stubsDir.path)
} else {
@@ -928,12 +934,12 @@
validateNullabilityTxt = null
emptyArray()
}
- val validateNullablityFromListFile: File?
+ val validateNullabilityFromListFile: File?
val validateNullabilityFromListArgs = if (validateNullabilityFromList != null) {
- validateNullablityFromListFile = temporaryFolder.newFile("validate-nullability-classes.txt")
- validateNullablityFromListFile.writeText(validateNullabilityFromList)
+ validateNullabilityFromListFile = temporaryFolder.newFile("validate-nullability-classes.txt")
+ validateNullabilityFromListFile.writeText(validateNullabilityFromList)
arrayOf(
- ARG_VALIDATE_NULLABILITY_FROM_LIST, validateNullablityFromListFile.path
+ ARG_VALIDATE_NULLABILITY_FROM_LIST, validateNullabilityFromListFile.path
)
} else {
emptyArray()
@@ -963,7 +969,7 @@
// test root folder such that we clean up the output strings referencing
// paths to the temp folder
"--temp-folder",
- temporaryFolder.newFolder("temp").path,
+ newFolder("temp").path,
// Annotation generation temporarily turned off by default while integrating with
// SDK builds; tests need these
@@ -1007,7 +1013,6 @@
*hideMetaAnnotationArguments,
*showForStubPurposesAnnotationArguments,
*showUnannotatedArgs,
- *includeSourceRetentionAnnotationArgs,
*apiLintArgs,
*sdkFilesArgs,
*importedPackageArgs.toTypedArray(),
@@ -1041,7 +1046,7 @@
assertEquals(expectedOutput.trimIndent().trim(), actualOutput.trim())
}
- if (api != null && apiFile != null) {
+ if (api != null) {
assertTrue("${apiFile.path} does not exist even though --api was used", apiFile.exists())
val actualText = readFile(apiFile, stripBlankLines, trim)
assertEquals(prepareExpectedApi(api, format), actualText)
@@ -1192,15 +1197,21 @@
if (stubFiles.isNotEmpty()) {
for (expected in stubFiles) {
val actual = File(stubsDir!!, expected.targetRelativePath)
- if (actual.exists()) {
- val actualContents = readFile(actual, stripBlankLines, trim)
- assertEquals(expected.contents, actualContents)
- } else {
- val existing = stubsDir.walkTopDown().filter { it.isFile }.map { it.path }.joinToString("\n ")
+ if (!actual.exists()) {
+ val existing = stubsDir.walkTopDown()
+ .filter { it.isFile }
+ .map { it.path }
+ .joinToString("\n ")
throw FileNotFoundException(
- "Could not find a generated stub for ${expected.targetRelativePath}. Found these files: \n $existing"
+ "Could not find a generated stub for ${expected.targetRelativePath}. " +
+ "Found these files: \n $existing"
)
}
+ val actualContents = readFile(actual, stripBlankLines, trim)
+ val stubSource = if (sourceFiles.isEmpty()) "text" else "source"
+ val message =
+ "Generated from-$stubSource stub contents does not match expected contents"
+ assertEquals(message, expected.contents, actualContents)
}
}
@@ -1364,7 +1375,8 @@
}
}
- private fun readFile(file: File, stripBlankLines: Boolean = false, trim: Boolean = false): String {
+ @JvmStatic
+ protected fun readFile(file: File, stripBlankLines: Boolean = false, trim: Boolean = false): String {
var apiLines: List<String> = Files.asCharSource(file, UTF_8).readLines()
if (stripBlankLines) {
apiLines = apiLines.asSequence().filter { it.isNotBlank() }.toList()
diff --git a/src/test/java/com/android/tools/metalava/ExtractAnnotationsTest.kt b/src/test/java/com/android/tools/metalava/ExtractAnnotationsTest.kt
index 0152c82..0a8b802 100644
--- a/src/test/java/com/android/tools/metalava/ExtractAnnotationsTest.kt
+++ b/src/test/java/com/android/tools/metalava/ExtractAnnotationsTest.kt
@@ -74,7 +74,6 @@
@Test
fun `Check java typedef extraction and warning about non-source retention of typedefs`() {
check(
- includeSourceRetentionAnnotations = false,
format = FileFormat.V2,
sourceFiles = sourceFiles1,
expectedIssues = "src/test/pkg/IntDefTest.java:13: error: This typedef annotation class should have @Retention(RetentionPolicy.SOURCE) [AnnotationExtraction]",
@@ -108,7 +107,6 @@
@Test
fun `Check Kotlin and referencing hidden constants from typedef`() {
check(
- includeSourceRetentionAnnotations = false,
sourceFiles = arrayOf(
kotlin(
"""
@@ -186,7 +184,6 @@
@Test
fun `Check including only class retention annotations other than typedefs`() {
check(
- includeSourceRetentionAnnotations = true,
sourceFiles = arrayOf(
kotlin(
"""
@@ -264,7 +261,6 @@
@Test
fun `Extract permission annotations`() {
check(
- includeSourceRetentionAnnotations = false,
sourceFiles = arrayOf(
java(
"""
@@ -333,7 +329,6 @@
@Test
fun `Include merged annotations in exported source annotations`() {
check(
- includeSourceRetentionAnnotations = true,
outputKotlinStyleNulls = false,
includeSystemApiAnnotations = false,
expectedIssues = "error: Unexpected reference to Nonexistent.Field [InternalError]",
@@ -397,7 +392,6 @@
@Test
fun `Only including class retention annotations in stubs`() {
check(
- includeSourceRetentionAnnotations = false,
outputKotlinStyleNulls = false,
includeSystemApiAnnotations = false,
sourceFiles = arrayOf(
@@ -446,7 +440,6 @@
@Test
fun `Check warning about unexpected returns from typedef method`() {
check(
- includeSourceRetentionAnnotations = false,
expectedIssues = "src/test/pkg/IntDefTest.java:36: warning: Returning unexpected constant UNRELATED; is @DialogStyle missing this constant? Expected one of STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT [ReturningUnexpectedConstant]",
sourceFiles = arrayOf(
java(
@@ -523,7 +516,6 @@
@Test
fun `No typedef signatures in api files`() {
check(
- includeSourceRetentionAnnotations = false,
extraArguments = arrayOf(
ARG_HIDE_PACKAGE, "android.annotation",
ARG_TYPEDEFS_IN_SIGNATURES, "none"
@@ -559,7 +551,6 @@
@Test
fun `Inlining typedef signatures in api files`() {
check(
- includeSourceRetentionAnnotations = false,
extraArguments = arrayOf(
ARG_HIDE_PACKAGE, "android.annotation",
ARG_TYPEDEFS_IN_SIGNATURES, "inline"
@@ -595,7 +586,6 @@
@Test
fun `Referencing typedef signatures in api files`() {
check(
- includeSourceRetentionAnnotations = false,
extraArguments = arrayOf(
ARG_HIDE_PACKAGE, "android.annotation",
ARG_TYPEDEFS_IN_SIGNATURES, "ref"
@@ -631,7 +621,6 @@
@Test
fun `Test generics in XML attributes are encoded`() {
check(
- includeSourceRetentionAnnotations = false,
outputKotlinStyleNulls = false,
includeSystemApiAnnotations = false,
sourceFiles = arrayOf(
diff --git a/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt b/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt
index 95920c3..4565002 100644
--- a/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt
+++ b/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt
@@ -98,7 +98,7 @@
package test.pkg;
@SuppressWarnings({"unchecked", "deprecation", "all"})
public abstract class MyTest {
- private MyTest() { throw new RuntimeException("Stub!"); }
+ MyTest() { throw new RuntimeException("Stub!"); }
@androidx.annotation.RecentlyNullable
public java.lang.Double convert1(java.lang.Float f) { throw new RuntimeException("Stub!"); }
}
@@ -148,7 +148,7 @@
package test.pkg;
@SuppressWarnings({"unchecked", "deprecation", "all"})
public abstract class MyTest {
- private MyTest() { throw new RuntimeException("Stub!"); }
+ MyTest() { throw new RuntimeException("Stub!"); }
public java.lang.Double convert1(@androidx.annotation.RecentlyNonNull java.lang.Float f) { throw new RuntimeException("Stub!"); }
}
"""
@@ -815,7 +815,7 @@
package test.pkg;
@SuppressWarnings({"unchecked", "deprecation", "all"})
public class Child1 extends test.pkg.Parent {
- private Child1() { throw new RuntimeException("Stub!"); }
+ Child1() { throw new RuntimeException("Stub!"); }
public void method1(@androidx.annotation.RecentlyNonNull java.lang.String first, int second) { throw new RuntimeException("Stub!"); }
}
"""
@@ -825,7 +825,7 @@
package test.pkg;
@SuppressWarnings({"unchecked", "deprecation", "all"})
public class Child2 extends test.pkg.Parent {
- private Child2() { throw new RuntimeException("Stub!"); }
+ Child2() { throw new RuntimeException("Stub!"); }
public void method0(java.lang.String first, int second) { throw new RuntimeException("Stub!"); }
public void method1(java.lang.String first, int second) { throw new RuntimeException("Stub!"); }
public void method2(@androidx.annotation.RecentlyNonNull java.lang.String first, int second) { throw new RuntimeException("Stub!"); }
diff --git a/src/test/java/com/android/tools/metalava/OptionsTest.kt b/src/test/java/com/android/tools/metalava/OptionsTest.kt
index 4390830..47e437a 100644
--- a/src/test/java/com/android/tools/metalava/OptionsTest.kt
+++ b/src/test/java/com/android/tools/metalava/OptionsTest.kt
@@ -241,13 +241,9 @@
encoded its types using Kotlin style types: a suffix of "?" for nullable
types, no suffix for non nullable types, and "!" for unknown. The default
is no.
---check-compatibility:type:state <file>
+--check-compatibility:type:released <file>
Check compatibility. Type is one of 'api' and 'removed', which checks
- either the public api or the removed api. State is one of 'current' and
- 'released', to check either the currently in development API or the last
- publicly released API, respectively. Different compatibility checks apply
- in the two scenarios. For example, to check the code base against the
- current public API, use --check-compatibility:api:current.
+ either the public api or the removed api.
--check-compatibility:base <file>
When performing a compat check, use the provided signature file as a base
api, which is treated as part of the API being checked. This allows us to
@@ -331,12 +327,6 @@
--extract-annotations <zipfile>
Extracts source annotations from the source files and writes them into the
given zip file
---include-annotation-classes <dir>
- Copies the given stub annotation source files into the generated stub
- sources; <dir> is typically metalava/stub-annotations/src/main/java/.
---rewrite-annotations <dir/jar>
- For a bytecode folder or output jar, rewrites the androidx annotations to
- be package private
--force-convert-to-warning-nullability-annotations <package1:-package2:...>
On every API declared in a class referenced by the given filter, makes
nullability issues appear to callers as warnings rather than errors by
@@ -361,6 +351,11 @@
--generate-api-levels <xmlfile>
Reads android.jar SDK files and generates an XML file recording the API
level for each class, method and field
+--remove-missing-class-references-in-api-levels
+ Removes references to missing classes when generating the API levels XML
+ file. This can happen when generating the XML file for the non-updatable
+ portions of the module-lib sdk, as those non-updatable portions can
+ reference classes that are part of an updatable apex.
--android-jar-pattern <pattern>
Patterns to use to locate Android JAR files. The default is
${"$"}ANDROID_HOME/platforms/android-%/android.jar.
@@ -372,6 +367,27 @@
Sets the code name for the current source code
--current-jar
Points to the current API jar, if any
+--sdk-extensions-root
+ Points to root of prebuilt extension SDK jars, if any. This directory is
+ expected to contain snapshots of historical extension SDK versions in the
+ form of stub jars. The paths should be on the format
+ "<int>/public/<module-name>.jar", where <int> corresponds to the extension
+ SDK version, and <module-name> to the name of the mainline module.
+--sdk-extensions-info
+ Points to map of extension SDK APIs to include, if any. The file is a plain
+ text file and describes, per extension SDK, what APIs from that extension
+ to include in the file created via --generate-api-levels. The format of
+ each line is one of the following: "<module-name> <pattern> <ext-name>
+ [<ext-name> [...]]", where <module-name> is the name of the mainline module
+ this line refers to, <pattern> is a common Java name prefix of the APIs
+ this line refers to, and <ext-name> is a list of extension SDK names in
+ which these SDKs first appeared, or "<ext-name> <ext-id> <type>", where
+ <ext-name> is the name of an SDK, <ext-id> its numerical ID and <type> is
+ one of "platform" (the Android platform SDK), "platform-ext" (an extension
+ to the Android platform SDK), "standalone" (a separate SDK). Fields are
+ separated by whitespace. A mainline module may be listed multiple times.
+ The special pattern "*" refers to all APIs in the given mainline module.
+ Lines beginning with # are comments.
Sandboxing:
diff --git a/src/test/java/com/android/tools/metalava/RewriteAnnotationsTest.kt b/src/test/java/com/android/tools/metalava/RewriteAnnotationsTest.kt
index 0f5e146..ec3aad7 100644
--- a/src/test/java/com/android/tools/metalava/RewriteAnnotationsTest.kt
+++ b/src/test/java/com/android/tools/metalava/RewriteAnnotationsTest.kt
@@ -16,17 +16,12 @@
package com.android.tools.metalava
-import com.android.tools.lint.checks.infrastructure.TestFiles.base64gzip
-import com.android.tools.lint.checks.infrastructure.TestFiles.jar
-import com.android.tools.lint.checks.infrastructure.TestFiles.xml
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.File
-import java.lang.reflect.Modifier
-import java.net.URLClassLoader
import kotlin.text.Charsets.UTF_8
class RewriteAnnotationsTest : DriverTest() {
@@ -135,81 +130,4 @@
)
}
}
-
- @Test
- fun `Test rewriting the bytecode for one of the public annotations`() {
- val bytecode = base64gzip(
- "androidx/annotation/CallSuper.class",
- "" +
- "H4sIAAAAAAAAAIWPsU4CQRRF70NhEQWxJMZoLCjdxs6KIMYCA2E3NlbD8kKG" +
- "DDNkmSXwaxZ+gB9FfGMBFps4yczc5J53kve9//wC8IirCK0IlxHahEbiijzj" +
- "F22Y0OorY5JixfnDQm0UoTMprNdLftdrPTXcs9Z55bWza8LdMDCxUXYeq0MR" +
- "T9izDemJUN0oU4i3+w86dkZnuzDQH/aShHBTPpCqfM5euPvyfmB4KcZ0t2KB" +
- "am+D9HX0LDZlZ7nTs+1f9rAqoX2UjaYLzjzhttR/3L9LIFTkniCcCk5/3ypq" +
- "8l9LiqSrM87QwHmIHyDGBZo/ObYRQoUBAAA="
- )
-
- val compiledStubs = temporaryFolder.newFolder("compiled-stubs")
- bytecode.createFile(compiledStubs)
-
- runDriver(
- ARG_NO_COLOR,
- ARG_NO_BANNER,
-
- ARG_REWRITE_ANNOTATIONS,
- compiledStubs.path,
-
- ARG_CLASS_PATH,
- getAndroidJar().path
- )
-
- // Load the class to make sure it's legit
- val url = compiledStubs.toURI().toURL()
- val loader = URLClassLoader(arrayOf(url), null)
- val annotationClass = loader.loadClass("androidx.annotation.CallSuper")
- val modifiers = annotationClass.modifiers
- assertEquals(0, modifiers and Modifier.PUBLIC)
- assertTrue(annotationClass.isAnnotation)
- }
-
- @Test
- fun `Test rewriting the bytecode for one of the public annotations in a jar file`() {
- val bytecode = base64gzip(
- "androidx/annotation/CallSuper.class",
- "" +
- "H4sIAAAAAAAAAIWPsU4CQRRF70NhEQWxJMZoLCjdxs6KIMYCA2E3NlbD8kKG" +
- "DDNkmSXwaxZ+gB9FfGMBFps4yczc5J53kve9//wC8IirCK0IlxHahEbiijzj" +
- "F22Y0OorY5JixfnDQm0UoTMprNdLftdrPTXcs9Z55bWza8LdMDCxUXYeq0MR" +
- "T9izDemJUN0oU4i3+w86dkZnuzDQH/aShHBTPpCqfM5euPvyfmB4KcZ0t2KB" +
- "am+D9HX0LDZlZ7nTs+1f9rAqoX2UjaYLzjzhttR/3L9LIFTkniCcCk5/3ypq" +
- "8l9LiqSrM87QwHmIHyDGBZo/ObYRQoUBAAA="
- )
-
- val jarDesc = jar(
- "myjar.jar",
- bytecode,
- xml("foo/bar/baz.xml", "<hello-world/>")
- )
-
- val jarFile = jarDesc.createFile(temporaryFolder.root)
-
- runDriver(
- ARG_NO_COLOR,
- ARG_NO_BANNER,
-
- ARG_REWRITE_ANNOTATIONS,
- jarFile.path,
-
- ARG_CLASS_PATH,
- getAndroidJar().path
- )
-
- // Load the class to make sure it's legit
- val url = jarFile.toURI().toURL()
- val loader = URLClassLoader(arrayOf(url), null)
- val annotationClass = loader.loadClass("androidx.annotation.CallSuper")
- val modifiers = annotationClass.modifiers
- assertEquals(0, modifiers and Modifier.PUBLIC)
- assertTrue(annotationClass.isAnnotation)
- }
}
diff --git a/src/test/java/com/android/tools/metalava/SubtractApiTest.kt b/src/test/java/com/android/tools/metalava/SubtractApiTest.kt
index a74c598..6bbabcd 100644
--- a/src/test/java/com/android/tools/metalava/SubtractApiTest.kt
+++ b/src/test/java/com/android/tools/metalava/SubtractApiTest.kt
@@ -77,7 +77,7 @@
package test.pkg;
@SuppressWarnings({"unchecked", "deprecation", "all"})
public class OnlyInNew {
- private OnlyInNew() { throw new RuntimeException("Stub!"); }
+ OnlyInNew() { throw new RuntimeException("Stub!"); }
public void method1() { throw new RuntimeException("Stub!"); }
public void method5() { throw new RuntimeException("Stub!"); }
public void method6() { throw new RuntimeException("Stub!"); }
diff --git a/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt b/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt
index 813a832..e4dbec3 100644
--- a/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt
+++ b/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt
@@ -21,12 +21,18 @@
import com.android.tools.metalava.ARG_CURRENT_VERSION
import com.android.tools.metalava.ARG_FIRST_VERSION
import com.android.tools.metalava.ARG_GENERATE_API_LEVELS
+import com.android.tools.metalava.ARG_REMOVE_MISSING_CLASS_REFERENCES_IN_API_LEVELS
+import com.android.tools.metalava.ARG_SDK_INFO_FILE
+import com.android.tools.metalava.ARG_SDK_JAR_ROOT
import com.android.tools.metalava.DriverTest
import com.android.tools.metalava.getApiLookup
import com.android.tools.metalava.java
+import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
import org.junit.Test
import java.io.File
import kotlin.text.Charsets.UTF_8
@@ -119,6 +125,55 @@
}
}
+ var extensionSdkJars = File("prebuilts/sdk/extensions")
+ if (!extensionSdkJars.isDirectory) {
+ extensionSdkJars = File("../../prebuilts/sdk/extensions")
+ if (!extensionSdkJars.isDirectory) {
+ println("Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found: $extensionSdkJars")
+ return
+ }
+ }
+
+ val filter = File.createTempFile("filter", "txt")
+ filter.deleteOnExit()
+ filter.writeText(
+ """
+ <sdk-extensions-info>
+ <!-- SDK definitions -->
+ <sdk shortname="R" name="R Extensions" id="30" reference="android/os/Build${'$'}VERSION_CODES${'$'}R" />
+ <sdk shortname="S" name="S Extensions" id="31" reference="android/os/Build${'$'}VERSION_CODES${'$'}S" />
+ <sdk shortname="T" name="T Extensions" id="33" reference="android/os/Build${'$'}VERSION_CODES${'$'}T" />
+
+ <!-- Rules -->
+ <symbol jar="art.module.public.api" pattern="*" sdks="R" />
+ <symbol jar="conscrypt.module.intra.core.api " pattern="" sdks="R" />
+ <symbol jar="conscrypt.module.platform.api" pattern="*" sdks="R" />
+ <symbol jar="conscrypt.module.public.api" pattern="*" sdks="R" />
+ <symbol jar="framework-mediaprovider" pattern="*" sdks="R" />
+ <symbol jar="framework-mediaprovider" pattern="android.provider.MediaStore#canManageMedia" sdks="T" />
+ <symbol jar="framework-permission-s" pattern="*" sdks="R" />
+ <symbol jar="framework-permission" pattern="*" sdks="R" />
+ <symbol jar="framework-sdkextensions" pattern="*" sdks="R" />
+ <symbol jar="framework-scheduling" pattern="*" sdks="R" />
+ <symbol jar="framework-statsd" pattern="*" sdks="R" />
+ <symbol jar="framework-tethering" pattern="*" sdks="R" />
+ <symbol jar="legacy.art.module.platform.api" pattern="*" sdks="R" />
+ <symbol jar="service-media-s" pattern="*" sdks="R" />
+ <symbol jar="service-permission" pattern="*" sdks="R" />
+
+ <!-- use framework-permissions-s to test the order of multiple SDKs is respected -->
+ <symbol jar="android.net.ipsec.ike" pattern="android.net.eap.EapAkaInfo" sdks="R,S,T" />
+ <symbol jar="android.net.ipsec.ike" pattern="android.net.eap.EapInfo" sdks="T,S,R" />
+ <symbol jar="android.net.ipsec.ike" pattern="*" sdks="R" />
+
+ <!-- framework-connectivity: only android.net.CaptivePortal should have the 'sdks' attribute -->
+ <symbol jar="framework-connectivity" pattern="android.net.CaptivePortal" sdks="R" />
+
+ <!-- framework-media explicitly omitted: nothing in this module should have the 'sdks' attribute -->
+ </sdk-extensions-info>
+ """.trimIndent()
+ )
+
val output = File.createTempFile("api-info", "xml")
output.deleteOnExit()
val outputPath = output.path
@@ -129,17 +184,35 @@
outputPath,
ARG_ANDROID_JAR_PATTERN,
"${platformJars.path}/%/public/android.jar",
+ ARG_SDK_JAR_ROOT,
+ "$extensionSdkJars",
+ ARG_SDK_INFO_FILE,
+ filter.path,
ARG_FIRST_VERSION,
- "21"
+ "21",
+ ARG_CURRENT_VERSION,
+ "33"
)
)
assertTrue(output.isFile)
val xml = output.readText(UTF_8)
- assertTrue(xml.contains("<api version=\"2\" min=\"21\">"))
+ assertTrue(xml.contains("<api version=\"3\" min=\"21\">"))
+ assertTrue(xml.contains("<sdk id=\"30\" shortname=\"R\" name=\"R Extensions\" reference=\"android/os/Build\$VERSION_CODES\$R\"/>"))
+ assertTrue(xml.contains("<sdk id=\"31\" shortname=\"S\" name=\"S Extensions\" reference=\"android/os/Build\$VERSION_CODES\$S\"/>"))
+ assertTrue(xml.contains("<sdk id=\"33\" shortname=\"T\" name=\"T Extensions\" reference=\"android/os/Build\$VERSION_CODES\$T\"/>"))
assertTrue(xml.contains("<class name=\"android/Manifest\" since=\"21\">"))
assertTrue(xml.contains("<field name=\"showWhenLocked\" since=\"27\"/>"))
+ // top level class marked as since=21 and R=1, implemented in the framework-mediaprovider mainline module
+ assertTrue(xml.contains("<class name=\"android/provider/MediaStore\" module=\"framework-mediaprovider\" since=\"21\" sdks=\"30:1,0:21\">"))
+
+ // method with identical sdks attribute as containing class: sdks attribute should be omitted
+ assertTrue(xml.contains("<method name=\"getMediaScannerUri()Landroid/net/Uri;\"/>"))
+
+ // method with different sdks attribute than containing class
+ assertTrue(xml.contains("<method name=\"canManageMedia(Landroid/content/Context;)Z\" since=\"31\" sdks=\"33:1,0:31\"/>"))
+
val apiLookup = getApiLookup(output)
apiLookup.getClassVersion("android.v")
// This field was added in API level 5, but when we're starting the count higher
@@ -148,6 +221,32 @@
val methodVersion = apiLookup.getMethodVersion("android/icu/util/CopticCalendar", "computeTime", "()")
assertEquals(24, methodVersion)
+
+ // The filter says 'framework-permission-s * R' so RoleManager should exist and should have a module/sdks attributes
+ assertTrue(apiLookup.containsClass("android/app/role/RoleManager"))
+ assertTrue(xml.contains("<method name=\"canManageMedia(Landroid/content/Context;)Z\" since=\"31\" sdks=\"33:1,0:31\"/>"))
+
+ // The filter doesn't mention framework-media, so no class in that module should have a module/sdks attributes
+ assertTrue(xml.contains("<class name=\"android/media/MediaFeature\" since=\"31\">"))
+
+ // The filter only defines a single API in framework-connectivity: verify that only that API has the module/sdks attributes
+ assertTrue(xml.contains("<class name=\"android/net/CaptivePortal\" module=\"framework-connectivity\" since=\"23\" sdks=\"30:1,0:23\">"))
+ assertTrue(xml.contains("<class name=\"android/net/ConnectivityDiagnosticsManager\" since=\"30\">"))
+
+ // The order of the SDKs should be respected
+ // android.net.eap.EapAkaInfo R S T -> 0,30,31,33
+ assertTrue(xml.contains("<class name=\"android/net/eap/EapAkaInfo\" module=\"android.net.ipsec.ike\" since=\"33\" sdks=\"30:3,31:3,33:3,0:33\">"))
+ // android.net.eap.EapInfo T S R -> 0,33,31,30
+ assertTrue(xml.contains("<class name=\"android/net/eap/EapInfo\" module=\"android.net.ipsec.ike\" since=\"33\" sdks=\"33:3,31:3,30:3,0:33\">"))
+
+ // Verify historical backfill
+ assertEquals(30, apiLookup.getClassVersion("android/os/ext/SdkExtensions"))
+ assertEquals(30, apiLookup.getMethodVersion("android/os/ext/SdkExtensions", "getExtensionVersion", "(I)I"))
+ assertEquals(31, apiLookup.getMethodVersion("android/os/ext/SdkExtensions", "getAllExtensionVersions", "()Ljava/util/Map;"))
+
+ // Verify there's no extension versions listed for SdkExtensions
+ val sdkExtClassLine = xml.lines().first { it.contains("<class name=\"android/os/ext/SdkExtensions\"") }
+ assertFalse(sdkExtClassLine.contains("sdks="))
}
@Test
@@ -259,4 +358,195 @@
val apiLookup = getApiLookup(output, temporaryFolder.newFolder())
assertEquals(90, apiLookup.getClassVersion("android.pkg.MyTest"))
}
+
+ @Test
+ fun `Generate API for test prebuilts`() {
+ var testPrebuiltsRoot = File(System.getenv("METALAVA_TEST_PREBUILTS_SDK_ROOT"))
+ if (!testPrebuiltsRoot.isDirectory) {
+ fail("test prebuilts not found: $testPrebuiltsRoot")
+ }
+
+ val api_versions_xml = File.createTempFile("api-versions", "xml")
+ api_versions_xml.deleteOnExit()
+
+ check(
+ extraArguments = arrayOf(
+ ARG_GENERATE_API_LEVELS,
+ api_versions_xml.path,
+ ARG_ANDROID_JAR_PATTERN,
+ "${testPrebuiltsRoot.path}/%/public/android.jar",
+ ARG_SDK_JAR_ROOT,
+ "${testPrebuiltsRoot.path}/extensions",
+ ARG_SDK_INFO_FILE,
+ "${testPrebuiltsRoot.path}/sdk-extensions-info.xml",
+ ARG_FIRST_VERSION,
+ "30",
+ ARG_CURRENT_VERSION,
+ "32",
+ ARG_CURRENT_CODENAME,
+ "Foo"
+ ),
+ sourceFiles = arrayOf(
+ java(
+ """
+ package android.test;
+ public class ClassAddedInApi31AndExt2 {
+ private ClassAddedInApi31AndExt2() {}
+ public static final int FIELD_ADDED_IN_API_31_AND_EXT_2 = 1;
+ public static final int FIELD_ADDED_IN_EXT_3 = 2;
+ public void methodAddedInApi31AndExt2() { throw new RuntimeException("Stub!"); }
+ public void methodAddedInExt3() { throw new RuntimeException("Stub!"); };
+ public void methodNotFinalized() { throw new RuntimeException("Stub!"); }
+ }
+ """
+ )
+ )
+ )
+
+ assertTrue(api_versions_xml.isFile)
+ val xml = api_versions_xml.readText(UTF_8)
+
+ val expected = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <api version="3" min="30">
+ <sdk id="30" shortname="R-ext" name="R Extensions" reference="android/os/Build${'$'}VERSION_CODES${'$'}R"/>
+ <sdk id="31" shortname="S-ext" name="S Extensions" reference="android/os/Build${'$'}VERSION_CODES${'$'}S"/>
+ <class name="android/test/ClassAddedInApi30" since="30">
+ <extends name="java/lang/Object"/>
+ <method name="methodAddedInApi30()V"/>
+ <method name="methodAddedInApi31()V" since="31"/>
+ </class>
+ <class name="android/test/ClassAddedInApi31AndExt2" module="framework-ext" since="31" sdks="30:2,31:2,0:31">
+ <extends name="java/lang/Object"/>
+ <method name="methodAddedInApi31AndExt2()V"/>
+ <method name="methodAddedInExt3()V" since="33" sdks="30:3,31:3"/>
+ <method name="methodNotFinalized()V" since="33" sdks="0:33"/>
+ <field name="FIELD_ADDED_IN_API_31_AND_EXT_2"/>
+ <field name="FIELD_ADDED_IN_EXT_3" since="33" sdks="30:3,31:3"/>
+ </class>
+ <class name="android/test/ClassAddedInExt1" module="framework-ext" since="31" sdks="30:1,31:1,0:31">
+ <extends name="java/lang/Object"/>
+ <method name="methodAddedInApi31AndExt2()V" sdks="30:2,31:2,0:31"/>
+ <method name="methodAddedInExt1()V"/>
+ <method name="methodAddedInExt3()V" since="33" sdks="30:3,31:3"/>
+ <field name="FIELD_ADDED_IN_API_31_AND_EXT_2" sdks="30:2,31:2,0:31"/>
+ <field name="FIELD_ADDED_IN_EXT_1"/>
+ <field name="FIELD_ADDED_IN_EXT_3" since="33" sdks="30:3,31:3"/>
+ </class>
+ <class name="android/test/ClassAddedInExt3" module="framework-ext" since="33" sdks="30:3,31:3">
+ <extends name="java/lang/Object"/>
+ <method name="methodAddedInExt3()V"/>
+ <field name="FIELD_ADDED_IN_EXT_3"/>
+ </class>
+ <class name="java/lang/Object" since="30">
+ <method name="<init>()V"/>
+ </class>
+ </api>
+ """
+
+ fun String.trimEachLine(): String =
+ lines().map {
+ it.trim()
+ }.filter {
+ it.isNotEmpty()
+ }.joinToString("\n")
+
+ assertEquals(expected.trimEachLine(), xml.trimEachLine())
+ }
+
+ @Test
+ fun `Generate API while removing missing class references`() {
+ val api_versions_xml = File.createTempFile("api-versions", "xml")
+ api_versions_xml.deleteOnExit()
+
+ check(
+ extraArguments = arrayOf(
+ ARG_GENERATE_API_LEVELS,
+ api_versions_xml.path,
+ ARG_REMOVE_MISSING_CLASS_REFERENCES_IN_API_LEVELS,
+ ARG_FIRST_VERSION,
+ "30",
+ ARG_CURRENT_VERSION,
+ "32",
+ ARG_CURRENT_CODENAME,
+ "Foo"
+ ),
+ sourceFiles = arrayOf(
+ java(
+ """
+ package android.test;
+ public class ClassThatImplementsMethodFromApex implements ClassFromApex {
+ }
+ """
+ )
+ )
+ )
+
+ assertTrue(api_versions_xml.isFile)
+ val xml = api_versions_xml.readText(UTF_8)
+
+ val expected = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <api version="3" min="30">
+ <class name="android/test/ClassThatImplementsMethodFromApex" since="33">
+ <method name="<init>()V"/>
+ </class>
+ </api>
+ """
+
+ fun String.trimEachLine(): String =
+ lines().map {
+ it.trim()
+ }.filter {
+ it.isNotEmpty()
+ }.joinToString("\n")
+
+ assertEquals(expected.trimEachLine(), xml.trimEachLine())
+ }
+
+ @Test
+ fun `Generate API finds missing class references`() {
+ var testPrebuiltsRoot = File(System.getenv("METALAVA_TEST_PREBUILTS_SDK_ROOT"))
+ if (!testPrebuiltsRoot.isDirectory) {
+ fail("test prebuilts not found: $testPrebuiltsRoot")
+ }
+
+ val api_versions_xml = File.createTempFile("api-versions", "xml")
+ api_versions_xml.deleteOnExit()
+
+ var exception: IllegalStateException? = null
+ try {
+ check(
+ extraArguments = arrayOf(
+ ARG_GENERATE_API_LEVELS,
+ api_versions_xml.path,
+ ARG_FIRST_VERSION,
+ "30",
+ ARG_CURRENT_VERSION,
+ "32",
+ ARG_CURRENT_CODENAME,
+ "Foo"
+ ),
+ sourceFiles = arrayOf(
+ java(
+ """
+ package android.test;
+ // Really this class should implement some interface that doesn't exist,
+ // but that's hard to set up in the test harness, so just verify that
+ // metalava complains about java/lang/Object not existing because we didn't
+ // include the testdata prebuilt jars.
+ public class ClassThatImplementsMethodFromApex {
+ }
+ """
+ )
+ )
+ )
+ } catch (e: IllegalStateException) {
+ exception = e
+ }
+
+ assertNotNull(exception)
+ assertThat(exception?.message ?: "").contains("There are classes in this API that reference other classes that do not exist in this API.")
+ assertThat(exception?.message ?: "").contains("java/lang/Object referenced by:\n android/test/ClassThatImplementsMethodFromApex")
+ }
}
diff --git a/src/test/java/com/android/tools/metalava/apilevels/ApiToExtensionsMapTest.kt b/src/test/java/com/android/tools/metalava/apilevels/ApiToExtensionsMapTest.kt
new file mode 100644
index 0000000..258c39e
--- /dev/null
+++ b/src/test/java/com/android/tools/metalava/apilevels/ApiToExtensionsMapTest.kt
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.apilevels
+
+import org.junit.Assert
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+
+class ApiToExtensionsMapTest {
+ @Test
+ fun `empty input`() {
+ val xml = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- No rules is a valid (albeit weird). -->
+ <sdk-extensions-info>
+ <sdk shortname="R-ext" name="R Extensions" id="30" reference="android/os/Build${'$'}VERSION_CODES${'$'}R" />
+ <sdk shortname="S-ext" name="S Extensions" id="31" reference="android/os/Build${'$'}VERSION_CODES${'$'}S" />
+ <sdk shortname="T-ext" name="T Extensions" id="33" reference="android/os/Build${'$'}VERSION_CODES${'$'}T" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ val map = ApiToExtensionsMap.fromXml("no-module", xml)
+
+ assertTrue(map.getExtensions("com.foo.Bar").isEmpty())
+ }
+
+ @Test
+ fun wildcard() {
+ val xml = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- All APIs will default to extension SDK A. -->
+ <sdk-extensions-info>
+ <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+ <symbol jar="mod" pattern="*" sdks="A" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ val map = ApiToExtensionsMap.fromXml("mod", xml)
+
+ assertEquals(map.getExtensions("com.foo.Bar"), listOf("A"))
+ assertEquals(map.getExtensions("com.foo.SomeOtherBar"), listOf("A"))
+ }
+
+ @Test
+ fun `single class`() {
+ val xml = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- A single class. The class, any internal classes, and any methods are allowed;
+ everything else is denied -->
+ <sdk-extensions-info>
+ <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+ <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ val map = ApiToExtensionsMap.fromXml("mod", xml)
+
+ assertEquals(map.getExtensions("com.foo.Bar"), listOf("A"))
+ assertEquals(map.getExtensions("com.foo.Bar#FIELD"), listOf("A"))
+ assertEquals(map.getExtensions("com.foo.Bar#method"), listOf("A"))
+ assertEquals(map.getExtensions("com.foo.Bar\$Inner"), listOf("A"))
+ assertEquals(map.getExtensions("com.foo.Bar\$Inner\$InnerInner"), listOf("A"))
+
+ val clazz = ApiClass("com/foo/Bar", 1, false)
+ val method = ApiElement("method(Ljava.lang.String;I)V", 2, false)
+ assertEquals(map.getExtensions(clazz), listOf("A"))
+ assertEquals(map.getExtensions(clazz, method), listOf("A"))
+
+ assertTrue(map.getExtensions("com.foo.SomeOtherClass").isEmpty())
+ }
+
+ @Test
+ fun `multiple extensions`() {
+ val xml = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- Any number of white space separated extension SDKs may be listed. -->
+ <sdk-extensions-info>
+ <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+ <sdk shortname="B" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
+ <sdk shortname="FOO" name="FOO Extensions" id="10" reference="android/os/Build${'$'}VERSION_CODES${'$'}FOO" />
+ <sdk shortname="BAR" name="BAR Extensions" id="11" reference="android/os/Build${'$'}VERSION_CODES${'$'}BAR" />
+ <symbol jar="mod" pattern="*" sdks="A,B,FOO,BAR" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ val map = ApiToExtensionsMap.fromXml("mod", xml)
+
+ assertEquals(listOf("A", "B", "FOO", "BAR"), map.getExtensions("com.foo.Bar"))
+ }
+
+ @Test
+ fun precedence() {
+ val xml = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- Multiple classes, and multiple rules with different precedence. -->
+ <sdk-extensions-info>
+ <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+ <sdk shortname="B" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
+ <sdk shortname="C" name="C Extensions" id="3" reference="android/os/Build${'$'}VERSION_CODES${'$'}C" />
+ <sdk shortname="D" name="D Extensions" id="4" reference="android/os/Build${'$'}VERSION_CODES${'$'}D" />
+ <symbol jar="mod" pattern="*" sdks="A" />
+ <symbol jar="mod" pattern="com.foo.Bar" sdks="B" />
+ <symbol jar="mod" pattern="com.foo.Bar${'$'}Inner#method" sdks="C" />
+ <symbol jar="mod" pattern="com.bar.Foo" sdks="D" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ val map = ApiToExtensionsMap.fromXml("mod", xml)
+
+ assertEquals(map.getExtensions("anything"), listOf("A"))
+
+ assertEquals(map.getExtensions("com.foo.Bar"), listOf("B"))
+ assertEquals(map.getExtensions("com.foo.Bar#FIELD"), listOf("B"))
+ assertEquals(map.getExtensions("com.foo.Bar\$Inner"), listOf("B"))
+
+ assertEquals(map.getExtensions("com.foo.Bar\$Inner#method"), listOf("C"))
+
+ assertEquals(map.getExtensions("com.bar.Foo"), listOf("D"))
+ assertEquals(map.getExtensions("com.bar.Foo#FIELD"), listOf("D"))
+ }
+
+ @Test
+ fun `multiple mainline modules`() {
+ val xml = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- The allow list will only consider patterns that are marked with the given mainline module -->
+ <sdk-extensions-info>
+ <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+ <sdk shortname="B" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
+ <symbol jar="foo" pattern="*" sdks="A" />
+ <symbol jar="bar" pattern="*" sdks="B" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ val allowListA = ApiToExtensionsMap.fromXml("foo", xml)
+ val allowListB = ApiToExtensionsMap.fromXml("bar", xml)
+ val allowListC = ApiToExtensionsMap.fromXml("baz", xml)
+
+ assertEquals(allowListA.getExtensions("anything"), listOf("A"))
+ assertEquals(allowListB.getExtensions("anything"), listOf("B"))
+ assertTrue(allowListC.getExtensions("anything").isEmpty())
+ }
+
+ @Test
+ fun `declarations and rules can be mixed`() {
+ val xml = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- SDK declarations and rule lines can be mixed in any order -->
+ <sdk-extensions-info>
+ <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+ <symbol jar="foo" pattern="*" sdks="A,B" />
+ <sdk shortname="B" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ val map = ApiToExtensionsMap.fromXml("foo", xml)
+
+ assertEquals(map.getExtensions("com.foo.Bar"), listOf("A", "B"))
+ }
+
+ @Test
+ fun `bad input`() {
+ assertFailsWith<IllegalArgumentException> {
+ ApiToExtensionsMap.fromXml(
+ "mod",
+ """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- Missing root element -->
+ <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+ <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+ """.trimIndent()
+ )
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ ApiToExtensionsMap.fromXml(
+ "mod",
+ """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- <sdk> tag at unexpected depth -->
+ <sdk-extensions-info version="2">
+ <foo>
+ <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" >
+ </foo>
+ <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ )
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ ApiToExtensionsMap.fromXml(
+ "mod",
+ """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- using 0 (reserved for the Android platform SDK) as ID -->
+ <sdk-extensions-info>
+ <sdk shortname="A" name="A Extensions" id="0" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+ <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ )
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ ApiToExtensionsMap.fromXml(
+ "mod",
+ """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- missing module attribute -->
+ <sdk-extensions-info>
+ <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+ <symbol pattern="com.foo.Bar" sdks="A" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ )
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ ApiToExtensionsMap.fromXml(
+ "mod",
+ """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- duplicate module+pattern pairs -->
+ <sdk-extensions-info>
+ <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+ <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+ <symbol jar="mod" pattern="com.foo.Bar" sdks="B" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ )
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ ApiToExtensionsMap.fromXml(
+ "mod",
+ """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- sdks attribute refer to non-declared SDK -->
+ <sdk-extensions-info>
+ <sdk shortname="B" name="A Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+ <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ )
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ ApiToExtensionsMap.fromXml(
+ "mod",
+ """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- duplicate numerical ID -->
+ <sdk-extensions-info>
+ <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+ <sdk shortname="B" name="B Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
+ <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ )
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ ApiToExtensionsMap.fromXml(
+ "mod",
+ """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- duplicate short SDK name -->
+ <sdk-extensions-info>
+ <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+ <sdk shortname="A" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
+ <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ )
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ ApiToExtensionsMap.fromXml(
+ "mod",
+ """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- duplicate long SDK name -->
+ <sdk-extensions-info>
+ <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+ <sdk shortname="B" name="A Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
+ <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ )
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ ApiToExtensionsMap.fromXml(
+ "mod",
+ """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- duplicate SDK reference -->
+ <sdk-extensions-info version="1">
+ <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+ <sdk shortname="B" name="B Extensions" id="2" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+ <symbol jar="mod" pattern="com.foo.Bar" sdks="A" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ )
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ ApiToExtensionsMap.fromXml(
+ "mod",
+ """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- duplicate SDK for same symbol -->
+ <sdk-extensions-info>
+ <sdk shortname="A" name="A Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}A" />
+ <sdk shortname="B" name="B Extensions" id="1" reference="android/os/Build${'$'}VERSION_CODES${'$'}B" />
+ <symbol jar="mod" pattern="com.foo.Bar" sdks="A,B,A" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ )
+ }
+ }
+
+ @Test
+ fun `calculate sdks xml attribute`() {
+ val xml = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- Verify the calculateSdksAttr method -->
+ <sdk-extensions-info>
+ <sdk shortname="R" name="R Extensions" id="30" reference="android/os/Build${'$'}VERSION_CODES${'$'}R" />
+ <sdk shortname="S" name="S Extensions" id="31" reference="android/os/Build${'$'}VERSION_CODES${'$'}S" />
+ <sdk shortname="T" name="T Extensions" id="33" reference="android/os/Build${'$'}VERSION_CODES${'$'}T" />
+ <sdk shortname="FOO" name="FOO Extensions" id="1000" reference="android/os/Build${'$'}VERSION_CODES${'$'}FOO" />
+ <sdk shortname="BAR" name="BAR Extensions" id="1001" reference="android/os/Build${'$'}VERSION_CODES${'$'}BAR" />
+ </sdk-extensions-info>
+ """.trimIndent()
+ val filter = ApiToExtensionsMap.fromXml("mod", xml)
+
+ Assert.assertEquals(
+ "0:34",
+ filter.calculateSdksAttr(34, 34, listOf(), ApiElement.NEVER)
+ )
+
+ Assert.assertEquals(
+ "30:4",
+ filter.calculateSdksAttr(34, 34, listOf("R"), 4)
+ )
+
+ Assert.assertEquals(
+ "30:4,31:4",
+ filter.calculateSdksAttr(34, 34, listOf("R", "S"), 4)
+ )
+
+ Assert.assertEquals(
+ "30:4,31:4,0:33",
+ filter.calculateSdksAttr(33, 34, listOf("R", "S"), 4)
+ )
+
+ Assert.assertEquals(
+ "30:4,31:4,1000:4,0:33",
+ filter.calculateSdksAttr(33, 34, listOf("R", "S", "FOO"), 4)
+ )
+
+ Assert.assertEquals(
+ "30:4,31:4,1000:4,1001:4,0:33",
+ filter.calculateSdksAttr(33, 34, listOf("R", "S", "FOO", "BAR"), 4)
+ )
+ }
+}
diff --git a/src/test/java/com/android/tools/metalava/apilevels/ExtensionSdkJarReaderTest.kt b/src/test/java/com/android/tools/metalava/apilevels/ExtensionSdkJarReaderTest.kt
new file mode 100644
index 0000000..b6e6b6a
--- /dev/null
+++ b/src/test/java/com/android/tools/metalava/apilevels/ExtensionSdkJarReaderTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.apilevels
+
+import org.junit.Assert.assertEquals
+import java.io.File
+import java.io.FileNotFoundException
+import java.nio.file.Files
+import kotlin.test.Test
+
+class ExtensionSdkJarReaderTest {
+ @Test
+ fun `Verify findExtensionSdkJarFiles`() {
+ TemporaryDirectoryHierarchy(
+ listOf(
+ "1/public/foo.jar",
+ "1/public/bar.jar",
+ "2/public/foo.jar",
+ "2/public/bar.jar",
+ "2/public/baz.jar",
+ )
+ ).use {
+ val root = it.root
+ val expected = mapOf(
+ "foo" to listOf(
+ VersionAndPath(1, File(root, "1/public/foo.jar")),
+ VersionAndPath(2, File(root, "2/public/foo.jar"))
+ ),
+ "bar" to listOf(
+ VersionAndPath(1, File(root, "1/public/bar.jar")),
+ VersionAndPath(2, File(root, "2/public/bar.jar"))
+ ),
+ "baz" to listOf(
+ VersionAndPath(2, File(root, "2/public/baz.jar"))
+ ),
+ )
+ val actual = ExtensionSdkJarReader.findExtensionSdkJarFiles(root)
+ assertEquals(expected, actual)
+ }
+ }
+}
+
+private class TemporaryDirectoryHierarchy(filenames: List<String>) : AutoCloseable {
+ val root: File
+
+ init {
+ root = Files.createTempDirectory("metalava").toFile()
+ for (file in filenames.map { File(root, it) }) {
+ createDirectoryRecursively(file.parentFile)
+ file.createNewFile()
+ }
+ }
+
+ override fun close() {
+ deleteDirectoryRecursively(root)
+ }
+
+ companion object {
+ private fun createDirectoryRecursively(file: File) {
+ val parent = file.parentFile ?: throw FileNotFoundException("$file has no parent")
+ if (!parent.exists()) {
+ createDirectoryRecursively(parent)
+ }
+ file.mkdir()
+ }
+
+ private fun deleteDirectoryRecursively(root: File) {
+ for (file in root.listFiles()) {
+ if (file.isDirectory()) {
+ deleteDirectoryRecursively(file)
+ } else {
+ file.delete()
+ }
+ }
+ root.delete()
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassMethodsAndConstructors.kt b/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassMethodsAndConstructors.kt
index 758d5f0..0246db0 100644
--- a/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassMethodsAndConstructors.kt
+++ b/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassMethodsAndConstructors.kt
@@ -281,8 +281,11 @@
)
}
@Test
- fun `Change final to non-final (Compatible)`() {
+ fun `Change final to non-final (Compatible but Disallowed)`() {
check(
+ expectedIssues = """
+ TESTROOT/load-api.txt:3: error: Method test.pkg.Foo.bar has removed 'final' qualifier [RemovedFinal]
+ """,
signatureSource = """
package test.pkg {
class Foo {
diff --git a/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassesTest.kt b/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassesTest.kt
index 1765f17..afb4e0f 100644
--- a/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassesTest.kt
+++ b/src/test/java/com/android/tools/metalava/binarycompatibility/BinaryCompatibilityClassesTest.kt
@@ -470,8 +470,11 @@
}
@Test
- fun `Change final to non-final (Compatible)`() {
+ fun `Change final to non-final (Compatible but Disallowed)`() {
check(
+ expectedIssues = """
+ TESTROOT/load-api.txt:3: error: Constructor test.pkg.Foo has removed 'final' qualifier [RemovedFinal]
+ """,
signatureSource = """
package test.pkg {
public class Foo {
diff --git a/src/test/java/com/android/tools/metalava/model/AnnotationItemTest.kt b/src/test/java/com/android/tools/metalava/model/AnnotationItemTest.kt
index 3231fbc..7cf4aca 100644
--- a/src/test/java/com/android/tools/metalava/model/AnnotationItemTest.kt
+++ b/src/test/java/com/android/tools/metalava/model/AnnotationItemTest.kt
@@ -28,7 +28,7 @@
@Test
fun `Test shortenAnnotation and unshortenAnnotation`() {
- checkShortenAnnotation("@Nullable", "@androidx.annotation.Nullable")
+ checkShortenAnnotation("@Nullable", "@android.annotation.Nullable")
checkShortenAnnotation("@Deprecated", "@java.lang.Deprecated")
checkShortenAnnotation("@SystemService", "@android.annotation.SystemService")
checkShortenAnnotation("@TargetApi", "@android.annotation.TargetApi")
diff --git a/src/test/java/com/android/tools/metalava/model/psi/JavadocTest.kt b/src/test/java/com/android/tools/metalava/model/psi/JavadocTest.kt
index 342b8ab..f3894fd 100644
--- a/src/test/java/com/android/tools/metalava/model/psi/JavadocTest.kt
+++ b/src/test/java/com/android/tools/metalava/model/psi/JavadocTest.kt
@@ -31,7 +31,6 @@
extraArguments: Array<String> = emptyArray(),
docStubs: Boolean = false,
showAnnotations: Array<String> = emptyArray(),
- includeSourceRetentionAnnotations: Boolean = true,
skipEmitPackages: List<String> = listOf("java.lang", "java.util", "java.io"),
sourceFiles: Array<TestFile>
) {
@@ -44,7 +43,6 @@
api = api,
extraArguments = extraArguments,
docStubs = docStubs,
- includeSourceRetentionAnnotations = includeSourceRetentionAnnotations,
skipEmitPackages = skipEmitPackages
)
}
diff --git a/src/test/java/com/android/tools/metalava/model/psi/PsiMethodItemTest.kt b/src/test/java/com/android/tools/metalava/model/psi/PsiMethodItemTest.kt
index 390643f..6a99f85 100644
--- a/src/test/java/com/android/tools/metalava/model/psi/PsiMethodItemTest.kt
+++ b/src/test/java/com/android/tools/metalava/model/psi/PsiMethodItemTest.kt
@@ -16,8 +16,10 @@
package com.android.tools.metalava.model.psi
+import com.android.tools.metalava.java
import com.android.tools.metalava.kotlin
import org.junit.Test
+import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertSame
@@ -48,4 +50,29 @@
assertNull(component1.property)
}
}
+
+ @Test
+ fun `method return type is non-null`() {
+ val codebase = java(
+ """
+ public class Foo {
+ public Foo() {}
+ public void bar() {}
+ }
+ """
+ )
+ testCodebase(codebase) { c ->
+ val ctorItem = c.assertClass("Foo").findMethod("Foo", "")
+ val ctorReturnType = ctorItem!!.returnType()
+
+ val methodItem = c.assertClass("Foo").findMethod("bar", "")
+ val methodReturnType = methodItem!!.returnType()
+
+ assertNotNull(ctorReturnType)
+ assertEquals("Foo", ctorReturnType.toString(), "Return type of the constructor item must be the containing class.")
+
+ assertNotNull(methodReturnType)
+ assertEquals("void", methodReturnType.toString(), "Return type of an method item should match the expected value.")
+ }
+ }
}
diff --git a/src/test/java/com/android/tools/metalava/model/text/TextClassItemTest.kt b/src/test/java/com/android/tools/metalava/model/text/TextClassItemTest.kt
new file mode 100644
index 0000000..1689ac4
--- /dev/null
+++ b/src/test/java/com/android/tools/metalava/model/text/TextClassItemTest.kt
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.model.text
+
+import org.junit.Test
+import kotlin.test.assertTrue
+
+class TextClassItemTest {
+ @Test
+ fun `test hasEqualReturnType() when return types are derived from interface type variables`() {
+ val codebase = ApiFile.parseApi(
+ "test",
+ """
+ package java.lang {
+ public final class Float extends java.lang.Number implements java.lang.Comparable<java.lang.Float> {
+ }
+ }
+ package java.time {
+ public final class LocalDateTime implements java.time.chrono.ChronoLocalDateTime<java.time.LocalDate> java.io.Serializable java.time.temporal.Temporal java.time.temporal.TemporalAdjuster {
+ method public java.time.LocalDate toLocalDate();
+ }
+ }
+ package java.time.chrono {
+ public interface ChronoLocalDateTime<D extends java.time.chrono.ChronoLocalDate> extends java.time.temporal.Temporal java.lang.Comparable<java.time.chrono.ChronoLocalDateTime<?>> java.time.temporal.TemporalAdjuster {
+ method public D toLocalDate();
+ }
+ }
+ package android.animation {
+ public interface TypeEvaluator<T> {
+ method public T evaluate(float, T, T);
+ }
+ public class FloatEvaluator implements android.animation.TypeEvaluator<java.lang.Number> {
+ method public Float evaluate(float, Number, Number);
+ }
+ }
+ package android.widget {
+ public abstract class AdapterView<T extends android.widget.Adapter> extends android.view.ViewGroup {
+ method public abstract T getAdapter();
+ }
+ public abstract class AbsListView extends android.widget.AdapterView<android.widget.ListAdapter> implements android.widget.Filter.FilterListener android.text.TextWatcher android.view.ViewTreeObserver.OnGlobalLayoutListener android.view.ViewTreeObserver.OnTouchModeChangeListener {
+ }
+ public interface ListAdapter extends android.widget.Adapter {
+ }
+ @android.widget.RemoteViews.RemoteView public class ListView extends android.widget.AbsListView {
+ method public android.widget.ListAdapter getAdapter();
+ }
+ }
+ package android.content {
+ public abstract class AsyncTaskLoader<D> extends android.content.Loader<D> {
+ method public abstract D loadInBackground();
+ }
+ public class CursorLoader extends android.content.AsyncTaskLoader<android.database.Cursor> {
+ method public android.database.Cursor loadInBackground();
+ }
+ }
+ package android.database {
+ public final class CursorJoiner implements java.lang.Iterable<android.database.CursorJoiner.Result> java.util.Iterator<android.database.CursorJoiner.Result> {
+ method public android.database.CursorJoiner.Result next();
+ }
+ }
+ package java.util {
+ public interface Iterator<E> {
+ method public E next();
+ }
+ }
+ package java.lang.invoke {
+ public final class MethodType implements java.io.Serializable java.lang.invoke.TypeDescriptor.OfMethod<java.lang.Class<?>,java.lang.invoke.MethodType> {
+ method public java.lang.invoke.MethodType changeParameterType(int, Class<?>);
+ method public Class<?>[] parameterArray();
+ }
+ public static interface TypeDescriptor.OfMethod<F extends java.lang.invoke.TypeDescriptor.OfField<F>, M extends java.lang.invoke.TypeDescriptor.OfMethod<F, M>> extends java.lang.invoke.TypeDescriptor {
+ method public M changeParameterType(int, F);
+ method public F[] parameterArray();
+ }
+ }
+ """.trimIndent(),
+ false
+ )
+
+ val toLocalDate1 = codebase.getOrCreateClass("java.time.LocalDateTime").findMethod("toLocalDate", "")!!
+ val toLocalDate2 = codebase.getOrCreateClass("java.time.chrono.ChronoLocalDateTime").findMethod("toLocalDate", "")!!
+ val evaluate1 = codebase.getOrCreateClass("android.animation.TypeEvaluator<T>").findMethod("evaluate", "float, T, T")!!
+ val evaluate2 = codebase.getOrCreateClass("android.animation.FloatEvaluator").findMethod("evaluate", "float, java.lang.Number, java.lang.Number")!!
+ val loadInBackground1 = codebase.getOrCreateClass("android.content.AsyncTaskLoader<D>").findMethod("loadInBackground", "")!!
+ val loadInBackground2 = codebase.getOrCreateClass("android.content.CursorLoader").findMethod("loadInBackground", "")!!
+ val next1 = codebase.getOrCreateClass("android.database.CursorJoiner").findMethod("next", "")!!
+ val next2 = codebase.getOrCreateClass("java.util.Iterator<E>").findMethod("next", "")!!
+ val changeParameterType1 = codebase.getOrCreateClass("java.lang.invoke.MethodType").findMethod("changeParameterType", "int, java.lang.Class")!!
+ val changeParameterType2 = codebase.getOrCreateClass("java.lang.invoke.TypeDescriptor.OfMethod").findMethod("changeParameterType", "int, java.lang.invoke.TypeDescriptor.OfField")!!
+ val parameterArray1 = codebase.getOrCreateClass("java.lang.invoke.MethodType").findMethod("parameterArray", "")!!
+ val parameterArray2 = codebase.getOrCreateClass("java.lang.invoke.TypeDescriptor.OfMethod").findMethod("parameterArray", "")!!
+
+ assertTrue(TextClassItem.hasEqualReturnType(toLocalDate1, toLocalDate2))
+ assertTrue(TextClassItem.hasEqualReturnType(evaluate1, evaluate2))
+ assertTrue(TextClassItem.hasEqualReturnType(loadInBackground1, loadInBackground2))
+ assertTrue(TextClassItem.hasEqualReturnType(next1, next2))
+ assertTrue(TextClassItem.hasEqualReturnType(changeParameterType1, changeParameterType2))
+ assertTrue(TextClassItem.hasEqualReturnType(parameterArray1, parameterArray2))
+ }
+
+ @Test
+ fun `test hasEqualReturnType() with equal bounds return types`() {
+ val codebase = ApiFile.parseApi(
+ "test",
+ """
+ package java.lang {
+ public final class Class<T> implements java.lang.reflect.AnnotatedElement {
+ method @Nullable public <A extends java.lang.annotation.Annotation> A getAnnotation(@NonNull Class<A>);
+ }
+ public interface AnnotatedElement {
+ method @Nullable public <T extends java.lang.annotation.Annotation> T getAnnotation(@NonNull Class<T>);
+ }
+ }
+ """.trimIndent(),
+ false
+ )
+
+ val getAnnotation1 = codebase.getOrCreateClass("java.lang.Class<T>").findMethod("getAnnotation", "java.lang.Class")!!
+ val getAnnotation2 = codebase.getOrCreateClass("java.lang.AnnotatedElement").findMethod("getAnnotation", "java.lang.Class")!!
+
+ assertTrue(TextClassItem.hasEqualReturnType(getAnnotation1, getAnnotation2))
+ }
+
+ @Test
+ fun `test hasEqualReturnType() with covariant return types`() {
+ val codebase = ApiFile.parseApi(
+ "test",
+ """
+ package android.widget {
+ public abstract class AdapterView<T extends android.widget.Adapter> extends android.view.ViewGroup {
+ method public abstract T getAdapter();
+ }
+ public abstract class AbsListView extends android.widget.AdapterView<android.widget.ListAdapter> implements android.widget.Filter.FilterListener android.text.TextWatcher android.view.ViewTreeObserver.OnGlobalLayoutListener android.view.ViewTreeObserver.OnTouchModeChangeListener {
+ }
+ public interface ListAdapter extends android.widget.Adapter {
+ }
+ @android.widget.RemoteViews.RemoteView public class ListView extends android.widget.AbsListView {
+ method public android.widget.ListAdapter getAdapter();
+ }
+ }
+ """.trimIndent(),
+ false
+ )
+
+ val getAdapter1 = codebase.getOrCreateClass("android.widget.AdapterView<T extends android.widget.Adapter>").findMethod("getAdapter", "")!!
+ val getAdapter2 = codebase.getOrCreateClass("android.widget.ListView").findMethod("getAdapter", "")!!
+
+ assertTrue(TextClassItem.hasEqualReturnType(getAdapter1, getAdapter2))
+ }
+
+ @Test
+ fun `test equalMethodInClassContext()`() {
+ val codebase = ApiFile.parseApi(
+ "test",
+ """
+ package java.lang {
+ public interface Comparable<T> {
+ method public int compareTo(T);
+ }
+ public final class String implements java.lang.CharSequence java.lang.Comparable<java.lang.String> java.io.Serializable {
+ method public int compareTo(@NonNull String);
+ }
+ }
+ package java.lang.invoke {
+ public final class MethodType implements java.io.Serializable java.lang.invoke.TypeDescriptor.OfMethod<java.lang.Class<?>,java.lang.invoke.MethodType> {
+ method public java.lang.invoke.MethodType insertParameterTypes(int, Class<?>...);
+ }
+ public static interface TypeDescriptor.OfMethod<F extends java.lang.invoke.TypeDescriptor.OfField<F>, M extends java.lang.invoke.TypeDescriptor.OfMethod<F, M>> extends java.lang.invoke.TypeDescriptor {
+ method public M insertParameterTypes(int, F...);
+ }
+ }
+ package android.animation {
+ public interface TypeEvaluator<T> {
+ method public T evaluate(float, T, T);
+ }
+ public class ArgbEvaluator implements android.animation.TypeEvaluator {
+ method public Object evaluate(float, Object, Object);
+ }
+ public class FloatArrayEvaluator implements android.animation.TypeEvaluator<float[]> {
+ method public float[] evaluate(float, float[], float[]);
+ }
+ }
+ """.trimIndent(),
+ false
+ )
+
+ val compareTo1 = codebase.getOrCreateClass("java.lang.Comparable").findMethod("compareTo", "T")!!
+ val compareTo2 = codebase.getOrCreateClass("java.lang.String").findMethod("compareTo", "java.lang.String")!!
+ val insertParameterTypes1 = codebase.getOrCreateClass("java.lang.invoke.MethodType").findMethod("insertParameterTypes", "int, java.lang.Class...")!!
+ val insertParameterTypes2 = codebase.getOrCreateClass("java.lang.invoke.TypeDescriptor.OfMethod").findMethod("insertParameterTypes", "int, F...")!!
+ val evaluate1 = codebase.getOrCreateClass("android.animation.TypeEvaluator<T>").findMethod("evaluate", "float, T, T")!!
+ val evaluate2 = codebase.getOrCreateClass("android.animation.ArgbEvaluator").findMethod("evaluate", "float, java.lang.Object, java.lang.Object")!!
+ val evaluate3 = codebase.getOrCreateClass("android.animation.FloatArrayEvaluator").findMethod("evaluate", "float, float[], float[]")!!
+
+ assertTrue(TextClassItem.equalMethodInClassContext(compareTo1, compareTo2))
+ assertTrue(TextClassItem.equalMethodInClassContext(insertParameterTypes1, insertParameterTypes2))
+ assertTrue(TextClassItem.equalMethodInClassContext(evaluate1, evaluate2))
+ assertTrue(TextClassItem.equalMethodInClassContext(evaluate1, evaluate3))
+ }
+}
diff --git a/src/test/java/com/android/tools/metalava/model/text/TextMethodItemTest.kt b/src/test/java/com/android/tools/metalava/model/text/TextMethodItemTest.kt
new file mode 100644
index 0000000..ab9db49
--- /dev/null
+++ b/src/test/java/com/android/tools/metalava/model/text/TextMethodItemTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.metalava.model.text
+
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+
+class TextMethodItemTest {
+ @Test
+ fun `text method item return type is non-null`() {
+ val codebase = ApiFile.parseApi(
+ "test",
+ """
+ package test.pkg {
+ public class Foo {
+ ctor public Foo();
+ method public void bar();
+ }
+ }
+ """.trimIndent(),
+ false
+ )
+
+ val cls = codebase.findClass("test.pkg.Foo")!!
+ val ctorItem = cls.findMethod("Foo", "")!!
+ val methodItem = cls.findMethod("bar", "")!!
+
+ assertNotNull(ctorItem.returnType())
+ assertEquals("test.pkg.Foo", ctorItem.returnType().toString(), "Return type of the constructor item must be the containing class.")
+ assertNotNull(methodItem.returnType())
+ assertEquals("void", methodItem.returnType().toString(), "Return type of an method item should match the expected value.")
+ }
+}
diff --git a/src/test/java/com/android/tools/metalava/stub/StubsTest.kt b/src/test/java/com/android/tools/metalava/stub/StubsTest.kt
index 59a2881..5fddbfc 100644
--- a/src/test/java/com/android/tools/metalava/stub/StubsTest.kt
+++ b/src/test/java/com/android/tools/metalava/stub/StubsTest.kt
@@ -20,6 +20,7 @@
import com.android.tools.lint.checks.infrastructure.LintDetectorTest.source
import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.metalava.ANDROIDX_NONNULL
import com.android.tools.metalava.ARG_CHECK_API
import com.android.tools.metalava.ARG_EXCLUDE_ALL_ANNOTATIONS
import com.android.tools.metalava.ARG_EXCLUDE_ANNOTATION
@@ -34,14 +35,11 @@
import com.android.tools.metalava.deprecatedForSdkSource
import com.android.tools.metalava.extractRoots
import com.android.tools.metalava.gatherSources
-import com.android.tools.metalava.intDefAnnotationSource
-import com.android.tools.metalava.intRangeAnnotationSource
import com.android.tools.metalava.java
import com.android.tools.metalava.kotlin
import com.android.tools.metalava.libcoreNonNullSource
import com.android.tools.metalava.model.SUPPORT_TYPE_USE_ANNOTATIONS
import com.android.tools.metalava.requiresApiSource
-import com.android.tools.metalava.requiresPermissionSource
import com.android.tools.metalava.restrictToSource
import com.android.tools.metalava.supportParameterName
import com.android.tools.metalava.systemApiSource
@@ -58,30 +56,59 @@
// TODO: test @DocOnly handling
private fun checkStubs(
- @Language("JAVA") source: String,
+ // source is a wrapper for stubFiles. When passing multiple stub Java files to test,
+ // use stubFiles.
+ @Language("JAVA") source: String = "",
+ stubFiles: Array<TestFile> = emptyArray(),
warnings: String? = "",
api: String? = null,
extraArguments: Array<String> = emptyArray(),
docStubs: Boolean = false,
showAnnotations: Array<String> = emptyArray(),
- includeSourceRetentionAnnotations: Boolean = true,
skipEmitPackages: List<String> = listOf("java.lang", "java.util", "java.io"),
format: FileFormat = FileFormat.latest,
- sourceFiles: Array<TestFile>
+ sourceFiles: Array<TestFile> = emptyArray(),
+ signatureSources: Array<String> = emptyArray(),
+ checkTextStubEquivalence: Boolean = false
) {
+ val stubFilesArr = if (source.isNotEmpty()) arrayOf(java(source)) else stubFiles
check(
sourceFiles = sourceFiles,
+ signatureSources = signatureSources,
showAnnotations = showAnnotations,
- stubFiles = arrayOf(java(source)),
+ stubFiles = stubFilesArr,
expectedIssues = warnings,
checkCompilation = true,
api = api,
extraArguments = extraArguments,
docStubs = docStubs,
- includeSourceRetentionAnnotations = includeSourceRetentionAnnotations,
skipEmitPackages = skipEmitPackages,
format = format
)
+ if (checkTextStubEquivalence) {
+ if (stubFilesArr.isEmpty()) {
+ addError("Stub files may not be empty when checkTextStubEquivalence is set to true.")
+ return
+ }
+ if (docStubs) {
+ addError("From-text stub generation is not supported for documentation stub.")
+ return
+ }
+ if (stubFilesArr.any { it !is TestFile.JavaTestFile }) {
+ addError("From-text stub generation is only supported for Java stubs.")
+ return
+ }
+ check(
+ signatureSources = arrayOf(readFile(getApiFile())),
+ showAnnotations = showAnnotations,
+ stubFiles = stubFilesArr,
+ expectedIssues = warnings,
+ checkCompilation = true,
+ extraArguments = arrayOf(*extraArguments, ARG_EXCLUDE_ANNOTATION, ANDROIDX_NONNULL),
+ skipEmitPackages = skipEmitPackages,
+ format = format
+ )
+ }
}
@Test
@@ -265,7 +292,7 @@
package test.pkg;
@SuppressWarnings({"unchecked", "deprecation", "all"})
public class Foo {
- private Foo() { throw new RuntimeException("Stub!"); }
+ Foo() { throw new RuntimeException("Stub!"); }
}
"""
)
@@ -292,7 +319,8 @@
public interface Foo {
public void foo();
}
- """
+ """,
+ checkTextStubEquivalence = true
)
}
@@ -420,6 +448,15 @@
public static final double field10 = Double.NaN;
public static final double field11 = Double.POSITIVE_INFINITY;
+ public static final boolean field12;
+ public static final byte field13;
+ public static final char field14;
+ public static final short field15;
+ public static final int field16;
+ public static final long field17;
+ public static final float field18;
+ public static final double field19;
+
public static final String GOOD_IRI_CHAR = "a-zA-Z0-9\u00a0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef";
public static final char HEX_INPUT = 61184;
}
@@ -445,8 +482,25 @@
public static final java.lang.String field09 = "String with \"escapes\" and \u00a9...";
public static final double field10 = (0.0/0.0);
public static final double field11 = (1.0/0.0);
+ public static final boolean field12;
+ static { field12 = false; }
+ public static final byte field13;
+ static { field13 = 0; }
+ public static final char field14;
+ static { field14 = 0; }
+ public static final short field15;
+ static { field15 = 0; }
+ public static final int field16;
+ static { field16 = 0; }
+ public static final long field17;
+ static { field17 = 0; }
+ public static final float field18;
+ static { field18 = 0; }
+ public static final double field19;
+ static { field19 = 0; }
}
- """
+ """,
+ checkTextStubEquivalence = true
)
}
@@ -688,7 +742,7 @@
@SuppressWarnings("ALL")
public class MoreAsserts {
- public static void assertEquals(String arg0, Set<? extends Object> arg1, Set<? extends Object> arg2) { }
+ public static void assertEquals(String arg1, Set<? extends Object> arg2, Set<? extends Object> arg3) { }
public static void assertEquals(Set<? extends Object> arg1, Set<? extends Object> arg2) { }
}
"""
@@ -700,10 +754,11 @@
@SuppressWarnings({"unchecked", "deprecation", "all"})
public class MoreAsserts {
public MoreAsserts() { throw new RuntimeException("Stub!"); }
- public static void assertEquals(java.lang.String arg0, java.util.Set<?> arg1, java.util.Set<?> arg2) { throw new RuntimeException("Stub!"); }
+ public static void assertEquals(java.lang.String arg1, java.util.Set<?> arg2, java.util.Set<?> arg3) { throw new RuntimeException("Stub!"); }
public static void assertEquals(java.util.Set<?> arg1, java.util.Set<?> arg2) { throw new RuntimeException("Stub!"); }
}
- """
+ """,
+ checkTextStubEquivalence = true
)
}
@@ -866,7 +921,8 @@
public void method2() { throw new RuntimeException("Stub!"); }
public static final java.lang.String CONSTANT = "MyConstant";
}
- """
+ """,
+ checkTextStubEquivalence = true
)
}
@@ -914,7 +970,8 @@
public void other() { throw new RuntimeException("Stub!"); }
public static final java.lang.String CONSTANT = "MyConstant";
}
- """
+ """,
+ checkTextStubEquivalence = true
)
}
@@ -944,7 +1001,8 @@
protected void finalize1() throws java.lang.Throwable { throw new RuntimeException("Stub!"); }
protected void finalize2() throws java.io.IOException, java.lang.IllegalArgumentException { throw new RuntimeException("Stub!"); }
}
- """
+ """,
+ checkTextStubEquivalence = true
)
}
@@ -974,7 +1032,8 @@
public static final int CONSTANT3 = 0; // 0x0
public static final java.lang.String CONSTANT4 = null;
}
- """
+ """,
+ checkTextStubEquivalence = true
)
}
@@ -993,7 +1052,7 @@
}
public static final class IsoFields {
public static final TemporalField DAY_OF_QUARTER = Field.DAY_OF_QUARTER;
- private IsoFields() {
+ IsoFields() {
throw new AssertionError("Not instantiable");
}
@@ -1018,7 +1077,7 @@
public FinalFieldTest() { throw new RuntimeException("Stub!"); }
@SuppressWarnings({"unchecked", "deprecation", "all"})
public static final class IsoFields {
- private IsoFields() { throw new RuntimeException("Stub!"); }
+ IsoFields() { throw new RuntimeException("Stub!"); }
public static final test.pkg.FinalFieldTest.TemporalField DAY_OF_QUARTER;
static { DAY_OF_QUARTER = null; }
}
@@ -1111,57 +1170,84 @@
}
@Test
- fun `Check generating annotation source`() {
+ fun `Check overridden method added for complex hierarchy`() {
checkStubs(
sourceFiles = arrayOf(
java(
"""
- package android.view.View;
- import android.annotation.IntDef;
- import android.annotation.IntRange;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- public class View {
- @SuppressWarnings("all")
- public static class MeasureSpec {
- private static final int MODE_SHIFT = 30;
- private static final int MODE_MASK = 0x3 << MODE_SHIFT;
- /** @hide */
- @SuppressWarnings("all")
- @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
- @Retention(RetentionPolicy.SOURCE)
- public @interface MeasureSpecMode {}
- public static final int UNSPECIFIED = 0 << MODE_SHIFT;
- public static final int EXACTLY = 1 << MODE_SHIFT;
- public static final int AT_MOST = 2 << MODE_SHIFT;
-
- public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
- @MeasureSpecMode int mode) {
- return 0;
- }
- }
- }
- """
- ),
- intDefAnnotationSource,
- intRangeAnnotationSource
- ),
- warnings = "",
- source = """
- package android.view.View;
- @SuppressWarnings({"unchecked", "deprecation", "all"})
- public class View {
- public View() { throw new RuntimeException("Stub!"); }
- @SuppressWarnings({"unchecked", "deprecation", "all"})
- public static class MeasureSpec {
- public MeasureSpec() { throw new RuntimeException("Stub!"); }
- public static int makeMeasureSpec(@androidx.annotation.IntRange(from=0, to=0x40000000 - 1) int size, int mode) { throw new RuntimeException("Stub!"); }
- public static final int AT_MOST = -2147483648; // 0x80000000
- public static final int EXACTLY = 1073741824; // 0x40000000
- public static final int UNSPECIFIED = 0; // 0x0
- }
- }
+ package test.pkg;
+ public final class A extends C implements B<String> {
+ @Override public void method2() { }
+ }
"""
+ ),
+ java(
+ """
+ package test.pkg;
+ public interface B<T> {
+ void method1(T arg1);
+ }
+ """
+ ),
+ java(
+ """
+ package test.pkg;
+ public abstract class C extends D {
+ public abstract void method2();
+ }
+ """
+ ),
+ java(
+ """
+ package test.pkg;
+ public abstract class D implements B<String> {
+ @Override public void method1(String arg1) { }
+ }
+ """
+ )
+ ),
+ stubFiles = arrayOf(
+ java(
+ """
+ package test.pkg;
+ @SuppressWarnings({"unchecked", "deprecation", "all"})
+ public final class A extends test.pkg.C implements test.pkg.B<java.lang.String> {
+ public A() { throw new RuntimeException("Stub!"); }
+ public void method2() { throw new RuntimeException("Stub!"); }
+ }
+ """
+ ),
+ java(
+ """
+ package test.pkg;
+ @SuppressWarnings({"unchecked", "deprecation", "all"})
+ public interface B<T> {
+ public void method1(T arg1);
+ }
+ """
+ ),
+ java(
+ """
+ package test.pkg;
+ @SuppressWarnings({"unchecked", "deprecation", "all"})
+ public abstract class C extends test.pkg.D {
+ public C() { throw new RuntimeException("Stub!"); }
+ public abstract void method2();
+ }
+ """
+ ),
+ java(
+ """
+ package test.pkg;
+ @SuppressWarnings({"unchecked", "deprecation", "all"})
+ public abstract class D implements test.pkg.B<java.lang.String> {
+ public D() { throw new RuntimeException("Stub!"); }
+ public void method1(java.lang.String arg1) { throw new RuntimeException("Stub!"); }
+ }
+ """
+ )
+ ),
+ checkTextStubEquivalence = true
)
}
@@ -1192,58 +1278,6 @@
}
@Test
- fun `Check generating annotation for hidden constants`() {
- checkStubs(
- sourceFiles = arrayOf(
- java(
- """
- package test.pkg;
-
- import android.content.Intent;
- import android.annotation.RequiresPermission;
-
- public abstract class HiddenPermission {
- @RequiresPermission(allOf = {
- android.Manifest.permission.INTERACT_ACROSS_USERS,
- android.Manifest.permission.BROADCAST_STICKY
- })
- public abstract void removeStickyBroadcast(@RequiresPermission Object intent);
- }
- """
- ),
- java(
- """
- package android;
-
- public final class Manifest {
- @SuppressWarnings("JavaDoc")
- public static final class permission {
- public static final String BROADCAST_STICKY = "android.permission.BROADCAST_STICKY";
- /** @SystemApi @hide Allows an application to call APIs that allow it to do interactions
- across the users on the device, using singleton services and
- user-targeted broadcasts. This permission is not available to
- third party applications. */
- public static final String INTERACT_ACROSS_USERS = "android.permission.INTERACT_ACROSS_USERS";
- }
- }
- """
- ),
- requiresPermissionSource
- ),
- warnings = "",
- source = """
- package test.pkg;
- @SuppressWarnings({"unchecked", "deprecation", "all"})
- public abstract class HiddenPermission {
- public HiddenPermission() { throw new RuntimeException("Stub!"); }
- @androidx.annotation.RequiresPermission(allOf={"android.permission.INTERACT_ACROSS_USERS", android.Manifest.permission.BROADCAST_STICKY})
- public abstract void removeStickyBroadcast(@androidx.annotation.RequiresPermission java.lang.Object intent);
- }
- """
- )
- }
-
- @Test
fun `Check generating type parameters in interface list`() {
checkStubs(
format = FileFormat.V2,
@@ -1502,35 +1536,35 @@
@SuppressWarnings("WeakerAccess")
public class Constructors {
public class Parent {
- public Parent(String s, int i, long l, boolean b, short sh) {
+ public Parent(String arg1, int arg2, long arg3, boolean arg4, short arg5) {
}
}
public class Child extends Parent {
- public Child(String s, int i, long l, boolean b, short sh) {
- super(s, i, l, b, sh);
+ public Child(String arg1, int arg2, long arg3, boolean arg4, short arg5) {
+ super(arg1, arg2, arg3, arg4, arg5);
}
- private Child(String s) {
- super(s, 0, 0, false, 0);
+ private Child(String arg1) {
+ super(arg1, 0, 0, false, 0);
}
}
public class Child2 extends Parent {
- Child2(String s) {
- super(s, 0, 0, false, 0);
+ Child2(String arg1) {
+ super(arg1, 0, 0, false, 0);
}
}
public class Child3 extends Child2 {
- private Child3(String s) {
+ private Child3(String arg1) {
super("something");
}
}
public class Child4 extends Parent {
- Child4(String s, HiddenClass hidden) {
- super(s, 0, 0, true, 0);
+ Child4(String arg1, HiddenClass arg2) {
+ super(arg1, 0, 0, true, 0);
}
}
/** @hide */
@@ -1547,7 +1581,7 @@
public Constructors() { throw new RuntimeException("Stub!"); }
@SuppressWarnings({"unchecked", "deprecation", "all"})
public class Child extends test.pkg.Constructors.Parent {
- public Child(java.lang.String s, int i, long l, boolean b, short sh) { super(null, 0, 0, false, (short)0); throw new RuntimeException("Stub!"); }
+ public Child(java.lang.String arg1, int arg2, long arg3, boolean arg4, short arg5) { super(null, 0, 0, false, (short)0); throw new RuntimeException("Stub!"); }
}
@SuppressWarnings({"unchecked", "deprecation", "all"})
public class Child2 extends test.pkg.Constructors.Parent {
@@ -1555,7 +1589,7 @@
}
@SuppressWarnings({"unchecked", "deprecation", "all"})
public class Child3 extends test.pkg.Constructors.Child2 {
- private Child3() { throw new RuntimeException("Stub!"); }
+ Child3() { throw new RuntimeException("Stub!"); }
}
@SuppressWarnings({"unchecked", "deprecation", "all"})
public class Child4 extends test.pkg.Constructors.Parent {
@@ -1563,10 +1597,11 @@
}
@SuppressWarnings({"unchecked", "deprecation", "all"})
public class Parent {
- public Parent(java.lang.String s, int i, long l, boolean b, short sh) { throw new RuntimeException("Stub!"); }
+ public Parent(java.lang.String arg1, int arg2, long arg3, boolean arg4, short arg5) { throw new RuntimeException("Stub!"); }
}
}
- """
+ """,
+ checkTextStubEquivalence = true
)
}
@@ -1636,7 +1671,7 @@
}
@SuppressWarnings({"unchecked", "deprecation", "all"})
public class Child3 extends test.pkg.Constructors.Child2 {
- private Child3() { throw new RuntimeException("Stub!"); }
+ Child3() { throw new RuntimeException("Stub!"); }
}
@SuppressWarnings({"unchecked", "deprecation", "all"})
public class Child4 extends test.pkg.Constructors.Parent {
@@ -1862,6 +1897,53 @@
}
@Test
+ fun `Check resolving override equivalent signatures`() {
+ // getAttributeNamespace in XmlResourceParser does not exist in the intermediate text file created.
+ checkStubs(
+ sourceFiles = arrayOf(
+ java(
+ """
+ package test.pkg;
+ public interface XmlResourceParser extends test.pkg.XmlPullParser, test.pkg.AttributeSet {
+ public void close();
+ String getAttributeNamespace (int arg1);
+ }
+ """
+ ),
+ java(
+ """
+ package test.pkg;
+ public interface XmlPullParser {
+ String getAttributeNamespace (int arg1);
+ }
+ """
+ ),
+ java(
+ """
+ package test.pkg;
+ public interface AttributeSet {
+ default String getAttributeNamespace (int arg1) { }
+ }
+ """
+ )
+ ),
+ stubFiles = arrayOf(
+ java(
+ """
+ package test.pkg;
+ @SuppressWarnings({"unchecked", "deprecation", "all"})
+ public interface XmlResourceParser extends test.pkg.XmlPullParser, test.pkg.AttributeSet {
+ public void close();
+ public java.lang.String getAttributeNamespace(int arg1);
+ }
+ """
+ )
+ ),
+ checkTextStubEquivalence = true
+ )
+ }
+
+ @Test
fun `Rewrite unknown nullability annotations as sdk stubs`() {
check(
format = FileFormat.V2,
@@ -2595,7 +2677,8 @@
public static interface TypeEvaluator<T> {
}
}
- """
+ """,
+ checkTextStubEquivalence = true
)
}
@@ -3354,7 +3437,6 @@
@Test
fun `Annotation metadata in stubs`() {
checkStubs(
- includeSourceRetentionAnnotations = false,
skipEmitPackages = emptyList(),
sourceFiles = arrayOf(
java(
@@ -3406,7 +3488,8 @@
public interface MyInterface {
public void run();
}
- """
+ """,
+ checkTextStubEquivalence = true
)
}
@@ -3490,7 +3573,8 @@
}
"""
)
- )
+ ),
+ docStubs = true
)
}
@@ -3520,7 +3604,6 @@
stubFiles = arrayOf(
java(
"""
- @androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.SUBCLASSES)
package test.pkg;
"""
),
@@ -3635,7 +3718,7 @@
@Deprecated
@test.pkg.MyRuntimeRetentionAnnotation
public class Foo {
- private Foo() { throw new RuntimeException("Stub!"); }
+ Foo() { throw new RuntimeException("Stub!"); }
}
"""
)
@@ -3704,7 +3787,7 @@
@test.pkg.MyClassRetentionAnnotation
@test.pkg.MyRuntimeRetentionAnnotation
public class Foo {
- private Foo() { throw new RuntimeException("Stub!"); }
+ Foo() { throw new RuntimeException("Stub!"); }
@Deprecated
public void bar() { throw new RuntimeException("Stub!"); }
@Deprecated protected int foo;
@@ -4256,7 +4339,7 @@
package test.pkg;
@SuppressWarnings({"unchecked", "deprecation", "all"})
public class Orange {
- private Orange() { throw new RuntimeException("Stub!"); }
+ Orange() { throw new RuntimeException("Stub!"); }
}
"""
),
@@ -4265,7 +4348,7 @@
package test.pkg;
@SuppressWarnings({"unchecked", "deprecation", "all"})
public class Alpha extends test.pkg.Charlie<test.pkg.Orange> {
- private Alpha() { throw new RuntimeException("Stub!"); }
+ Alpha() { throw new RuntimeException("Stub!"); }
}
"""
)
@@ -4588,7 +4671,7 @@
package test.pkg;
@SuppressWarnings({"unchecked", "deprecation", "all"})
public class PublicApi {
- private PublicApi() { throw new RuntimeException("Stub!"); }
+ PublicApi() { throw new RuntimeException("Stub!"); }
/** @deprecated My deprecation reason 1 */
@Deprecated
public static void method1() { throw new RuntimeException("Stub!"); }
@@ -4697,7 +4780,7 @@
package test.pkg;
@SuppressWarnings({"unchecked", "deprecation", "all"})
public class PublicApi2 {
- private PublicApi2() { throw new RuntimeException("Stub!"); }
+ PublicApi2() { throw new RuntimeException("Stub!"); }
public static void method1() { throw new RuntimeException("Stub!"); }
/**
* My docs.
diff --git a/src/testdata/prebuilts-sdk-test/30/ClassAddedInApi30.java b/src/testdata/prebuilts-sdk-test/30/ClassAddedInApi30.java
new file mode 100644
index 0000000..50ea924
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/30/ClassAddedInApi30.java
@@ -0,0 +1,6 @@
+package android.test;
+
+public class ClassAddedInApi30 {
+ private ClassAddedInApi30() {}
+ public void methodAddedInApi30() { throw new RuntimeException("Stub!"); }
+}
diff --git a/src/testdata/prebuilts-sdk-test/30/Object.java b/src/testdata/prebuilts-sdk-test/30/Object.java
new file mode 100644
index 0000000..a673c1a
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/30/Object.java
@@ -0,0 +1,4 @@
+package java.lang;
+
+public class Object {
+}
diff --git a/src/testdata/prebuilts-sdk-test/31/ClassAddedInApi30.java b/src/testdata/prebuilts-sdk-test/31/ClassAddedInApi30.java
new file mode 100644
index 0000000..9cb6703
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/31/ClassAddedInApi30.java
@@ -0,0 +1,7 @@
+package android.test;
+
+public class ClassAddedInApi30 {
+ private ClassAddedInApi30() {}
+ public void methodAddedInApi30() { throw new RuntimeException("Stub!"); }
+ public void methodAddedInApi31() { throw new RuntimeException("Stub!"); }
+}
diff --git a/src/testdata/prebuilts-sdk-test/31/ClassAddedInApi31AndExt2.java b/src/testdata/prebuilts-sdk-test/31/ClassAddedInApi31AndExt2.java
new file mode 100644
index 0000000..772b830
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/31/ClassAddedInApi31AndExt2.java
@@ -0,0 +1,7 @@
+package android.test;
+
+public class ClassAddedInApi31AndExt2 {
+ private ClassAddedInApi31AndExt2() {}
+ public static final int FIELD_ADDED_IN_API_31_AND_EXT_2 = 1;
+ public void methodAddedInApi31AndExt2() { throw new RuntimeException("Stub!"); }
+}
diff --git a/src/testdata/prebuilts-sdk-test/31/ClassAddedInExt1.java b/src/testdata/prebuilts-sdk-test/31/ClassAddedInExt1.java
new file mode 100644
index 0000000..cb9c864
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/31/ClassAddedInExt1.java
@@ -0,0 +1,9 @@
+package android.test;
+
+public class ClassAddedInExt1 {
+ private ClassAddedInExt1() {}
+ public static final int FIELD_ADDED_IN_EXT_1 = 1;
+ public static final int FIELD_ADDED_IN_API_31_AND_EXT_2 = 2;
+ public void methodAddedInExt1() { throw new RuntimeException("Stub!"); }
+ public void methodAddedInApi31AndExt2() { throw new RuntimeException("Stub!"); }
+}
diff --git a/src/testdata/prebuilts-sdk-test/31/Object.java b/src/testdata/prebuilts-sdk-test/31/Object.java
new file mode 100644
index 0000000..a673c1a
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/31/Object.java
@@ -0,0 +1,4 @@
+package java.lang;
+
+public class Object {
+}
diff --git a/src/testdata/prebuilts-sdk-test/README.md b/src/testdata/prebuilts-sdk-test/README.md
new file mode 100644
index 0000000..3e8cf72
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/README.md
@@ -0,0 +1,11 @@
+Sources for building a prebuilts/sdk tree for metalava use in unit tests.
+
+The directory tree and generated jar files will be build as part of the metalava
+unit tests, and placed in $(gettop)/out/metalava/prebuilts/sdk.
+
+The project emulates the following history, in order of earliest to latest event:
+
+- finalized Android API 30 "Android R" (android-30)
+- finalized SDK extensions "R-ext" version 1 (android-30-ext1)
+- finalized Android API 31 "Android S" and SDK extensions "S-ext", version 2, at the same time (android-31-ext2)
+- finalized SDK extensions "S-ext", version 3 (android-31-ext3)
diff --git a/src/testdata/prebuilts-sdk-test/extensions/1/ClassAddedInExt1.java b/src/testdata/prebuilts-sdk-test/extensions/1/ClassAddedInExt1.java
new file mode 100644
index 0000000..aef7e0a
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/extensions/1/ClassAddedInExt1.java
@@ -0,0 +1,7 @@
+package android.test;
+
+public class ClassAddedInExt1 {
+ private ClassAddedInExt1() {}
+ public static final int FIELD_ADDED_IN_EXT_1 = 1;
+ public void methodAddedInExt1() { throw new RuntimeException("Stub!"); }
+}
diff --git a/src/testdata/prebuilts-sdk-test/extensions/2/ClassAddedInApi31AndExt2.java b/src/testdata/prebuilts-sdk-test/extensions/2/ClassAddedInApi31AndExt2.java
new file mode 120000
index 0000000..1d7d592
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/extensions/2/ClassAddedInApi31AndExt2.java
@@ -0,0 +1 @@
+../../31/ClassAddedInApi31AndExt2.java
\ No newline at end of file
diff --git a/src/testdata/prebuilts-sdk-test/extensions/2/ClassAddedInExt1.java b/src/testdata/prebuilts-sdk-test/extensions/2/ClassAddedInExt1.java
new file mode 120000
index 0000000..cb0c486
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/extensions/2/ClassAddedInExt1.java
@@ -0,0 +1 @@
+../../31/ClassAddedInExt1.java
\ No newline at end of file
diff --git a/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInApi31AndExt2.java b/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInApi31AndExt2.java
new file mode 100644
index 0000000..fa31b8e
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInApi31AndExt2.java
@@ -0,0 +1,9 @@
+package android.test;
+
+public class ClassAddedInApi31AndExt2 {
+ private ClassAddedInApi31AndExt2() {}
+ public static final int FIELD_ADDED_IN_API_31_AND_EXT_2 = 1;
+ public static final int FIELD_ADDED_IN_EXT_3 = 2;
+ public void methodAddedInApi31AndExt2() { throw new RuntimeException("Stub!"); }
+ public void methodAddedInExt3() { throw new RuntimeException("Stub!"); }
+}
diff --git a/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInExt1.java b/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInExt1.java
new file mode 100644
index 0000000..c5798d6
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInExt1.java
@@ -0,0 +1,11 @@
+package android.test;
+
+public class ClassAddedInExt1 {
+ private ClassAddedInExt1() {}
+ public static final int FIELD_ADDED_IN_EXT_1 = 1;
+ public static final int FIELD_ADDED_IN_API_31_AND_EXT_2 = 2;
+ public static final int FIELD_ADDED_IN_EXT_3 = 3;
+ public void methodAddedInExt1() { throw new RuntimeException("Stub!"); }
+ public void methodAddedInApi31AndExt2() { throw new RuntimeException("Stub!"); }
+ public void methodAddedInExt3() { throw new RuntimeException("Stub!"); }
+}
diff --git a/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInExt3.java b/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInExt3.java
new file mode 100644
index 0000000..31b58ec
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/extensions/3/ClassAddedInExt3.java
@@ -0,0 +1,7 @@
+package android.test;
+
+public class ClassAddedInExt3 {
+ private ClassAddedInExt3() {}
+ public static final int FIELD_ADDED_IN_EXT_3 = 1;
+ public void methodAddedInExt3() { throw new RuntimeException("Stub!"); }
+}
diff --git a/src/testdata/prebuilts-sdk-test/sdk-extensions-info.xml b/src/testdata/prebuilts-sdk-test/sdk-extensions-info.xml
new file mode 100644
index 0000000..a7598e9
--- /dev/null
+++ b/src/testdata/prebuilts-sdk-test/sdk-extensions-info.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<sdk-extensions-info>
+ <!-- SDK definitions -->
+ <sdk
+ id="30"
+ shortname="R-ext"
+ name="R Extensions"
+ reference="android/os/Build$VERSION_CODES$R" />
+ <sdk
+ id="31"
+ shortname="S-ext"
+ name="S Extensions"
+ reference="android/os/Build$VERSION_CODES$S" />
+
+ <!-- TEST EXTENSION -->
+ <symbol
+ jar="framework-ext"
+ pattern="*"
+ sdks="R-ext,S-ext" />
+</sdk-extensions-info>