diff --git a/src/main/java/com/android/tools/metalava/Driver.kt b/src/main/java/com/android/tools/metalava/Driver.kt index a1c3bf27aa42a79a5c4d5ec6af8aa9e270b4674e..19d6749c035e37a7ab9d52ce535bf36430d7ec4b 100644 --- a/src/main/java/com/android/tools/metalava/Driver.kt +++ b/src/main/java/com/android/tools/metalava/Driver.kt @@ -17,9 +17,10 @@ package com.android.tools.metalava -import com.android.SdkConstants +import com.android.SdkConstants.DOT_JAR import com.android.SdkConstants.DOT_JAVA import com.android.SdkConstants.DOT_KT +import com.android.SdkConstants.DOT_TXT import com.android.ide.common.process.CachedProcessOutputHandler import com.android.ide.common.process.DefaultProcessExecutor import com.android.ide.common.process.ProcessInfoBuilder @@ -37,6 +38,7 @@ import com.android.tools.metalava.doclava1.ApiPredicate import com.android.tools.metalava.doclava1.Errors import com.android.tools.metalava.doclava1.FilterPredicate import com.android.tools.metalava.doclava1.TextCodebase +import com.android.tools.metalava.model.ClassItem import com.android.tools.metalava.model.Codebase import com.android.tools.metalava.model.Item import com.android.tools.metalava.model.PackageDocs @@ -207,14 +209,14 @@ private fun processFlags() { processNonCodebaseFlags() val codebase = - if (options.sources.size == 1 && options.sources[0].path.endsWith(SdkConstants.DOT_TXT)) { + if (options.sources.size == 1 && options.sources[0].path.endsWith(DOT_TXT)) { SignatureFileLoader.load( file = options.sources[0], kotlinStyleNulls = options.inputKotlinStyleNulls ) } else if (options.apiJar != null) { loadFromJarFile(options.apiJar!!) - } else if (options.sources.size == 1 && options.sources[0].path.endsWith(SdkConstants.DOT_JAR)) { + } else if (options.sources.size == 1 && options.sources[0].path.endsWith(DOT_JAR)) { loadFromJarFile(options.sources[0]) } else if (options.sources.isNotEmpty() || options.sourcePath.isNotEmpty()) { loadFromSources() @@ -227,6 +229,10 @@ private fun processFlags() { options.stdout.println("\n$PROGRAM_NAME analyzed API in ${stopwatch.elapsed(TimeUnit.SECONDS)} seconds") } + options.subtractApi?.let { + subtractApi(codebase, it) + } + val androidApiLevelXml = options.generateApiLevelXml val apiLevelJars = options.apiLevelJars if (androidApiLevelXml != null && apiLevelJars != null) { @@ -372,7 +378,7 @@ private fun processFlags() { val previousApiFile = options.migrateNullsFrom if (previousApiFile != null) { val previous = - if (previousApiFile.path.endsWith(SdkConstants.DOT_JAR)) { + if (previousApiFile.path.endsWith(DOT_JAR)) { loadFromJarFile(previousApiFile) } else { SignatureFileLoader.load( @@ -450,6 +456,23 @@ private fun processFlags() { invokeDocumentationTool() } +fun subtractApi(codebase: Codebase, subtractApiFile: File) { + val path = subtractApiFile.path + val oldCodebase = + when { + path.endsWith(DOT_TXT) -> SignatureFileLoader.load(subtractApiFile) + path.endsWith(DOT_JAR) -> loadFromJarFile(subtractApiFile) + else -> throw DriverException("Unsupported $ARG_SUBTRACT_API format, expected .txt or .jar: ${subtractApiFile.name}") + } + + CodebaseComparator().compare(object : ComparisonVisitor() { + override fun compare(old: ClassItem, new: ClassItem) { + new.included = false + new.emit = false + } + }, oldCodebase, codebase, ApiType.ALL.getReferenceFilter()) +} + fun processNonCodebaseFlags() { // --copy-annotations? val privateAnnotationsSource = options.privateAnnotationsSource @@ -554,7 +577,7 @@ fun checkCompatibility( val signatureFile = check.file val current = - if (signatureFile.path.endsWith(SdkConstants.DOT_JAR)) { + if (signatureFile.path.endsWith(DOT_JAR)) { loadFromJarFile(signatureFile) } else { SignatureFileLoader.load( @@ -801,7 +824,7 @@ private fun loadFromSources(): Codebase { val previous = when { previousApiFile == null -> null - previousApiFile.path.endsWith(SdkConstants.DOT_JAR) -> loadFromJarFile(previousApiFile) + previousApiFile.path.endsWith(DOT_JAR) -> loadFromJarFile(previousApiFile) else -> SignatureFileLoader.load( file = previousApiFile, kotlinStyleNulls = options.inputKotlinStyleNulls @@ -863,7 +886,7 @@ internal fun parseSources( // Create project environment with those paths projectEnvironment.registerPaths(joined) - val kotlinFiles = sources.filter { it.path.endsWith(SdkConstants.DOT_KT) } + val kotlinFiles = sources.filter { it.path.endsWith(DOT_KT) } val trace = KotlinLintAnalyzerFacade().analyze(kotlinFiles, joined, project) val rootDir = sourceRoots.firstOrNull() ?: sourcePath.firstOrNull() ?: File("").canonicalFile diff --git a/src/main/java/com/android/tools/metalava/Options.kt b/src/main/java/com/android/tools/metalava/Options.kt index 7b9698d7c5a9ca74dcee3da7e296a3ae2ac0e767..c2c354b720c71bf50b557c2b65154f298f43ca92 100644 --- a/src/main/java/com/android/tools/metalava/Options.kt +++ b/src/main/java/com/android/tools/metalava/Options.kt @@ -147,6 +147,7 @@ const val ARG_MERGE_BASELINE = "--merge-baseline" const val ARG_STUB_PACKAGES = "--stub-packages" const val ARG_STUB_IMPORT_PACKAGES = "--stub-import-packages" const val ARG_DELETE_EMPTY_BASELINES = "--delete-empty-baselines" +const val ARG_SUBTRACT_API = "--subtract-api" class Options( private val args: Array<String>, @@ -198,6 +199,9 @@ class Options( */ var noDocs = false + /** API to subtract from signature and stub generation. Corresponds to [ARG_SUBTRACT_API]. */ + var subtractApi: File? = null + /** * Validator for nullability annotations, if validation is enabled. */ @@ -420,7 +424,7 @@ class Options( var removedDexApiFile: File? = null /** Whether output should be colorized */ - var color = System.getenv("TERM")?.startsWith("xterm") ?: false + var color = System.getenv("TERM")?.startsWith("xterm") ?: System.getenv("COLORTERM") != null ?: false /** Whether to omit Java and Kotlin runtime library packages from annotation coverage stats */ var omitRuntimePackageStats = false @@ -645,6 +649,13 @@ class Options( } } + ARG_SUBTRACT_API -> { + if (subtractApi != null) { + throw DriverException(stderr = "Only one $ARG_SUBTRACT_API can be supplied") + } + subtractApi = stringToExistingFile(getValue(args, ++index)) + } + // TODO: Remove the legacy --merge-annotations flag once it's no longer used to update P docs ARG_MERGE_QUALIFIER_ANNOTATIONS, "--merge-zips", "--merge-annotations" -> mutableMergeQualifierAnnotations.addAll( stringToExistingDirsOrFiles( @@ -1901,11 +1912,14 @@ class Options( "as hidden", ARG_SHOW_UNANNOTATED, "Include un-annotated public APIs in the signature file as well", "$ARG_JAVA_SOURCE <level>", "Sets the source level for Java source files; default is 1.8.", - "$ARG_STUB_PACKAGES <path>", "List of packages (separated by ${File.pathSeparator} which will be " + - "used to filter out irrelevant code. If specified, only code in these packages will be " + + "$ARG_STUB_PACKAGES <package-list>", "List of packages (separated by ${File.pathSeparator}) which will " + + "be used to filter out irrelevant code. If specified, only code in these packages will be " + "included in signature files, stubs, etc. (This is not limited to just the stubs; the name " + "is historical.) You can also use \".*\" at the end to match subpackages, so `foo.*` will " + "match both `foo` and `foo.bar`.", + "$ARG_SUBTRACT_API <api file>", "Subtracts the API in the given signature or jar file from the " + + "current API being emitted via $ARG_API, $ARG_STUBS, $ARG_DOC_STUBS, etc. " + + "Note that the subtraction only applies to classes; it does not subtract members.", "", "\nDocumentation:", ARG_PUBLIC, "Only include elements that are public", diff --git a/src/test/java/com/android/tools/metalava/DriverTest.kt b/src/test/java/com/android/tools/metalava/DriverTest.kt index 2e790eaa07129141628fb4757f806adacd99809e..9c27ca41e0b8a2d8992f1f2b349857ba0d723e68 100644 --- a/src/test/java/com/android/tools/metalava/DriverTest.kt +++ b/src/test/java/com/android/tools/metalava/DriverTest.kt @@ -226,6 +226,9 @@ abstract class DriverTest { dexApi: String? = null, /** The DEX mapping API (corresponds to --dex-api-mapping) */ dexApiMapping: String? = null, + /** The subtract api signature content (corresponds to --subtract-api) */ + @Language("TEXT") + subtractApi: String? = null, /** Expected stubs (corresponds to --stubs) */ @Language("JAVA") stubs: Array<String> = emptyArray(), /** Stub source file list generated */ @@ -773,6 +776,15 @@ abstract class DriverTest { emptyArray() } + var subtractApiFile: File? = null + val subtractApiArgs = if (subtractApi != null) { + subtractApiFile = temporaryFolder.newFile("subtract-api.txt") + subtractApiFile.writeText(subtractApi.trimIndent()) + arrayOf(ARG_SUBTRACT_API, subtractApiFile.path) + } else { + emptyArray() + } + val convertFiles = mutableListOf<Options.ConvertFile>() val convertArgs = if (convertToJDiff.isNotEmpty()) { val args = mutableListOf<String>() @@ -998,6 +1010,7 @@ abstract class DriverTest { *dexApiArgs, *privateDexApiArgs, *dexApiMappingArgs, + *subtractApiArgs, *stubsArgs, *stubsSourceListArgs, "$ARG_COMPAT_OUTPUT=${if (compatibilityMode) "yes" else "no"}", @@ -1276,7 +1289,10 @@ abstract class DriverTest { "${stubsSourceListFile.path} does not exist even though --write-stubs-source-list was used", stubsSourceListFile.exists() ) - val actualText = readFile(stubsSourceListFile, stripBlankLines, trim) + val actualText = cleanupString(readFile(stubsSourceListFile, stripBlankLines, trim), project) + // To make golden files look better put one entry per line instead of a single + // space separated line + .replace(' ', '\n') assertEquals(stripComments(stubsSourceList, stripLineComments = false).trimIndent(), actualText) } diff --git a/src/test/java/com/android/tools/metalava/OptionsTest.kt b/src/test/java/com/android/tools/metalava/OptionsTest.kt index c405942eb5e2c475ea16dfe556dfa09c04471be5..d625725a9be67f692a8e360a1fa0e1ee76f89f53 100644 --- a/src/test/java/com/android/tools/metalava/OptionsTest.kt +++ b/src/test/java/com/android/tools/metalava/OptionsTest.kt @@ -108,7 +108,7 @@ API sources: signature file as well --java-source <level> Sets the source level for Java source files; default is 1.8. ---stub-packages <path> List of packages (separated by : which will be +--stub-packages <package-list> List of packages (separated by :) which will be used to filter out irrelevant code. If specified, only code in these packages will be included in signature files, stubs, etc. (This @@ -116,6 +116,11 @@ API sources: historical.) You can also use ".*" at the end to match subpackages, so `foo.*` will match both `foo` and `foo.bar`. +--subtract-api <api file> Subtracts the API in the given signature or jar + file from the current API being emitted via + --api, --stubs, --doc-stubs, etc. Note that the + subtraction only applies to classes; it does not + subtract members. Documentation: --public Only include elements that are public diff --git a/src/test/java/com/android/tools/metalava/StubsTest.kt b/src/test/java/com/android/tools/metalava/StubsTest.kt index d0eb814955e9acf2dd4591a4d8998228bebc49f7..b8719ed7a0716911fdccb69ff84ec0b1ef4454cd 100644 --- a/src/test/java/com/android/tools/metalava/StubsTest.kt +++ b/src/test/java/com/android/tools/metalava/StubsTest.kt @@ -1995,7 +1995,13 @@ class StubsTest : DriverTest() { public MySubClass2() { super(0); throw new RuntimeException("Stub!"); } } """ - ) + ), + stubsSourceList = """ + TESTROOT/stubs/test/pkg/MyClass1.java + TESTROOT/stubs/test/pkg/MyClass2.java + TESTROOT/stubs/test/pkg/MySubClass1.java + TESTROOT/stubs/test/pkg/MySubClass2.java + """ ) } diff --git a/src/test/java/com/android/tools/metalava/SubtractApiTest.kt b/src/test/java/com/android/tools/metalava/SubtractApiTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..02cb8dd92dd7114484dbf35aeb809760c719958e --- /dev/null +++ b/src/test/java/com/android/tools/metalava/SubtractApiTest.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2019 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. + */ + +@file:Suppress("ALL") + +package com.android.tools.metalava + +import org.junit.Test + +class SubtractApiTest : DriverTest() { + @Test + fun `Subtract APIs`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + public class OnlyInNew { + private OnlyInNew() { } + public void method1() { } + public void method5() { } + public void method6() { } + } + """ + ), + java( + """ + package test.pkg; + public class InBoth { + private InBoth() { } + public void method1() { } + public void method5() { } + public void method9() { } + } + """ + ) + ), + subtractApi = """ + package test.pkg { + public class InBoth { + method public void method1(); + method public void method5(); + method public void method9(); + } + public class OnlyInOld { + method public void method1(); + method public void method2(); + method public void method3(); + } + } + """, + api = """ + package test.pkg { + public class OnlyInNew { + method public void method1(); + method public void method5(); + method public void method6(); + } + } + """, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class OnlyInNew { + 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!"); } + } + """ + ), + stubsSourceList = """ + TESTROOT/stubs/test/pkg/OnlyInNew.java + """ + ) + } +}