diff --git a/README.md b/README.md
index ed88779e0a690a64cbb4b9952c7830009383f77e..2034bcca2351a9d10abac04ffc5f9f58a0e36225 100644
--- a/README.md
+++ b/README.md
@@ -180,6 +180,10 @@ example, you'll see something like this (unless running with --quiet) :
* API Lint: Metalava can optionally (with --api-lint) run a series of additional
checks on the public API in the codebase and flag issues that are discouraged
or forbidden by the Android API Council; there are currently around 80 checks.
+ Some of these take advantage of looking at the source code which wasn't
+ possible with the signature-file based Python version; for example, it looks
+ inside method bodies to see if you're synchronizing on this or the current
+ class, which is forbidden.
* Baselines: Metalava can report all of its issues into a "baseline" file, which
records the current set of issues. From that point forward, when metalava
@@ -288,6 +292,12 @@ Top referenced un-annotated members:
android.jar files themselves to ensure that it computes the exact available
SDK data for each API level.)
+* Misc other features. For example, if you use the @VisibleForTesting annotation
+ from the support library, where you can express the intended visibility if the
+ method had not required visibility for testing, then metalava will treat that
+ method using the intended visibility instead when generating signature files
+ and stubs.
+
## Architecture & Implementation
Metalava is implemented on top of IntelliJ parsing APIs (PSI and UAST). However,
diff --git a/src/main/java/com/android/tools/metalava/Baseline.kt b/src/main/java/com/android/tools/metalava/Baseline.kt
index 6a865916872cd7324e9ab5cd1da1a494ca53921d..335cee4abfa1c381ef11604c0ab5b20e9ed537f4 100644
--- a/src/main/java/com/android/tools/metalava/Baseline.kt
+++ b/src/main/java/com/android/tools/metalava/Baseline.kt
@@ -41,6 +41,7 @@ const val DEFAULT_BASELINE_NAME = "baseline.txt"
class Baseline(
val file: File?,
var updateFile: File?,
+ var merge: Boolean = false,
private var headerComment: String = "",
/**
* Whether, when updating the baseline, we should fail the build if the main baseline does not
@@ -54,7 +55,7 @@ class Baseline(
private val map = HashMap<Errors.Error, MutableMap<String, String>>()
init {
- if (file?.isFile == true && !silentUpdate) {
+ if (file?.isFile == true && (!silentUpdate || merge)) {
// We've set a baseline for a nonexistent file: read it
read()
}
@@ -180,33 +181,37 @@ class Baseline(
private fun read() {
val file = this.file ?: return
- file.readLines(Charsets.UTF_8).forEach { line ->
- if (!(line.startsWith("//") || line.startsWith("#") || line.isBlank() || line.startsWith(" "))) {
- val idEnd = line.indexOf(':')
- val elementEnd = line.indexOf(':', idEnd + 1)
- if (idEnd == -1 || elementEnd == -1) {
- println("Invalid metalava baseline format: $line")
- }
- val errorId = line.substring(0, idEnd).trim()
- val elementId = line.substring(idEnd + 2, elementEnd).trim()
+ val lines = file.readLines(Charsets.UTF_8)
+ for (i in 0 until lines.size - 1) {
+ val line = lines[i]
+ if (line.startsWith("//") ||
+ line.startsWith("#") ||
+ line.isBlank() ||
+ line.startsWith(" ")) {
+ continue
+ }
+ val idEnd = line.indexOf(':')
+ val elementEnd = line.indexOf(':', idEnd + 1)
+ if (idEnd == -1 || elementEnd == -1) {
+ println("Invalid metalava baseline format: $line")
+ }
+ val errorId = line.substring(0, idEnd).trim()
+ val elementId = line.substring(idEnd + 2, elementEnd).trim()
- // For now we don't need the actual messages since we're only matching by
- // issue id and API location, so don't bother reading. (These are listed
- // on separate, indented, lines, so to read them we'd need to alternate
- // line readers.)
- val message = ""
+ // Unless merging, we don't need the actual messages since we're only matching by
+ // issue id and API location, so don't bother computing.
+ val message = if (merge) lines[i + 1].trim() else ""
- val error = Errors.findErrorById(errorId)
- if (error == null) {
- println("Invalid metalava baseline file: unknown error id '$errorId'")
- } else {
- val newIdMap = map[error] ?: run {
- val new = HashMap<String, String>()
- map[error] = new
- new
- }
- newIdMap[elementId] = message
+ val error = Errors.findErrorById(errorId)
+ if (error == null) {
+ println("Invalid metalava baseline file: unknown error id '$errorId'")
+ } else {
+ val newIdMap = map[error] ?: run {
+ val new = HashMap<String, String>()
+ map[error] = new
+ new
}
+ newIdMap[elementId] = message
}
}
}
diff --git a/src/main/java/com/android/tools/metalava/Options.kt b/src/main/java/com/android/tools/metalava/Options.kt
index 497bf5f10340bce530975f20770f121c60e60455..b83105dbbd3feaf40b94dec2f18be361dc2a69f9 100644
--- a/src/main/java/com/android/tools/metalava/Options.kt
+++ b/src/main/java/com/android/tools/metalava/Options.kt
@@ -138,6 +138,7 @@ const val ARG_DEX_API_MAPPING = "--dex-api-mapping"
const val ARG_GENERATE_DOCUMENTATION = "--generate-documentation"
const val ARG_BASELINE = "--baseline"
const val ARG_UPDATE_BASELINE = "--update-baseline"
+const val ARG_MERGE_BASELINE = "--merge-baseline"
const val ARG_STUB_PACKAGES = "--stub-packages"
const val ARG_STUB_IMPORT_PACKAGES = "--stub-import-packages"
@@ -550,6 +551,7 @@ class Options(
var currentJar: File? = null
var updateBaselineFile: File? = null
var baselineFile: File? = null
+ var mergeBaseline = false
var index = 0
while (index < args.size) {
@@ -720,8 +722,9 @@ class Options(
baselineFile = stringToExistingFile(relative)
}
- ARG_UPDATE_BASELINE -> {
+ ARG_UPDATE_BASELINE, ARG_MERGE_BASELINE -> {
updateBaseline = true
+ mergeBaseline = arg == ARG_MERGE_BASELINE
if (index < args.size - 1) {
val nextArg = args[index + 1]
if (!nextArg.startsWith("-")) {
@@ -1294,18 +1297,19 @@ class Options(
}
if (baselineFile == null) {
- val defaultBaseline = getDefaultBaselineFile()
- if (defaultBaseline != null && defaultBaseline.isFile) {
- baseline = Baseline(defaultBaseline, updateBaselineFile)
+ val defaultBaselineFile = getDefaultBaselineFile()
+ if (defaultBaselineFile != null && defaultBaselineFile.isFile) {
+ baseline = Baseline(defaultBaselineFile, updateBaselineFile, mergeBaseline)
} else if (updateBaselineFile != null) {
- baseline = Baseline(null, updateBaselineFile)
+ baseline = Baseline(null, updateBaselineFile, mergeBaseline)
}
} else {
- val headerComment = if (baselineFile.path.contains("frameworks/base/"))
+ // Add helpful doc in AOSP baseline files?
+ val headerComment = if (System.getenv("ANDROID_BUILD_TOP") != null)
"// See tools/metalava/API-LINT.md for how to update this file.\n\n"
else
""
- baseline = Baseline(baselineFile, updateBaselineFile, headerComment)
+ baseline = Baseline(baselineFile, updateBaselineFile, mergeBaseline, headerComment)
}
checkFlagConsistency()
@@ -1828,6 +1832,10 @@ class Options(
"If some warnings have been fixed, this will delete them from the baseline files. If a file " +
"is provided, the updated baseline is written to the given file; otherwise the original source " +
"baseline file is updated.",
+ "$ARG_MERGE_BASELINE [file]", "Like $ARG_UPDATE_BASELINE, but instead of always replacing entries " +
+ "in the baseline, it will merge the existing baseline with the new baseline. This is useful " +
+ "if $PROGRAM_NAME runs multiple times on the same source tree with different flags at different " +
+ "times, such as occasionally with $ARG_API_LINT.",
"", "\nJDiff:",
"$ARG_XML_API <file>", "Like $ARG_API, but emits the API in the JDiff XML format instead",
diff --git a/src/test/java/com/android/tools/metalava/BaselineTest.kt b/src/test/java/com/android/tools/metalava/BaselineTest.kt
index c836c690532a07b4800b6fd96522191e48afeb0c..955019e3c41eaa9a8a0f01b1850e36912310ba8f 100644
--- a/src/test/java/com/android/tools/metalava/BaselineTest.kt
+++ b/src/test/java/com/android/tools/metalava/BaselineTest.kt
@@ -233,4 +233,72 @@ class BaselineTest : DriverTest() {
checkDoclava1 = false
)
}
+
+ @Test
+ fun `Check merging`() {
+ // Checks merging existing baseline with new baseline: here we have 2 issues that are no longer
+ // in the code base, one issue that is in the code base before and after, and one new issue, and
+ // all 4 end up in the merged baseline.
+ check(
+ extraArguments = arrayOf(
+ ARG_HIDE,
+ "HiddenSuperclass",
+ ARG_HIDE,
+ "UnavailableSymbol",
+ ARG_HIDE,
+ "HiddenTypeParameter",
+ ARG_ERROR,
+ "ReferencesHidden"
+ ),
+ baseline = """
+ // Baseline format: 1.0
+ BothPackageInfoAndHtml: test/visible/package-info.java:
+ It is illegal to provide both a package-info.java file and a package.html file for the same package
+ IgnoringSymlink: test/pkg/sub1/sub2/sub3:
+ Ignoring symlink during package.html discovery directory traversal
+ ReferencesHidden: test.pkg.Foo#hidden2:
+ Class test.pkg.Hidden2 is hidden but was referenced (as field type) from public field test.pkg.Foo.hidden2
+ """,
+ updateBaseline = false,
+ mergeBaseline = """
+ // Baseline format: 1.0
+ BothPackageInfoAndHtml: test/visible/package-info.java:
+ It is illegal to provide both a package-info.java file and a package.html file for the same package
+ IgnoringSymlink: test/pkg/sub1/sub2/sub3:
+ Ignoring symlink during package.html discovery directory traversal
+ ReferencesHidden: test.pkg.Foo#hidden1:
+ Class test.pkg.Hidden1 is not public but was referenced (as field type) from public field test.pkg.Foo.hidden1
+ ReferencesHidden: test.pkg.Foo#hidden2:
+ Class test.pkg.Hidden2 is hidden but was referenced (as field type) from public field test.pkg.Foo.hidden2
+ """,
+ sourceFiles = *arrayOf(
+ java(
+ """
+ package test.pkg;
+ public class Foo extends Hidden2 {
+ public Hidden1 hidden1;
+ public Hidden2 hidden2;
+ }
+ """
+ ),
+ java(
+ """
+ package test.pkg;
+ // Implicitly not part of the API by being package private
+ class Hidden1 {
+ }
+ """
+ ),
+ java(
+ """
+ package test.pkg;
+ /** @hide */
+ public class Hidden2 {
+ }
+ """
+ )
+ ),
+ checkDoclava1 = false
+ )
+ }
}
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/metalava/DriverTest.kt b/src/test/java/com/android/tools/metalava/DriverTest.kt
index 50c861a0e3225eee8e6d608c5d421697bc7ffaca..a30654cd21376f92f26c7434138940f17ba9e3c6 100644
--- a/src/test/java/com/android/tools/metalava/DriverTest.kt
+++ b/src/test/java/com/android/tools/metalava/DriverTest.kt
@@ -191,8 +191,6 @@ abstract class DriverTest {
)
protected fun check(
- /** The source files to pass to the analyzer */
- vararg sourceFiles: TestFile,
/** Any jars to add to the class path */
classpath: Array<TestFile>? = null,
/** The API signature content (corresponds to --api) */
@@ -337,11 +335,15 @@ abstract class DriverTest {
baseline: String? = null,
/** Whether to create the baseline if it does not exist. Requires [baseline] to be set. */
updateBaseline: Boolean = false,
+ /** Merge instead of replacing the baseline */
+ mergeBaseline: String? = null,
/**
* If non null, enable API lint. If non-blank, a codebase where only new APIs not in the codebase
* are linted.
*/
- @Language("TEXT") apiLint: String? = null
+ @Language("TEXT") apiLint: String? = null,
+ /** The source files to pass to the analyzer */
+ vararg sourceFiles: TestFile
) {
// Ensure different API clients don't interfere with each other
try {
@@ -817,10 +819,13 @@ abstract class DriverTest {
val baselineArgs = if (baseline != null) {
baselineFile = temporaryFolder.newFile("baseline.txt")
baselineFile?.writeText(baseline.trimIndent())
- if (!updateBaseline) {
+ if (!(updateBaseline || mergeBaseline != null)) {
arrayOf(ARG_BASELINE, baselineFile.path)
} else {
- arrayOf(ARG_BASELINE, baselineFile.path, ARG_UPDATE_BASELINE, baselineFile.path)
+ arrayOf(ARG_BASELINE,
+ baselineFile.path,
+ if (mergeBaseline != null) ARG_MERGE_BASELINE else ARG_UPDATE_BASELINE,
+ baselineFile.path)
}
} else {
emptyArray()
@@ -1027,7 +1032,8 @@ abstract class DriverTest {
baselineFile.exists()
)
val actualText = readFile(baselineFile, stripBlankLines, trim)
- assertEquals(stripComments(baseline, stripLineComments = false).trimIndent(), actualText)
+ val sourceFile = mergeBaseline ?: baseline
+ assertEquals(stripComments(sourceFile, stripLineComments = false).trimIndent(), actualText)
}
if (convertFiles.isNotEmpty()) {
diff --git a/src/test/java/com/android/tools/metalava/OptionsTest.kt b/src/test/java/com/android/tools/metalava/OptionsTest.kt
index e006c9a40df6399a209b0abaa52e59ee9802bee6..7c61c9eac59ae290eb06e303ba54052f58d7753f 100644
--- a/src/test/java/com/android/tools/metalava/OptionsTest.kt
+++ b/src/test/java/com/android/tools/metalava/OptionsTest.kt
@@ -234,6 +234,13 @@ Diffs and Checks:
updated baseline is written to the given file;
otherwise the original source baseline file is
updated.
+--merge-baseline [file] Like --update-baseline, but instead of always
+ replacing entries in the baseline, it will merge
+ the existing baseline with the new baseline.
+ This is useful if metalava runs multiple times
+ on the same source tree with different flags at
+ different times, such as occasionally with
+ --api-lint.
JDiff:
--api-xml <file> Like --api, but emits the API in the JDiff XML
diff --git a/src/test/java/com/android/tools/metalava/StubsTest.kt b/src/test/java/com/android/tools/metalava/StubsTest.kt
index ee9f0d19e9d9e73cb2fcfe4047374383cae4444d..c5439aac77ef784c09494e8b23fefa311c8e05f0 100644
--- a/src/test/java/com/android/tools/metalava/StubsTest.kt
+++ b/src/test/java/com/android/tools/metalava/StubsTest.kt
@@ -43,7 +43,7 @@ class StubsTest : DriverTest() {
vararg sourceFiles: TestFile
) {
check(
- *sourceFiles,
+ sourceFiles = *sourceFiles,
showAnnotations = showAnnotations,
stubs = arrayOf(source),
compatibilityMode = compatibilityMode,
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 ea7d7028d9d17b54b96bac7adffe21ae54dcb9ad..b7de34f6c635fd36d1297bf14071607cc326848a 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
@@ -37,7 +37,7 @@ class JavadocTest : DriverTest() {
vararg sourceFiles: TestFile
) {
check(
- *sourceFiles,
+ sourceFiles = *sourceFiles,
showAnnotations = showAnnotations,
stubs = arrayOf(source),
compatibilityMode = compatibilityMode,