diff --git a/.idea/dictionaries/metalava.xml b/.idea/dictionaries/metalava.xml
index bf8e8982a93e77ab643f62705c6437073726c30e..6b14ab559968878c961737867ca04abb40564b1f 100644
--- a/.idea/dictionaries/metalava.xml
+++ b/.idea/dictionaries/metalava.xml
@@ -1,6 +1,7 @@
<component name="ProjectDictionaryState">
<dictionary name="metalava">
<words>
+ <w>androidx</w>
<w>apidocsdir</w>
<w>argnum</w>
<w>bootclasspath</w>
@@ -23,6 +24,7 @@
<w>includeable</w>
<w>inheritdoc</w>
<w>interop</w>
+ <w>jaif</w>
<w>javadocs</w>
<w>jvmstatic</w>
<w>knowntags</w>
diff --git a/.idea/dictionaries/tnorbye.xml b/.idea/dictionaries/tnorbye.xml
deleted file mode 100644
index c86b073dd697bf7ac05393727bdb780034c17751..0000000000000000000000000000000000000000
--- a/.idea/dictionaries/tnorbye.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<component name="ProjectDictionaryState">
- <dictionary name="tnorbye">
- <words>
- <w>androidx</w>
- </words>
- </dictionary>
-</component>
\ No newline at end of file
diff --git a/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt b/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt
index 97b8cf3739cec1b7235b98438fe054c481909eb8..f321ce9170875ef40190422fa7c6601b453c4289 100644
--- a/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt
+++ b/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt
@@ -31,7 +31,6 @@ import com.android.SdkConstants.STRING_DEF_ANNOTATION
import com.android.SdkConstants.TYPE_DEF_FLAG_ATTRIBUTE
import com.android.SdkConstants.TYPE_DEF_VALUE_ATTRIBUTE
import com.android.SdkConstants.VALUE_TRUE
-import com.android.annotations.NonNull
import com.android.tools.lint.annotations.Extractor.ANDROID_INT_DEF
import com.android.tools.lint.annotations.Extractor.ANDROID_NOTNULL
import com.android.tools.lint.annotations.Extractor.ANDROID_NULLABLE
@@ -54,6 +53,7 @@ import com.android.tools.metalava.model.Codebase
import com.android.tools.metalava.model.DefaultAnnotationValue
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.psi.PsiAnnotationItem
import com.android.tools.metalava.model.visitors.ApiVisitor
import com.android.utils.XmlUtils
@@ -80,7 +80,7 @@ class AnnotationsMerger(
mergeAnnotations.forEach { mergeExisting(it) }
}
- private fun mergeExisting(@NonNull file: File) {
+ private fun mergeExisting(file: File) {
if (file.isDirectory) {
val files = file.listFiles()
if (files != null) {
@@ -98,11 +98,18 @@ class AnnotationsMerger(
} catch (e: IOException) {
error("Aborting: I/O problem during transform: " + e.toString())
}
+ } else if (file.path.endsWith(".jaif")) {
+ try {
+ val jaif = Files.asCharSource(file, Charsets.UTF_8).read()
+ mergeAnnotationsJaif(file.path, jaif)
+ } catch (e: IOException) {
+ error("Aborting: I/O problem during transform: " + e.toString())
+ }
}
}
}
- private fun mergeFromJar(@NonNull jar: File) {
+ private fun mergeFromJar(jar: File) {
// Reads in an existing annotations jar and merges in entries found there
// with the annotations analyzed from source.
var zis: JarInputStream? = null
@@ -129,7 +136,7 @@ class AnnotationsMerger(
}
}
- private fun mergeAnnotationsXml(@NonNull path: String, @NonNull xml: String) {
+ private fun mergeAnnotationsXml(path: String, xml: String) {
try {
val document = XmlUtils.parseDocument(xml, false)
mergeDocument(document)
@@ -145,6 +152,103 @@ class AnnotationsMerger(
}
}
+ private fun mergeAnnotationsJaif(path: String, jaif: String) {
+ var pkgItem: PackageItem? = null
+ var clsItem: ClassItem? = null
+ var methodItem: MethodItem? = null
+ var curr: Item? = null
+
+ for (rawLine in jaif.split("\n")) {
+ val line = rawLine.trim()
+ if (line.isEmpty()) {
+ continue
+ }
+ if (line.startsWith("//")) {
+ continue
+ }
+ if (line.startsWith("package ")) {
+ val pkg = line.substring("package ".length, line.length - 1)
+ pkgItem = codebase.findPackage(pkg)
+ curr = pkgItem
+ } else if (line.startsWith("class ")) {
+ val cls = line.substring("class ".length, line.length - 1)
+ clsItem = if (pkgItem != null)
+ codebase.findClass(pkgItem.qualifiedName() + "." + cls)
+ else
+ null
+ curr = clsItem
+ } else if (line.startsWith("annotation ")) {
+ val cls = line.substring("annotation ".length, line.length - 1)
+ clsItem = if (pkgItem != null)
+ codebase.findClass(pkgItem.qualifiedName() + "." + cls)
+ else
+ null
+ curr = clsItem
+ } else if (line.startsWith("method ")) {
+ val method = line.substring("method ".length, line.length - 1)
+ methodItem = null
+ if (clsItem != null) {
+ val index = method.indexOf('(')
+ if (index != -1) {
+ val name = method.substring(0, index)
+ val desc = method.substring(index)
+ methodItem = clsItem.findMethodByDesc(name, desc, true, true)
+ }
+ }
+ curr = methodItem
+ } else if (line.startsWith("field ")) {
+ val field = line.substring("field ".length, line.length - 1)
+ val fieldItem = clsItem?.findField(field, true, true)
+ curr = fieldItem
+ } else if (line.startsWith("parameter #")) {
+ val parameterIndex = line.substring("parameter #".length, line.length - 1).toInt()
+ val parameterItem = if (methodItem != null) {
+ methodItem.parameters()[parameterIndex]
+ } else {
+ null
+ }
+ curr = parameterItem
+ } else if (line.startsWith("type: ")) {
+ val typeAnnotation = line.substring("type: ".length)
+ if (curr != null) {
+ mergeJaifAnnotation(path, curr, typeAnnotation)
+ }
+ } else if (line.startsWith("return: ")) {
+ val annotation = line.substring("return: ".length)
+ if (methodItem != null) {
+ mergeJaifAnnotation(path, methodItem, annotation)
+ }
+ } else if (line.startsWith("inner-type")) {
+ warning("$path: Skipping inner-type annotations for now ($line)")
+ } else if (line.startsWith("int ")) {
+ // warning("Skipping int attribute definitions for annotations now ($line)")
+ }
+ }
+ }
+
+ private fun mergeJaifAnnotation(
+ path: String,
+ item: Item,
+ annotationSource: String
+ ) {
+ if (annotationSource.isEmpty()) {
+ return
+ }
+
+ if (annotationSource.contains("(")) {
+ warning("$path: Can't merge complex annotations from jaif yet: $annotationSource")
+ return
+ }
+ val originalName = annotationSource.substring(1) // remove "@"
+ val qualifiedName = AnnotationItem.mapName(codebase, originalName) ?: originalName
+ if (hasNullnessConflicts(item, qualifiedName)) {
+ return
+ }
+
+ val annotationItem = codebase.createAnnotation("@$qualifiedName")
+ item.mutableModifiers().addAnnotation(annotationItem)
+ }
+
internal fun error(message: String) {
// TODO: Integrate with metalava error facility
options.stderr.println("Error: $message")
@@ -163,7 +267,7 @@ class AnnotationsMerger(
"(\\S+) (\\S+|((.*)\\s+)?(\\S+)\\((.*)\\)( \\d+)?)"
)
- private fun mergeDocument(@NonNull document: Document) {
+ private fun mergeDocument(document: Document) {
val root = document.documentElement
val rootTag = root.tagName
@@ -324,39 +428,44 @@ class AnnotationsMerger(
return qualifiedName
}
- private fun mergeAnnotations(xmlElement: Element, item: Item): Int {
- var count = 0
-
+ private fun mergeAnnotations(xmlElement: Element, item: Item) {
loop@ for (annotationElement in getChildren(xmlElement)) {
val originalName = getAnnotationName(annotationElement)
val qualifiedName = AnnotationItem.mapName(codebase, originalName) ?: originalName
- var haveNullable = false
- var haveNotNull = false
- for (existing in item.modifiers.annotations()) {
- val name = existing.qualifiedName() ?: continue
- if (isNonNull(name)) {
- haveNotNull = true
- }
- if (isNullable(name)) {
- haveNullable = true
- }
- if (name == qualifiedName) {
- continue@loop
- }
- }
-
- // Make sure we don't have a conflict between nullable and not nullable
- if (isNonNull(qualifiedName) && haveNullable || isNullable(qualifiedName) && haveNotNull) {
- warning("Found both @Nullable and @NonNull after import for $item")
- continue
+ if (hasNullnessConflicts(item, qualifiedName)) {
+ continue@loop
}
val annotationItem = createAnnotation(annotationElement) ?: continue
item.mutableModifiers().addAnnotation(annotationItem)
- count++
}
+ }
- return count
+ private fun hasNullnessConflicts(
+ item: Item,
+ qualifiedName: String
+ ): Boolean {
+ var haveNullable = false
+ var haveNotNull = false
+ for (existing in item.modifiers.annotations()) {
+ val name = existing.qualifiedName() ?: continue
+ if (isNonNull(name)) {
+ haveNotNull = true
+ }
+ if (isNullable(name)) {
+ haveNullable = true
+ }
+ if (name == qualifiedName) {
+ return true
+ }
+ }
+
+ // Make sure we don't have a conflict between nullable and not nullable
+ if (isNonNull(qualifiedName) && haveNullable || isNullable(qualifiedName) && haveNotNull) {
+ warning("Found both @Nullable and @NonNull after import for $item")
+ return true
+ }
+ return false
}
/** Reads in annotation data from an XML item (using IntelliJ IDE's external annotations XML format) and
@@ -561,8 +670,7 @@ class AnnotationsMerger(
name == SUPPORT_NULLABLE
}
- @NonNull
- private fun unescapeXml(@NonNull escaped: String): String {
+ private fun unescapeXml(escaped: String): String {
var workingString = escaped.replace(QUOT_ENTITY, "\"")
workingString = workingString.replace(LT_ENTITY, "<")
workingString = workingString.replace(GT_ENTITY, ">")
diff --git a/src/main/java/com/android/tools/metalava/Driver.kt b/src/main/java/com/android/tools/metalava/Driver.kt
index 20a0c3d30c05e9a83aae55fdcb76f4905c7851a0..820d69445ab2facf95a2559d4eec83effe484401 100644
--- a/src/main/java/com/android/tools/metalava/Driver.kt
+++ b/src/main/java/com/android/tools/metalava/Driver.kt
@@ -185,6 +185,23 @@ private fun processFlags() {
// Generate the documentation stubs *before* we migrate nullness information.
options.docStubsDir?.let { createStubFiles(it, codebase, docStubs = true, writeStubList = true) }
+ val currentApiFile = options.currentApi
+ if (currentApiFile != null && options.checkCompatibility) {
+ val current =
+ if (currentApiFile.path.endsWith(SdkConstants.DOT_JAR)) {
+ loadFromJarFile(currentApiFile)
+ } else {
+ loadFromSignatureFiles(
+ currentApiFile, options.inputKotlinStyleNulls,
+ supportsStagedNullability = true
+ )
+ }
+
+ // If configured, compares the new API with the previous API and reports
+ // any incompatibilities.
+ CompatibilityCheck.checkCompatibility(codebase, current)
+ }
+
val previousApiFile = options.previousApi
if (previousApiFile != null) {
val previous =
@@ -199,7 +216,7 @@ private fun processFlags() {
// If configured, compares the new API with the previous API and reports
// any incompatibilities.
- if (options.checkCompatibility) {
+ if (options.checkCompatibility && options.currentApi == null) { // otherwise checked against currentApi above
CompatibilityCheck.checkCompatibility(codebase, previous)
}
@@ -224,6 +241,17 @@ private fun processFlags() {
}
}
+ options.dexApiFile?.let { apiFile ->
+ val apiFilter = FilterPredicate(ApiPredicate(codebase))
+ val memberIsNotCloned: Predicate<Item> = Predicate { !it.isCloned() }
+ val apiReference = ApiPredicate(codebase, ignoreShown = true)
+ val dexApiEmit = memberIsNotCloned.and(apiFilter)
+
+ createReportFile(
+ codebase, apiFile, "DEX API"
+ ) { printWriter -> DexApiWriter(printWriter, dexApiEmit, apiReference) }
+ }
+
options.removedApiFile?.let { apiFile ->
val unfiltered = codebase.original ?: codebase
@@ -543,7 +571,6 @@ private fun ensurePsiFileCapacity() {
private fun extractAnnotations(codebase: Codebase, file: File) {
val localTimer = Stopwatch.createStarted()
- val units = codebase.units
options.externalAnnotations?.let { outputFile ->
@Suppress("UNCHECKED_CAST")
diff --git a/src/main/java/com/android/tools/metalava/KotlinInteropChecks.kt b/src/main/java/com/android/tools/metalava/KotlinInteropChecks.kt
index e7b621c96d3fbf62c207d32191e3c4154dfd8695..7af08d1cf3deeb0e91e1201a4eca58650e0f18d7 100644
--- a/src/main/java/com/android/tools/metalava/KotlinInteropChecks.kt
+++ b/src/main/java/com/android/tools/metalava/KotlinInteropChecks.kt
@@ -16,7 +16,6 @@
package com.android.tools.metalava
-import com.android.annotations.NonNull
import com.android.tools.metalava.doclava1.Errors
import com.android.tools.metalava.model.Codebase
import com.android.tools.metalava.model.FieldItem
@@ -346,7 +345,7 @@ class KotlinInteropChecks {
}
/** Returns true if the given string is a reserved Java keyword */
- fun isJavaKeyword(@NonNull keyword: String): Boolean {
+ fun isJavaKeyword(keyword: String): Boolean {
// TODO when we built on top of IDEA core replace this with
// JavaLexer.isKeyword(candidate, LanguageLevel.JDK_1_5)
when (keyword) {
diff --git a/src/main/java/com/android/tools/metalava/Options.kt b/src/main/java/com/android/tools/metalava/Options.kt
index 1d1c8104f136866ad3248eedb365a9976482dd78..3f4df5928693bfe05c1b228adf8c4b657f669a47 100644
--- a/src/main/java/com/android/tools/metalava/Options.kt
+++ b/src/main/java/com/android/tools/metalava/Options.kt
@@ -47,6 +47,7 @@ private const val ARG_SOURCE_PATH = "--source-path"
private const val ARG_SOURCE_FILES = "--source-files"
private const val ARG_API = "--api"
private const val ARG_PRIVATE_API = "--private-api"
+private const val ARG_DEX_API = "--dex-api"
private const val ARG_PRIVATE_DEX_API = "--private-dex-api"
private const val ARG_SDK_VALUES = "--sdk-values"
private const val ARG_REMOVED_API = "--removed-api"
@@ -63,6 +64,7 @@ private const val ARG_EXCLUDE_ANNOTATIONS = "--exclude-annotations"
private const val ARG_HIDE_PACKAGE = "--hide-package"
private const val ARG_MANIFEST = "--manifest"
private const val ARG_PREVIOUS_API = "--previous-api"
+private const val ARG_CURRENT_API = "--current-api"
private const val ARG_MIGRATE_NULLNESS = "--migrate-nullness"
private const val ARG_CHECK_COMPATIBILITY = "--check-compatibility"
private const val ARG_INPUT_KOTLIN_NULLS = "--input-kotlin-nulls"
@@ -239,6 +241,9 @@ class Options(
/** If set, a file to write the private API file to. Corresponds to the --private-api/-privateApi flag. */
var privateApiFile: File? = null
+ /** If set, a file to write the DEX signatures to. Corresponds to --dex-api. */
+ var dexApiFile: File? = null
+
/** If set, a file to write the private DEX signatures to. Corresponds to --private-dex-api. */
var privateDexApiFile: File? = null
@@ -267,11 +272,16 @@ class Options(
var generateAnnotations = true
/**
- * A signature file for the previous version of this API (for compatibility checks, nullness
- * migration, etc.)
+ * A signature file for the previous version of this API (for nullness
+ * migration, possibly for compatibility checking (if [currentApi] is not defined), etc.)
*/
var previousApi: File? = null
+ /**
+ * A signature file for the current version of this API (for compatibility checks).
+ */
+ var currentApi: File? = null
+
/** Whether we should check API compatibility based on the previous API in [previousApi] */
var checkCompatibility: Boolean = false
@@ -373,7 +383,6 @@ class Options(
stdout.println()
stdout.flush()
- val apiFilters = mutableListOf<File>()
var androidJarPatterns: MutableList<String>? = null
var currentCodeName: String? = null
var currentJar: File? = null
@@ -431,6 +440,7 @@ class Options(
"-sdkvalues", ARG_SDK_VALUES -> sdkValueDir = stringToNewDir(getValue(args, ++index))
ARG_API, "-api" -> apiFile = stringToNewFile(getValue(args, ++index))
+ ARG_DEX_API, "-dexApi" -> dexApiFile = stringToNewFile(getValue(args, ++index))
ARG_PRIVATE_API, "-privateApi" -> privateApiFile = stringToNewFile(getValue(args, ++index))
ARG_PRIVATE_DEX_API, "-privateDexApi" -> privateDexApiFile = stringToNewFile(getValue(args, ++index))
@@ -509,6 +519,7 @@ class Options(
ARG_EXTRACT_ANNOTATIONS -> externalAnnotations = stringToNewFile(getValue(args, ++index))
ARG_PREVIOUS_API -> previousApi = stringToExistingFile(getValue(args, ++index))
+ ARG_CURRENT_API -> currentApi = stringToExistingFile(getValue(args, ++index))
ARG_MIGRATE_NULLNESS -> migrateNulls = true
@@ -889,8 +900,8 @@ class Options(
/** Makes sure that the flag combinations make sense */
private fun checkFlagConsistency() {
- if (checkCompatibility && previousApi == null) {
- throw DriverException(stderr = "$ARG_CHECK_COMPATIBILITY requires $ARG_PREVIOUS_API")
+ if (checkCompatibility && currentApi == null && previousApi == null) {
+ throw DriverException(stderr = "$ARG_CHECK_COMPATIBILITY requires $ARG_CURRENT_API")
}
if (migrateNulls && previousApi == null) {
@@ -1137,7 +1148,8 @@ class Options(
"source files",
"$ARG_MERGE_ANNOTATIONS <file>", "An external annotations file (using IntelliJ's external " +
- "annotations database format) to merge and overlay the sources",
+ "annotations database format) to merge and overlay the sources. A subset of .jaif files " +
+ "is also supported.",
"$ARG_INPUT_API_JAR <file>", "A .jar file to read APIs from directly",
@@ -1161,6 +1173,7 @@ class Options(
// TODO: Document --show-annotation!
"$ARG_API <file>", "Generate a signature descriptor file",
"$ARG_PRIVATE_API <file>", "Generate a signature descriptor file listing the exact private APIs",
+ "$ARG_DEX_API <file>", "Generate a DEX signature descriptor file listing the APIs",
"$ARG_PRIVATE_DEX_API <file>", "Generate a DEX signature descriptor file listing the exact private APIs",
"$ARG_REMOVED_API <file>", "Generate a signature descriptor file for APIs that have been removed",
"$ARG_OUTPUT_KOTLIN_NULLS[=yes|no]", "Controls whether nullness annotations should be formatted as " +
@@ -1203,6 +1216,9 @@ class Options(
ARG_CHECK_COMPATIBILITY, "Check compatibility with the previous API",
ARG_CHECK_KOTLIN_INTEROP, "Check API intended to be used from both Kotlin and Java for interoperability " +
"issues",
+ "$ARG_CURRENT_API <signature file>", "A signature file for the current version of this " +
+ "API to check compatibility with. If not specified, $ARG_PREVIOUS_API will be used " +
+ "instead.",
ARG_MIGRATE_NULLNESS, "Compare nullness information with the previous API and mark newly " +
"annotated APIs as under migration.",
ARG_WARNINGS_AS_ERRORS, "Promote all warnings to errors",
diff --git a/src/main/java/com/android/tools/metalava/doclava1/TextCodebase.kt b/src/main/java/com/android/tools/metalava/doclava1/TextCodebase.kt
index 0e73a535f98bfb18ccb45c18544ac328dd0f98ec..2b66cf62cd5be635e168ec1403a0c78cec76f358 100644
--- a/src/main/java/com/android/tools/metalava/doclava1/TextCodebase.kt
+++ b/src/main/java/com/android/tools/metalava/doclava1/TextCodebase.kt
@@ -16,7 +16,6 @@
package com.android.tools.metalava.doclava1
-import com.android.annotations.NonNull
import com.android.tools.metalava.CodebaseComparator
import com.android.tools.metalava.ComparisonVisitor
import com.android.tools.metalava.JAVA_LANG_ANNOTATION
@@ -70,7 +69,7 @@ class TextCodebase : DefaultCodebase() {
return mPackages.size
}
- override fun findClass(@NonNull className: String): TextClassItem? {
+ override fun findClass(className: String): TextClassItem? {
return mAllClasses[className]
}
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 68fbab0b264d873ebf50769d395ef3acf176bd60..8c31c8eef855233fc6e27c6810863cea8b3b03b3 100644
--- a/src/main/java/com/android/tools/metalava/model/ClassItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/ClassItem.kt
@@ -387,6 +387,37 @@ interface ClassItem : Item {
return null
}
+ /** Finds a given method in this class matching the VM name signature */
+ fun findMethodByDesc(
+ name: String,
+ desc: String,
+ includeSuperClasses: Boolean = false,
+ includeInterfaces: Boolean = false
+ ): MethodItem? {
+ if (desc.startsWith("<init>")) {
+ constructors().asSequence()
+ .filter { it.internalDesc() == desc }
+ .forEach { return it }
+ return null
+ } else {
+ methods().asSequence()
+ .filter { it.name() == name && it.internalDesc() == desc }
+ .forEach { return it }
+ }
+
+ if (includeSuperClasses) {
+ superClass()?.findMethodByDesc(name, desc, true, includeInterfaces)?.let { return it }
+ }
+
+ if (includeInterfaces) {
+ for (itf in interfaceTypes()) {
+ val cls = itf.asClass() ?: continue
+ cls.findMethodByDesc(name, desc, includeSuperClasses, true)?.let { return it }
+ }
+ }
+ return null
+ }
+
fun findConstructor(template: ConstructorItem): ConstructorItem? {
constructors().asSequence()
.filter { it.matches(template) }
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 7740930ed8f99898e5ca77e25a7372cc388a6987..03eb3878747e0a263a4b108aa8c867128c145ba9 100644
--- a/src/main/java/com/android/tools/metalava/model/MethodItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/MethodItem.kt
@@ -42,7 +42,7 @@ interface MethodItem : MemberItem {
* e.g. for the method "void create(int x, int y)" the internal name of
* the constructor is "create" and the desc is "(II)V"
*/
- fun internalDesc(voidConstructorTypes: Boolean): String {
+ fun internalDesc(voidConstructorTypes: Boolean = false): String {
val sb = StringBuilder()
sb.append("(")
diff --git a/src/test/java/com/android/tools/metalava/AnnotationsMergerTest.kt b/src/test/java/com/android/tools/metalava/AnnotationsMergerTest.kt
index f9472a4365d3af0a954ef773c48784602d0cb8a7..3f077c9cd33cf5dc67b54df5f9213fdca31e5095 100644
--- a/src/test/java/com/android/tools/metalava/AnnotationsMergerTest.kt
+++ b/src/test/java/com/android/tools/metalava/AnnotationsMergerTest.kt
@@ -131,4 +131,43 @@ class AnnotationsMergerTest : DriverTest() {
"""
)
}
+
+ @Test
+ fun `Merge jaif files`() {
+ check(
+ sourceFiles = *arrayOf(
+ java(
+ """
+ package test.pkg;
+
+ public interface Appendable {
+ Appendable append(CharSequence csq) throws IOException;
+ }
+ """
+ )
+ ),
+ compatibilityMode = false,
+ outputKotlinStyleNulls = false,
+ omitCommonPackages = false,
+ mergeJaifAnnotations = """
+ //
+ // Copyright (C) 2017 The Android Open Source Project
+ //
+ package test.pkg:
+ class Appendable:
+ method append(Ljava/lang/CharSequence;)Ltest/pkg/Appendable;:
+ parameter #0:
+ type: @libcore.util.Nullable
+ // Is expected to return self
+ return: @libcore.util.NonNull
+ """,
+ api = """
+ package test.pkg {
+ public interface Appendable {
+ method @androidx.annotation.NonNull public test.pkg.Appendable append(@androidx.annotation.Nullable java.lang.CharSequence);
+ }
+ }
+ """
+ )
+ }
}
diff --git a/src/test/java/com/android/tools/metalava/ApiFileTest.kt b/src/test/java/com/android/tools/metalava/ApiFileTest.kt
index 9643b997650c0f2b7564aad2748e47c3bae48cf3..2659f82095ea83c830ca8fe82f128f9d605ea95f 100644
--- a/src/test/java/com/android/tools/metalava/ApiFileTest.kt
+++ b/src/test/java/com/android/tools/metalava/ApiFileTest.kt
@@ -2027,7 +2027,15 @@ class ApiFileTest : DriverTest() {
ctor public Parent();
}
}
- """
+ """,
+ dexApi = """
+ Ltest/pkg/Child;
+ Ltest/pkg/Child;-><init>()V
+ Ltest/pkg/Child;->toString()Ljava/lang/String;
+ Ltest/pkg/Parent;
+ Ltest/pkg/Parent;-><init>()V
+ Ltest/pkg/Parent;->toString()Ljava/lang/String;
+ """
)
}
diff --git a/src/test/java/com/android/tools/metalava/DriverTest.kt b/src/test/java/com/android/tools/metalava/DriverTest.kt
index da7a913041d4f04ca675e1de6845deecc51737cf..69905adcdeb53d20419142a0e45567d97a7190ba 100644
--- a/src/test/java/com/android/tools/metalava/DriverTest.kt
+++ b/src/test/java/com/android/tools/metalava/DriverTest.kt
@@ -133,6 +133,8 @@ abstract class DriverTest {
privateApi: String? = null,
/** The private DEX API (corresponds to --private-dex-api) */
privateDexApi: String? = null,
+ /** The DEX API (corresponds to --dex-api) */
+ dexApi: String? = null,
/** Expected stubs (corresponds to --stubs) */
@Language("JAVA") stubs: Array<String> = emptyArray(),
/** Stub source file list generated */
@@ -153,6 +155,8 @@ abstract class DriverTest {
checkCompilation: Boolean = false,
/** Annotations to merge in */
@Language("XML") mergeAnnotations: String? = null,
+ /** Annotations to merge in */
+ @Language("TEXT") mergeJaifAnnotations: String? = null,
/** An optional API signature file content to load **instead** of Java/Kotlin source files */
@Language("TEXT") signatureSource: String? = null,
/** An optional API jar file content to load **instead** of Java/Kotlin source files */
@@ -223,6 +227,12 @@ abstract class DriverTest {
"annotations output in doclava1"
)
}
+ if (compatibilityMode && mergeJaifAnnotations != null) {
+ fail(
+ "Can't specify both compatibilityMode and mergeJaifAnnotations: there were no " +
+ "annotations output in doclava1"
+ )
+ }
Errors.resetLevels()
@@ -280,6 +290,14 @@ abstract class DriverTest {
emptyArray()
}
+ val jaifAnnotationsArgs = if (mergeJaifAnnotations != null) {
+ val merged = File(project, "merged-annotations.jaif")
+ Files.asCharSink(merged, Charsets.UTF_8).write(mergeJaifAnnotations.trimIndent())
+ arrayOf("--merge-annotations", merged.path)
+ } else {
+ emptyArray()
+ }
+
val previousApiFile = if (previousApi != null) {
val prevApiJar = File(previousApi)
if (prevApiJar.isFile) {
@@ -394,7 +412,7 @@ abstract class DriverTest {
var apiFile: File? = null
val apiArgs = if (api != null) {
- apiFile = temporaryFolder.newFile("api.txt")
+ apiFile = temporaryFolder.newFile("public-api.txt")
arrayOf("--api", apiFile.path)
} else {
emptyArray()
@@ -416,6 +434,14 @@ abstract class DriverTest {
emptyArray()
}
+ var dexApiFile: File? = null
+ val dexApiArgs = if (dexApi != null) {
+ dexApiFile = temporaryFolder.newFile("public-dex.txt")
+ arrayOf("--dex-api", dexApiFile.path)
+ } else {
+ emptyArray()
+ }
+
var privateDexApiFile: File? = null
val privateDexApiArgs = if (privateDexApi != null) {
privateDexApiFile = temporaryFolder.newFile("private-dex.txt")
@@ -543,6 +569,7 @@ abstract class DriverTest {
*apiArgs,
*exactApiArgs,
*privateApiArgs,
+ *dexApiArgs,
*privateDexApiArgs,
*stubsArgs,
*stubsSourceListArgs,
@@ -553,6 +580,7 @@ abstract class DriverTest {
*coverageStats,
*quiet,
*mergeAnnotationsArgs,
+ *jaifAnnotationsArgs,
*previousApiArgs,
*migrateNullsArguments,
*checkCompatibilityArguments,
@@ -617,6 +645,15 @@ abstract class DriverTest {
assertEquals(stripComments(privateApi, stripLineComments = false).trimIndent(), expectedText)
}
+ if (dexApi != null && dexApiFile != null) {
+ assertTrue(
+ "${dexApiFile.path} does not exist even though --dex-api was used",
+ dexApiFile.exists()
+ )
+ val expectedText = readFile(dexApiFile, stripBlankLines, trim)
+ assertEquals(stripComments(dexApi, stripLineComments = false).trimIndent(), expectedText)
+ }
+
if (privateDexApi != null && privateDexApiFile != null) {
assertTrue(
"${privateDexApiFile.path} does not exist even though --private-dex-api was used",
@@ -892,6 +929,29 @@ abstract class DriverTest {
showUnannotated = showUnannotated
)
}
+
+ if (CHECK_OLD_DOCLAVA_TOO && checkDoclava1 && signatureSource == null &&
+ dexApi != null && dexApiFile != null
+ ) {
+ dexApiFile.delete()
+ checkSignaturesWithDoclava1(
+ api = dexApi,
+ argument = "-dexApi",
+ output = dexApiFile,
+ expected = dexApiFile,
+ sourceList = sourceList,
+ sourcePath = sourcePath,
+ packages = packages,
+ androidJar = androidJar,
+ trim = trim,
+ stripBlankLines = stripBlankLines,
+ showAnnotationArgs = showAnnotationArguments,
+ stubImportPackages = importedPackages,
+ // Workaround: -dexApi is a no-op if you don't also provide -api
+ extraArguments = arrayOf("-api", File(dexApiFile.parentFile, "dummy-api.txt").path),
+ showUnannotated = showUnannotated
+ )
+ }
}
/** Checks that the given zip annotations file contains the given XML package contents */
diff --git a/src/test/java/com/android/tools/metalava/OptionsTest.kt b/src/test/java/com/android/tools/metalava/OptionsTest.kt
index 676449b87686ec6d1dfaaa34551596fd7e7df4d6..c61501a95fe1c4e62f036392715cb336b3f6a0fa 100644
--- a/src/test/java/com/android/tools/metalava/OptionsTest.kt
+++ b/src/test/java/com/android/tools/metalava/OptionsTest.kt
@@ -53,7 +53,8 @@ API sources:
when parsing the source files
--merge-annotations <file> An external annotations file (using IntelliJ's
external annotations database format) to merge and
- overlay the sources
+ overlay the sources. A subset of .jaif files is
+ also supported.
--input-api-jar <file> A .jar file to read APIs from directly
--manifest <file> A manifest file, used to for check permissions to
cross check APIs
@@ -78,6 +79,8 @@ Extracting Signature Files:
--api <file> Generate a signature descriptor file
--private-api <file> Generate a signature descriptor file listing the
exact private APIs
+--dex-api <file> Generate a DEX signature descriptor file listing
+ the APIs
--private-dex-api <file> Generate a DEX signature descriptor file listing
the exact private APIs
--removed-api <file> Generate a signature descriptor file for APIs that
@@ -135,6 +138,9 @@ Diffs and Checks:
--check-compatibility Check compatibility with the previous API
--check-kotlin-interop Check API intended to be used from both Kotlin and
Java for interoperability issues
+--current-api <signature file> A signature file for the current version of this
+ API to check compatibility with. If not specified,
+ --previous-api will be used instead.
--migrate-nullness Compare nullness information with the previous API
and mark newly annotated APIs as under migration.
--warnings-as-errors Promote all warnings to errors