diff --git a/build.gradle b/build.gradle index 1fb56b1b6507ea6b2384e4893c5240c44b04ade8..ad5e88029928872ad5b64e5718e7e519d90df19f 100644 --- a/build.gradle +++ b/build.gradle @@ -109,7 +109,7 @@ configurations { } dependencies { - ktlint "com.github.shyiko:ktlint:0.29.0" + ktlint "com.github.shyiko:ktlint:0.30.0" } task ktlint(type: JavaExec, group: "verification") { diff --git a/src/main/java/com/android/tools/metalava/DocAnalyzer.kt b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt index b831a5b39dccb1e9c8117f4f7ac00ed08ae60f0c..13f02a9382344b571bea27785be99c818f7265e8 100644 --- a/src/main/java/com/android/tools/metalava/DocAnalyzer.kt +++ b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt @@ -701,7 +701,7 @@ class DocAnalyzer( if (level > 1) { // TODO: *pre*pend instead! val description = - "<p class=\"caution\"><strong>This class was deprecated in API level 21.</strong></p>" + "<p class=\"caution\"><strong>This class was deprecated in API level $level.</strong></p>" item.appendDocumentation(description, "@deprecated", append = false) } } 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 8afb8cb5fd51ca34fb911a6fab223f35851b8f6f..c2c354b720c71bf50b557c2b65154f298f43ca92 100644 --- a/src/main/java/com/android/tools/metalava/Options.kt +++ b/src/main/java/com/android/tools/metalava/Options.kt @@ -136,7 +136,8 @@ 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_INCLUDE_SIG_VERSION = "--include-signature-version" -const val ARG_UPDATE_API = "--update-api" +const val ARG_UPDATE_API = "--only-update-api" +const val ARG_CHECK_API = "--only-check-api" const val ARG_PASS_BASELINE_UPDATES = "--pass-baseline-updates" const val ARG_DEX_API_MAPPING = "--dex-api-mapping" const val ARG_GENERATE_DOCUMENTATION = "--generate-documentation" @@ -146,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>, @@ -197,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. */ @@ -241,7 +246,20 @@ class Options( * signature files. This avoids having duplicate metalava invocation logic where potentially newly * added flags are missing in one of the invocations etc. */ - var updateApi = false + var onlyUpdateApi = false + + /** + * Whether metalava is invoked as part of running the checkapi target. When this is true, metalava + * should *cancel* various other flags that are also being passed in, such as updating signature + * 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), + * 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. + */ + var onlyCheckApi = false /** * Whether signature files should emit in "compat" mode, preserving the various @@ -406,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 @@ -420,7 +438,7 @@ class Options( var migrateNullsFrom: File? = null /** Private backing list for [compatibilityChecks]] */ - private var mutableCompatibilityChecks: MutableList<CheckRequest> = mutableListOf() + private val mutableCompatibilityChecks: MutableList<CheckRequest> = mutableListOf() /** The list of compatibility checks to run */ val compatibilityChecks: List<CheckRequest> = mutableCompatibilityChecks @@ -631,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( @@ -986,7 +1011,8 @@ class Options( ARG_NO_DOCS, "-nodocs" -> noDocs = true - ARG_UPDATE_API -> updateApi = true + ARG_UPDATE_API, "--update-api" -> onlyUpdateApi = true + ARG_CHECK_API -> onlyCheckApi = true ARG_GENERATE_DOCUMENTATION -> { // Digest all the remaining arguments. @@ -1350,7 +1376,10 @@ class Options( generateAnnotations = false } - if (updateApi) { + if (onlyUpdateApi) { + if (onlyCheckApi) { + throw DriverException(stderr = "Cannot supply both $ARG_UPDATE_API and $ARG_CHECK_API at the same time") + } // We're running in update API mode: cancel other "action" flags; only signature file generation // flags count annotationCoverageClassReport = null @@ -1373,6 +1402,46 @@ class Options( mutableCompatibilityChecks.clear() mutableAnnotationCoverageOf.clear() artifactRegistrations.clear() + mutableConvertToXmlFiles.clear() + nullabilityAnnotationsValidator = null + nullabilityWarningsTxt = null + validateNullabilityFromMergedStubs = false + validateNullabilityFromMergedStubs = false + validateNullabilityFromList = null + } else if (onlyCheckApi) { + annotationCoverageClassReport = null + annotationCoverageMemberReport = null + dumpAnnotationStatistics = false + apiLevelJars = null + generateApiLevelXml = null + applyApiLevelsXml = null + androidJarSignatureFiles = null + stubsDir = null + docStubsDir = null + stubsSourceList = null + docStubsSourceList = null + sdkValueDir = null + externalAnnotations = null + proguard = null + noDocs = true + invokeDocumentationToolArguments = emptyArray() + checkKotlinInterop = false + mutableAnnotationCoverageOf.clear() + artifactRegistrations.clear() + mutableConvertToXmlFiles.clear() + nullabilityAnnotationsValidator = null + nullabilityWarningsTxt = null + validateNullabilityFromMergedStubs = false + validateNullabilityFromMergedStubs = false + validateNullabilityFromList = null + apiFile = null + apiXmlFile = null + privateApiFile = null + dexApiFile = null + dexApiMappingFile = null + privateDexApiFile = null + removedApiFile = null + removedDexApiFile = null } if (baselineFile == null) { @@ -1790,6 +1859,8 @@ class Options( "to make it easier customize build system tasks.", ARG_UPDATE_API, "Cancel any other \"action\" flags other than generating signature files. This is here " + "to make it easier customize build system tasks, particularly for the \"make update-api\" task.", + ARG_CHECK_API, "Cancel any other \"action\" flags other than checking signature files. This is here " + + "to make it easier customize build system tasks, particularly for the \"make checkapi\" task.", "", "\nAPI sources:", "$ARG_SOURCE_FILES <files>", "A comma separated list of source files to be parsed. Can also be " + @@ -1841,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/DocAnalyzerTest.kt b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt index 205e133d87271d08bc70aab0eb901a3bba660183..99d76a338ad2a98b5740251f05d73cc5c30e9dd3 100644 --- a/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt +++ b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt @@ -1163,7 +1163,7 @@ class DocAnalyzerTest : DriverTest() { <method name="<init>()V"/> <method name="addCallbackBuffer([B)V" since="8"/> <method name="getLogo()Landroid/graphics/drawable/Drawable;"/> - <field name="ACTION_NEW_VIDEO" since="14" deprecated="25"/> + <field name="ACTION_NEW_VIDEO" since="14" deprecated="19"/> </class> </api> """, @@ -1186,7 +1186,7 @@ class DocAnalyzerTest : DriverTest() { public Camera() { throw new RuntimeException("Stub!"); } /** * @deprecated - * <p class="caution"><strong>This class was deprecated in API level 21.</strong></p> + * <p class="caution"><strong>This class was deprecated in API level 19.</strong></p> * Use something else. * @since 14 */ 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 36d02bf03f2735ac1277cb4e3293ab13f91f6a2f..d625725a9be67f692a8e360a1fa0e1ee76f89f53 100644 --- a/src/test/java/com/android/tools/metalava/OptionsTest.kt +++ b/src/test/java/com/android/tools/metalava/OptionsTest.kt @@ -42,10 +42,14 @@ General: --no-docs Cancel any other documentation flags supplied to metalava. This is here to make it easier customize build system tasks. ---update-api Cancel any other "action" flags other than +--only-update-api Cancel any other "action" flags other than generating signature files. This is here to make it easier customize build system tasks, particularly for the "make update-api" task. +--only-check-api Cancel any other "action" flags other than + checking signature files. This is here to make + it easier customize build system tasks, + particularly for the "make checkapi" task. API sources: --source-files <files> A comma separated list of source files to be @@ -104,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 @@ -112,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 3143036218d450022a35bf0d0422b7127c7df4c0..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 + """ ) } @@ -3715,6 +3721,41 @@ class StubsTest : DriverTest() { ) } + @Test(expected = AssertionError::class) + fun `Test check-api should not generate stubs or API files`() { + check( + extraArguments = arrayOf( + ARG_CHECK_API, + ARG_EXCLUDE_ANNOTATIONS + ), + compatibilityMode = false, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + public class Foo { + /** + * @deprecated Use checkPermission instead. + */ + @Deprecated + protected boolean inClass(String name) { + return false; + } + } + """ + ) + ), + api = """ + package test.pkg { + public class Foo { + ctor public Foo(); + method @Deprecated protected boolean inClass(String); + } + } + """ + ) + } + @Test fun `Include package private classes referenced from public API`() { // Real world example: android.net.http.Connection in apache-http referenced from RequestHandle 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 + """ + ) + } +}