From 5f229d7809cb70a28691f205b54b7a21eb7948b2 Mon Sep 17 00:00:00 2001 From: Tor Norbye <tnorbye@google.com> Date: Tue, 20 Feb 2018 15:50:40 -0800 Subject: [PATCH] Support @RequiresFeature, -sdkvalues, etc This CL includes a number of changes to metalava: - Track recent features added to doclava, such as (a) supporting the @RequiresFeature annotation to insert special documentation comments which links to the required feature and how to check for it (b) omitting "final" from methods in signatures where the surrounding class is also final - Changing the way relative references are handled in javadocs. Previously, metalava attempted to rewrite all javadocs to use fully qualified references in all cases, such that running javadoc on the stubs themselves would work (and since the rest of the stubs are using fully qualified references). However, the javadoc rewriting was a bit brittle, so instead leave the docs alone and include all the import statements from the original code instead (limited to the imports for classes/methods that are part of the API that is.) - Add support for the -sdkvalues flag from doclava1: this basically ports the code which lets metalva emit the various SDK files in platforms/android-X/data/ such as broadcast_actions.txt and widgets.txt. - Fixing the markdown formatting to correctly handle tables in gitiles - Allow for all compatibility flags to be specified from the command line Test: Unit tests included & updated Change-Id: Idb13fe42a746cfeebabf65a3c4b4c912fbd22e0e --- README.md | 13 +- build.gradle | 4 +- .../tools/metalava/AnnotationStatistics.kt | 19 +- .../com/android/tools/metalava/ApiAnalyzer.kt | 25 +- .../android/tools/metalava/Compatibility.kt | 10 +- .../com/android/tools/metalava/Constants.kt | 25 + .../com/android/tools/metalava/DocAnalyzer.kt | 40 +- .../com/android/tools/metalava/DocLevel.kt | 26 ++ .../java/com/android/tools/metalava/Driver.kt | 43 +- .../com/android/tools/metalava/Options.kt | 108 ++++- .../android/tools/metalava/SdkFileWriter.kt | 300 ++++++++++++ .../com/android/tools/metalava/StubWriter.kt | 30 +- .../tools/metalava/doclava1/ApiFile.java | 15 +- .../tools/metalava/doclava1/ApiInfo.kt | 5 +- .../tools/metalava/doclava1/ApiPredicate.kt | 2 +- .../tools/metalava/model/AnnotationItem.kt | 13 +- .../android/tools/metalava/model/ClassItem.kt | 6 +- .../tools/metalava/model/CompilationUnit.kt | 2 +- .../com/android/tools/metalava/model/Item.kt | 6 +- .../tools/metalava/model/ModifierList.kt | 27 ++ .../android/tools/metalava/model/TypeItem.kt | 35 +- .../metalava/model/psi/PsiBasedCodebase.kt | 4 + .../tools/metalava/model/psi/PsiClassItem.kt | 42 +- .../metalava/model/psi/PsiCompilationUnit.kt | 169 +++++++ .../tools/metalava/model/psi/PsiItem.kt | 1 - .../tools/metalava/model/psi/PsiMethodItem.kt | 7 + .../metalava/AnnotationStatisticsTest.kt | 4 - .../com/android/tools/metalava/ApiFileTest.kt | 148 +++++- .../android/tools/metalava/DocAnalyzerTest.kt | 53 ++- .../com/android/tools/metalava/DriverTest.kt | 430 +++++++++++------- .../com/android/tools/metalava/OptionsTest.kt | 10 + .../tools/metalava/SdkFileWriterTest.kt | 80 ++++ .../com/android/tools/metalava/StubsTest.kt | 69 +-- .../tools/metalava/model/TypeItemTest.kt | 3 +- 34 files changed, 1435 insertions(+), 339 deletions(-) create mode 100644 src/main/java/com/android/tools/metalava/Constants.kt create mode 100644 src/main/java/com/android/tools/metalava/DocLevel.kt create mode 100644 src/main/java/com/android/tools/metalava/SdkFileWriter.kt create mode 100644 src/main/java/com/android/tools/metalava/model/psi/PsiCompilationUnit.kt create mode 100644 src/test/java/com/android/tools/metalava/SdkFileWriterTest.kt diff --git a/README.md b/README.md index 5a46b2c..58ac765 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,15 @@ example, you'll see something like this (unless running with --quiet) : inconsistency. In metalava the stub files contain **exactly** the same signatures as in the signature files. + (This turned out to be incredibly important; this revealed for example that + StringBuilder.setLength(int) was missing from the API signatures since it is + a public method inherited from a package protected super class, which the + API extraction code in doclava1 missed, but accidentally included in the SDK + anyway since it packages package private classes. Metalava strictly applies + the exact same API as is listed in the signature files, and once this was + hooked up to the build it immediately became apparent that it was missing + important methods that should really be part of the API.) + * Metalava can generate reports about nullness annotation coverage (which helps target efforts since we plan to annotate the entire API). First, it can generate a raw count: @@ -194,7 +203,6 @@ example, you'll see something like this (unless running with --quiet) : 324 methods and fields were missing nullness annotations out of 650 total API references. API nullness coverage is 50% - |--------------------------------------------------------------|------------------| | Qualified Class Name | Usage Count | |--------------------------------------------------------------|-----------------:| | android.os.Parcel | 146 | @@ -214,11 +222,9 @@ example, you'll see something like this (unless running with --quiet) : | android.text.SpannableStringBuilder | 23 | | android.view.ViewGroup.MarginLayoutParams | 21 | | ... (99 more items | | - |--------------------------------------------------------------|------------------| Top referenced un-annotated members: - |--------------------------------------------------------------|------------------| | Member | Usage Count | |--------------------------------------------------------------|-----------------:| | Parcel.readString() | 62 | @@ -238,7 +244,6 @@ example, you'll see something like this (unless running with --quiet) : | Context.getResources() | 18 | | EditText.getText() | 18 | | ... (309 more items | | - |--------------------------------------------------------------|------------------| From this it's clear that it would be useful to start annotating android.os.Parcel diff --git a/build.gradle b/build.gradle index 01e847a..5a8b5cb 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ apply plugin: 'kotlin' apply plugin: 'maven' group = 'com.android' -version = '0.9.2' +version = '0.9.3' mainClassName = "com.android.tools.metalava.Driver" applicationDefaultJvmArgs = ["-ea", "-Xms2g", "-Xmx4g"] @@ -69,7 +69,7 @@ shadowJar { zip64 = true } -defaultTasks 'installDist' +defaultTasks 'clean', 'installDist' /* * With the build server you are given two env variables: diff --git a/src/main/java/com/android/tools/metalava/AnnotationStatistics.kt b/src/main/java/com/android/tools/metalava/AnnotationStatistics.kt index f82e67c..1f2e4aa 100644 --- a/src/main/java/com/android/tools/metalava/AnnotationStatistics.kt +++ b/src/main/java/com/android/tools/metalava/AnnotationStatistics.kt @@ -43,9 +43,11 @@ import java.util.zip.ZipFile const val CLASS_COLUMN_WIDTH = 60 const val COUNT_COLUMN_WIDTH = 16 const val USAGE_REPORT_MAX_ROWS = 15 +/** Sadly gitiles' markdown support doesn't handle tables with top/bottom horizontal edges */ +const val INCLUDE_HORIZONTAL_EDGES = false class AnnotationStatistics(val api: Codebase) { - val apiFilter = ApiPredicate(api) + private val apiFilter = ApiPredicate(api) /** Measure the coverage statistics for the API */ fun count() { @@ -191,7 +193,10 @@ class AnnotationStatistics(val api: Codebase) { printer: PrintWriter = options.stdout ) { // Print table in clean Markdown table syntax - edge(printer, CLASS_COLUMN_WIDTH + 2, COUNT_COLUMN_WIDTH + 2) + @Suppress("ConstantConditionIf") + if (INCLUDE_HORIZONTAL_EDGES) { + edge(printer, CLASS_COLUMN_WIDTH + COUNT_COLUMN_WIDTH + 7) + } printer.printf( "| %-${CLASS_COLUMN_WIDTH}s | %${COUNT_COLUMN_WIDTH}s |\n", labelHeader, countHeader @@ -215,7 +220,10 @@ class AnnotationStatistics(val api: Codebase) { break } } - edge(printer, CLASS_COLUMN_WIDTH + 2, COUNT_COLUMN_WIDTH + 2) + @Suppress("ConstantConditionIf") + if (INCLUDE_HORIZONTAL_EDGES) { + edge(printer, CLASS_COLUMN_WIDTH + 2, COUNT_COLUMN_WIDTH + 2) + } } private fun printClassTable(classes: List<Item>, classCount: MutableMap<Item, Int>) { @@ -254,6 +262,11 @@ class AnnotationStatistics(val api: Codebase) { } } + private fun edge(printer: PrintWriter, max: Int) { + dashes(printer, max) + printer.println() + } + private fun edge(printer: PrintWriter, column1: Int, column2: Int) { printer.print("|") dashes(printer, column1) diff --git a/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt b/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt index bf36dda..3819c1e 100644 --- a/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt +++ b/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt @@ -412,6 +412,24 @@ class ApiAnalyzer( } } + if (compatibility.includePublicMethodsFromHiddenSuperClasses) { + // Also add in any concrete public methods from hidden super classes + for (superClass in hiddenSuperClasses) { + for (method in superClass.methods()) { + if (method.modifiers.isAbstract()) { + continue + } + val name = method.name() + val list = interfaceNames[name] ?: run { + val list = ArrayList<MethodItem>() + interfaceNames[name] = list + list + } + list.add(method) + } + } + } + // Find all methods that are inherited from these classes into our class // (making sure that we don't have duplicates, e.g. a method defined by one // inherited class and then overridden by another closer one). @@ -470,9 +488,14 @@ class ApiAnalyzer( // interfaces that are listed in this class. Create stubs for them: map.values.flatten().forEach { val method = cls.createMethod(it) + /* Insert comment marker: This is useful for debugging purposes but doesn't + belong in the stub method.documentation = "// Inlined stub from hidden parent class ${it.containingClass().qualifiedName()}\n" + method.documentation - method.inheritedInterfaceMethod = true + */ + if (it.containingClass().isInterface()) { + method.inheritedInterfaceMethod = true + } cls.addMethod(method) } } diff --git a/src/main/java/com/android/tools/metalava/Compatibility.kt b/src/main/java/com/android/tools/metalava/Compatibility.kt index 7abc4de..a4cced9 100644 --- a/src/main/java/com/android/tools/metalava/Compatibility.kt +++ b/src/main/java/com/android/tools/metalava/Compatibility.kt @@ -156,7 +156,15 @@ class Compatibility( /** * Whether to include parameter names in the signature file */ - val parameterNames: Boolean = true + var parameterNames: Boolean = true + + /** + * Whether we should include public methods from super classes. + * Docalava1 did not do this in its signature files, but they + * were included in stub files. An example of this scenario + * is StringBuilder#setLength. + */ + var includePublicMethodsFromHiddenSuperClasses = !compat // Other examples: sometimes we sort by qualified name, sometimes by full name } \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/Constants.kt b/src/main/java/com/android/tools/metalava/Constants.kt new file mode 100644 index 0000000..7badba3 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/Constants.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018 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 + +const val JAVA_LANG_PREFIX = "java.lang." +const val JAVA_LANG_OBJECT = "java.lang.Object" +const val JAVA_LANG_STRING = "java.lang.String" +const val JAVA_LANG_ENUM = "java.lang.Enum" +const val JAVA_LANG_ANNOTATION = "java.lang.annotation.Annotation" +const val ANDROID_SUPPORT_ANNOTATION_PREFIX = "android.support.annotation." + diff --git a/src/main/java/com/android/tools/metalava/DocAnalyzer.kt b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt index 4fe3b2f..872531b 100644 --- a/src/main/java/com/android/tools/metalava/DocAnalyzer.kt +++ b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt @@ -113,7 +113,7 @@ class DocAnalyzer( var result: MutableList<String>? = null for (annotation in annotations) { val name = annotation.qualifiedName() - if (name != null && name.endsWith("Thread") && name.startsWith("android.support.annotation.")) { + if (name != null && name.endsWith("Thread") && name.startsWith(ANDROID_SUPPORT_ANNOTATION_PREFIX)) { if (result == null) { result = mutableListOf() } @@ -143,7 +143,7 @@ class DocAnalyzer( item: Item, depth: Int ) { val name = annotation.qualifiedName() - if (name == null || name.startsWith("java.lang.")) { + if (name == null || name.startsWith(JAVA_LANG_PREFIX)) { // Ignore java.lang.Retention etc. return } @@ -311,6 +311,42 @@ class DocAnalyzer( appendDocumentation(sb.toString(), item, true) } + // Required Features + if (name == "android.annotation.RequiresFeature") { + val value = annotation.findAttribute("value")?.leafValues()?.firstOrNull() ?: return + val sb = StringBuilder(100) + val resolved = value.resolve() + val field = if (resolved is FieldItem) + resolved + else { + val v: Any = value.value() ?: value.toSource() + findPermissionField(codebase, v) + } + sb.append("Requires the ") + if (field == null) { + reporter.report( + Errors.MISSING_PERMISSION, item, + "Cannot find feature field for $value required by $item (may be hidden or removed)" + ) + sb.append("{@link ${value.toSource()}}") + + } else { + if (field.isHiddenOrRemoved()) { + reporter.report( + Errors.MISSING_PERMISSION, item, + "Feature field $value required by $item is hidden or removed" + ) + } + + sb.append("{@link ${field.containingClass().qualifiedName()}#${field.name()} ${field.containingClass().simpleName()}#${field.name()}} ") + } + + sb.append("feature which can be detected using ") + sb.append("{@link android.content.pm.PackageManager#hasSystemFeature(String) ") + sb.append("PackageManager.hasSystemFeature(String)}.") + appendDocumentation(sb.toString(), item, false) + } + // Thread annotations are ignored here because they're handled as a group afterwards // TODO: Resource type annotations diff --git a/src/main/java/com/android/tools/metalava/DocLevel.kt b/src/main/java/com/android/tools/metalava/DocLevel.kt new file mode 100644 index 0000000..633aa6a --- /dev/null +++ b/src/main/java/com/android/tools/metalava/DocLevel.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2018 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 + +/** Javadoc filtering levels */ +enum class DocLevel { + PUBLIC, + PROTECTED, + PACKAGE, + PRIVATE, + HIDDEN +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/Driver.kt b/src/main/java/com/android/tools/metalava/Driver.kt index 4b79517..1342e22 100644 --- a/src/main/java/com/android/tools/metalava/Driver.kt +++ b/src/main/java/com/android/tools/metalava/Driver.kt @@ -99,14 +99,14 @@ fun run( args } + compatibility = Compatibility(compat = Options.useCompatMode(args)) options = Options(modifiedArgs, stdout, stderr) - compatibility = Compatibility(options.compatOutput) processFlags() stdout.flush() stderr.flush() if (setExitCode && reporter.hasErrors()) { - System.exit(-1) + exit(-1) } return true } catch (e: DriverException) { @@ -117,9 +117,7 @@ fun run( stdout.println("\n${e.stdout}") } if (setExitCode) { // always true in production; not set from tests - stdout.flush() - stderr.flush() - System.exit(e.exitCode) + exit(e.exitCode) } } stdout.flush() @@ -127,6 +125,15 @@ fun run( return false } +private fun exit(exitCode: Int = 0) { + if (options.verbose) { + options.stdout.println("$PROGRAM_NAME exiting with exit code $exitCode") + } + options.stdout.flush() + options.stderr.flush() + System.exit(exitCode) +} + private fun processFlags() { val stopwatch = Stopwatch.createStarted() @@ -290,22 +297,22 @@ private fun loadFromSources(): Codebase { KotlinInteropChecks().check(codebase) } - progress("\nInsert missing constructors: ") val ignoreShown = options.showUnannotated + val filterEmit = ApiPredicate(codebase, ignoreShown = ignoreShown, ignoreRemoved = false) + val apiEmit = ApiPredicate(codebase, ignoreShown = ignoreShown) + val apiReference = ApiPredicate(codebase, ignoreShown = true) + + // Copy methods from soon-to-be-hidden parents into descendant classes, when necessary + progress("\nInsert missing stubs methods: ") + analyzer.generateInheritedStubs(apiEmit, apiReference) + // Compute default constructors (and add missing package private constructors // to make stubs compilable if necessary) if (options.stubsDir != null) { - val filterEmit = ApiPredicate(codebase, ignoreShown = ignoreShown, ignoreRemoved = false) + progress("\nInsert missing constructors: ") analyzer.addConstructors(filterEmit) - val apiEmit = ApiPredicate(codebase, ignoreShown = ignoreShown) - val apiReference = ApiPredicate(codebase, ignoreShown = true) - - // Copy methods from soon-to-be-hidden parents into descendant classes, when necessary - progress("\nInsert missing stubs methods: ") - analyzer.generateInheritedStubs(apiEmit, apiReference) - progress("\nEnhancing docs: ") val docAnalyzer = DocAnalyzer(codebase) docAnalyzer.enhance() @@ -320,9 +327,6 @@ private fun loadFromSources(): Codebase { progress("\nPerforming misc API checks: ") analyzer.performChecks() -// // TODO: Move the filtering earlier -// progress("\nFiltering API: ") -// return filterCodebase(codebase) return codebase } @@ -422,6 +426,11 @@ private fun generateOutputs(codebase: Codebase) { { printWriter -> ProguardWriter(printWriter, apiEmit, apiReference) }) } + options.sdkValueDir?.let { dir -> + dir.mkdirs() + SdkFileWriter(codebase, dir).generate() + } + options.stubsDir?.let { createStubFiles(it, codebase) } // Otherwise, if we've asked to write out a file list, write out the // input file list instead diff --git a/src/main/java/com/android/tools/metalava/Options.kt b/src/main/java/com/android/tools/metalava/Options.kt index 06fbbda..5d72e52 100644 --- a/src/main/java/com/android/tools/metalava/Options.kt +++ b/src/main/java/com/android/tools/metalava/Options.kt @@ -17,6 +17,7 @@ package com.android.tools.metalava import com.android.SdkConstants +import com.android.sdklib.SdkVersionInfo import com.android.tools.lint.annotations.ApiDatabase import com.android.tools.metalava.doclava1.Errors import com.android.utils.SdkUtils.wrap @@ -29,12 +30,15 @@ import java.io.IOException import java.io.OutputStreamWriter import java.io.PrintWriter import java.io.StringWriter +import kotlin.reflect.KMutableProperty1 +import kotlin.reflect.full.memberProperties /** Global options for the metadata extraction tool */ var options = Options(emptyArray()) private const val MAX_LINE_WIDTH = 90 +private const val ARGS_COMPAT_OUTPUT = "--compatible-output" private const val ARG_HELP = "--help" private const val ARG_QUIET = "--quiet" private const val ARG_VERBOSE = "--verbose" @@ -44,7 +48,7 @@ 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_PRIVATE_DEX_API = "--private-dex-api" -private const val ARGS_COMPAT_OUTPUT = "--compatible-output" +private const val ARG_SDK_VALUES = "--sdk-values" private const val ARG_REMOVED_API = "--removed-api" private const val ARG_MERGE_ANNOTATIONS = "--merge-annotations" private const val ARG_INPUT_API_JAR = "--input-api-jar" @@ -93,6 +97,11 @@ private const val ARG_CURRENT_VERSION = "--current-version" private const val ARG_CURRENT_CODENAME = "--current-codename" private const val ARG_CURRENT_JAR = "--current-jar" private const val ARG_CHECK_KOTLIN_INTEROP = "--check-kotlin-interop" +private const val ARG_PUBLIC = "--public" +private const val ARG_PROTECTED = "--protected" +private const val ARG_PACKAGE = "--package" +private const val ARG_PRIVATE = "--private" +private const val ARG_HIDDEN = "--hidden" class Options( args: Array<String>, @@ -133,7 +142,7 @@ class Options( * for more fine grained control (which is not (currently) exposed as individual command line * flags. */ - var compatOutput = COMPAT_MODE_BY_DEFAULT && !args.contains("$ARGS_COMPAT_OUTPUT=no") + var compatOutput = useCompatMode(args) /** Whether nullness annotations should be displayed as ?/!/empty instead of with @NonNull/@Nullable. */ var outputKotlinStyleNulls = !compatOutput @@ -216,6 +225,9 @@ class Options( /** If set, a file to write the private DEX signatures to. Corresponds to --private-dex-api. */ var privateDexApiFile: File? = null + /** Path to directory to write SDK values to */ + var sdkValueDir: File? = null + /** If set, a file to write extracted annotations to. Corresponds to the --extract-annotations flag. */ var externalAnnotations: File? = null @@ -309,6 +321,9 @@ class Options( /** Reads API XML file to apply into documentation */ var applyApiLevelsXml: File? = null + /** Level to include for javadoc */ + var docLevel = DocLevel.PROTECTED + init { // Pre-check whether --color/--no-color is present and use that to decide how // to emit the banner even before we emit errors @@ -381,6 +396,8 @@ class Options( ) ) + "-sdkvalues", ARG_SDK_VALUES -> sdkValueDir = stringToNewDir(getValue(args, ++index)) + ARG_API, "-api" -> apiFile = stringToNewFile(getValue(args, ++index)) ARG_PRIVATE_API, "-privateApi" -> privateApiFile = stringToNewFile(getValue(args, ++index)) @@ -409,6 +426,7 @@ class Options( ARG_STUBS_SOURCE_LIST -> stubsSourceList = stringToNewFile(getValue(args, ++index)) ARG_EXCLUDE_ANNOTATIONS -> generateAnnotations = false + "--include-annotations" -> generateAnnotations = true // temporary for tests ARG_PROGUARD, "-proguard" -> proguard = stringToNewFile(getValue(args, ++index)) @@ -437,6 +455,12 @@ class Options( mutableSkipEmitPackages += packages.split(File.pathSeparatorChar) } + ARG_PUBLIC, "-public" -> docLevel = DocLevel.PUBLIC + ARG_PROTECTED, "-protected" -> docLevel = DocLevel.PROTECTED + ARG_PACKAGE, "-package" -> docLevel = DocLevel.PACKAGE + ARG_PRIVATE, "-private" -> docLevel = DocLevel.PRIVATE + ARG_HIDDEN, "-hidden" -> docLevel = DocLevel.HIDDEN + ARG_INPUT_API_JAR -> apiJar = stringToExistingFile(getValue(args, ++index)) ARG_EXTRACT_ANNOTATIONS -> externalAnnotations = stringToNewFile(getValue(args, ++index)) @@ -561,6 +585,17 @@ class Options( // doclava1 doc-related flags: only supported here to make this command a drop-in // replacement "-referenceonly", + "-devsite", + "-ignoreJdLinks", + "-nodefaultassets", + "-parsecomments", + "-offlinemode", + "-gcmref", + "-metadataDebug", + "-includePreview", + "-staticonly", + "-navtreeonly", + "-atLinksNavtree", "-nodocs" -> { javadoc(arg) } @@ -573,6 +608,16 @@ class Options( "-knowntags", "-resourcesdir", "-resourcesoutdir", + "-yaml", + "-apidocsdir", + "-toroot", + "-samplegroup", + "-samplesdir", + "-dac_libraryroot", + "-dac_dataname", + "-title", + "-proofread", + "-todo", "-overview" -> { javadoc(arg) index++ @@ -580,11 +625,19 @@ class Options( // doclava1 flags with two arguments "-federate", - "-federationapi" -> { + "-federationapi", + "-artifact", + "-htmldir2" -> { javadoc(arg) index += 2 } + // doclava1 flags with three arguments + "-samplecode" -> { + javadoc(arg) + index += 3 + } + // doclava1 flag with variable number of arguments; skip everything until next arg "-hdf" -> { javadoc(arg) @@ -635,8 +688,23 @@ class Options( else yesNo(arg.substring(ARGS_COMPAT_OUTPUT.length + 1)) } else if (arg.startsWith("-")) { - val usage = getUsage(includeHeader = false, colorize = color) - throw DriverException(stderr = "Invalid argument $arg\n\n$usage") + // Compatibility flag; map to mutable properties in the Compatibility + // class and assign it + val compatibilityArg = findCompatibilityFlag(arg) + if (compatibilityArg != null) { + val dash = arg.indexOf('=') + val value = if (dash == -1) { + true + } else { + arg.substring(dash + 1).toBoolean() + } + compatibilityArg.set(compatibility, value) + } else { + // Some other argument: display usage info and exit + + val usage = getUsage(includeHeader = false, colorize = color) + throw DriverException(stderr = "Invalid argument $arg\n\n$usage") + } } else { // All args that don't start with "-" are taken to be filenames mutableSources.addAll(stringToExistingFiles(arg)) @@ -685,6 +753,20 @@ class Options( checkFlagConsistency() } + private fun findCompatibilityFlag(arg: String): KMutableProperty1<Compatibility, Boolean>? { + val index = arg.indexOf('=') + val name = arg + .substring(0, if (index != -1) index else arg.length) + .removePrefix("--") + .replace('-', '_') + val propertyName = SdkVersionInfo.underlinesToCamelCase(name).decapitalize() + return Compatibility::class.memberProperties + .filterIsInstance<KMutableProperty1<Compatibility, Boolean>>() + .find { + it.name == propertyName + } + } + private fun findAndroidJars( androidJarPatterns: List<String>, currentApiLevel: Int, currentCodeName: String?, currentJar: File? @@ -802,7 +884,6 @@ class Options( else -> "" } reporter.report(Severity.WARNING, null as String?, message, color = color) - } } @@ -1011,6 +1092,13 @@ class Options( ARG_SHOW_ANNOTATION + " <annotation class>", "Include the given annotation in the API analysis", ARG_SHOW_UNANNOTATED, "Include un-annotated public APIs in the signature file as well", + "", "\nDocumentation:", + ARG_PUBLIC, "Only include elements that are public", + ARG_PROTECTED, "Only include elements that are public or protected", + ARG_PACKAGE, "Only include elements that are public, protected or package protected", + ARG_PRIVATE, "Include all elements except those that are marked hidden", + ARG_HIDDEN, "INclude all elements, including hidden", + "", "\nExtracting Signature Files:", // TODO: Document --show-annotation! ARG_API + " <file>", "Generate a signature descriptor file", @@ -1030,6 +1118,7 @@ class Options( "@NonNull.", ARG_PROGUARD + " <file>", "Write a ProGuard keep file for the API", + ARG_SDK_VALUES + " <dir>", "Write SDK values files to the given directory", "", "\nGenerating Stubs:", ARG_STUBS + " <dir>", "Generate stub source files for the API", @@ -1145,4 +1234,11 @@ class Options( i += 2 } } + + companion object { + /** Whether we should use [Compatibility] mode */ + fun useCompatMode(args: Array<String>): Boolean { + return COMPAT_MODE_BY_DEFAULT && !args.contains("$ARGS_COMPAT_OUTPUT=no") + } + } } diff --git a/src/main/java/com/android/tools/metalava/SdkFileWriter.kt b/src/main/java/com/android/tools/metalava/SdkFileWriter.kt new file mode 100644 index 0000000..c6980c5 --- /dev/null +++ b/src/main/java/com/android/tools/metalava/SdkFileWriter.kt @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2018 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 + +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.FieldItem +import java.io.BufferedWriter +import java.io.File +import java.io.FileWriter +import java.io.IOException + +// Ported from doclava1 + +private const val ANDROID_VIEW_VIEW = "android.view.View" +private const val ANDROID_VIEW_VIEW_GROUP = "android.view.ViewGroup" +private const val ANDROID_VIEW_VIEW_GROUP_LAYOUT_PARAMS = "android.view.ViewGroup.LayoutParams" +private const val SDK_CONSTANT_ANNOTATION = "android.annotation.SdkConstant" +private const val SDK_CONSTANT_TYPE_ACTIVITY_ACTION = + "android.annotation.SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION" +private const val SDK_CONSTANT_TYPE_BROADCAST_ACTION = + "android.annotation.SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION" +private const val SDK_CONSTANT_TYPE_SERVICE_ACTION = + "android.annotation.SdkConstant.SdkConstantType.SERVICE_ACTION" +private const val SDK_CONSTANT_TYPE_CATEGORY = "android.annotation.SdkConstant.SdkConstantType.INTENT_CATEGORY" +private const val SDK_CONSTANT_TYPE_FEATURE = "android.annotation.SdkConstant.SdkConstantType.FEATURE" +private const val SDK_WIDGET_ANNOTATION = "android.annotation.Widget" +private const val SDK_LAYOUT_ANNOTATION = "android.annotation.Layout" + +private const val TYPE_NONE = 0 +private const val TYPE_WIDGET = 1 +private const val TYPE_LAYOUT = 2 +private const val TYPE_LAYOUT_PARAM = 3 + +/** + * Writes various SDK metadata files packaged with the SDK, such as + * {@code platforms/android-27/data/features.txt} + */ +class SdkFileWriter(val codebase: Codebase, private val outputDir: java.io.File) { + /** + * Collect the values used by the Dev tools and write them in files packaged with the SDK + */ + fun generate() { + val activityActions = mutableListOf<String>() + val broadcastActions = mutableListOf<String>() + val serviceActions = mutableListOf<String>() + val categories = mutableListOf<String>() + val features = mutableListOf<String>() + val layouts = mutableListOf<ClassItem>() + val widgets = mutableListOf<ClassItem>() + val layoutParams = mutableListOf<ClassItem>() + + val classes = codebase.getPackages().allClasses() + + // The topmost LayoutParams class - android.view.ViewGroup.LayoutParams + var topLayoutParams: ClassItem? = null + + // Go through all the fields of all the classes, looking SDK stuff. + for (clazz in classes) { + // first check constant fields for the SdkConstant annotation. + val fields = clazz.fields() + for (field in fields) { + val value = field.initialValue() ?: continue + val annotations = field.modifiers.annotations() + for (annotation in annotations) { + if (SDK_CONSTANT_ANNOTATION == annotation.qualifiedName()) { + val resolved = + annotation.findAttribute(null)?.leafValues()?.firstOrNull()?.resolve() as? FieldItem + ?: continue + val type = resolved.containingClass().qualifiedName() + "." + resolved.name() + when { + SDK_CONSTANT_TYPE_ACTIVITY_ACTION == type -> activityActions.add(value.toString()) + SDK_CONSTANT_TYPE_BROADCAST_ACTION == type -> broadcastActions.add(value.toString()) + SDK_CONSTANT_TYPE_SERVICE_ACTION == type -> serviceActions.add(value.toString()) + SDK_CONSTANT_TYPE_CATEGORY == type -> categories.add(value.toString()) + SDK_CONSTANT_TYPE_FEATURE == type -> features.add(value.toString()) + } + } + } + } + + // Now check the class for @Widget or if its in the android.widget package + // (unless the class is hidden or abstract, or non public) + if (!clazz.isHiddenOrRemoved() && clazz.isPublic && !clazz.modifiers.isAbstract()) { + var annotated = false + val annotations = clazz.modifiers.annotations() + if (!annotations.isEmpty()) { + for (annotation in annotations) { + if (SDK_WIDGET_ANNOTATION == annotation.qualifiedName()) { + widgets.add(clazz) + annotated = true + break + } else if (SDK_LAYOUT_ANNOTATION == annotation.qualifiedName()) { + layouts.add(clazz) + annotated = true + break + } + } + } + + if (!annotated) { + if (topLayoutParams == null && ANDROID_VIEW_VIEW_GROUP_LAYOUT_PARAMS == clazz.qualifiedName()) { + topLayoutParams = clazz + } + // let's check if this is inside android.widget or android.view + if (isIncludedPackage(clazz)) { + // now we check what this class inherits either from android.view.ViewGroup + // or android.view.View, or android.view.ViewGroup.LayoutParams + val type = checkInheritance(clazz) + when (type) { + TYPE_WIDGET -> widgets.add(clazz) + TYPE_LAYOUT -> layouts.add(clazz) + TYPE_LAYOUT_PARAM -> layoutParams.add(clazz) + } + } + } + } + } + + // now write the files, whether or not the list are empty. + // the SDK built requires those files to be present. + + activityActions.sort() + writeValues("activity_actions.txt", activityActions) + + broadcastActions.sort() + writeValues("broadcast_actions.txt", broadcastActions) + + serviceActions.sort() + writeValues("service_actions.txt", serviceActions) + + categories.sort() + writeValues("categories.txt", categories) + + features.sort() + writeValues("features.txt", features) + + // Before writing the list of classes, we do some checks, to make sure the layout params + // are enclosed by a layout class (and not one that has been declared as a widget) + var i = 0 + while (i < layoutParams.size) { + var clazz: ClassItem? = layoutParams[i] + val containingClass = clazz?.containingClass() + var remove = containingClass == null || layouts.indexOf(containingClass) == -1 + // Also ensure that super classes of the layout params are in android.widget or android.view. + while (!remove && clazz != null) { + clazz = clazz.superClass() ?: break + if (clazz == topLayoutParams) { + break + } + remove = !isIncludedPackage(clazz) + } + if (remove) { + layoutParams.removeAt(i) + } else { + i++ + } + } + + writeClasses("widgets.txt", widgets, layouts, layoutParams) + } + + /** + * Check if the clazz is in package android.view or android.widget + */ + private fun isIncludedPackage(clazz: ClassItem): Boolean { + val pckg = clazz.containingPackage().qualifiedName() + return "android.widget" == pckg || "android.view" == pckg + } + + /** + * Writes a list of values into a text files. + * + * @param name the name of the file to write in the SDK directory + * @param values the list of values to write. + */ + private fun writeValues(name: String, values: List<String>) { + val pathname = File(outputDir, name) + var fw: FileWriter? = null + var bw: BufferedWriter? = null + try { + fw = FileWriter(pathname, false) + bw = BufferedWriter(fw) + + for (value in values) { + bw.append(value).append('\n') + } + } catch (e: IOException) { + // pass for now + } finally { + try { + if (bw != null) bw.close() + } catch (e: IOException) { + // pass for now + } + + try { + if (fw != null) fw.close() + } catch (e: IOException) { + // pass for now + } + } + } + + /** + * Writes the widget/layout/layout param classes into a text files. + * + * @param name the name of the output file. + * @param widgets the list of widget classes to write. + * @param layouts the list of layout classes to write. + * @param layoutParams the list of layout param classes to write. + */ + private fun writeClasses( + name: String, + widgets: List<ClassItem>, + layouts: List<ClassItem>, + layoutParams: List<ClassItem> + ) { + var fw: FileWriter? = null + var bw: BufferedWriter? = null + try { + val pathname = File(outputDir, name) + fw = FileWriter(pathname, false) + bw = BufferedWriter(fw) + + // write the 3 types of classes. + for (clazz in widgets) { + writeClass(bw, clazz, 'W') + } + for (clazz in layoutParams) { + writeClass(bw, clazz, 'P') + } + for (clazz in layouts) { + writeClass(bw, clazz, 'L') + } + } catch (ignore: IOException) { + } finally { + bw?.close() + fw?.close() + } + } + + /** + * Writes a class name and its super class names into a [BufferedWriter]. + * + * @param writer the BufferedWriter to write into + * @param clazz the class to write + * @param prefix the prefix to put at the beginning of the line. + * @throws IOException + */ + @Throws(IOException::class) + private fun writeClass(writer: BufferedWriter, clazz: ClassItem, prefix: Char) { + writer.append(prefix).append(clazz.qualifiedName()) + var superClass: ClassItem? = clazz.superClass() + while (superClass != null) { + writer.append(' ').append(superClass.qualifiedName()) + superClass = superClass.superClass() + } + writer.append('\n') + } + + /** + * Checks the inheritance of [ClassItem] objects. This method return + * + * * [.TYPE_LAYOUT]: if the class extends `android.view.ViewGroup` + * * [.TYPE_WIDGET]: if the class extends `android.view.View` + * * [.TYPE_LAYOUT_PARAM]: if the class extends + * `android.view.ViewGroup$LayoutParams` + * * [.TYPE_NONE]: in all other cases + * + * @param clazz the [ClassItem] to check. + */ + private fun checkInheritance(clazz: ClassItem): Int { + return when { + ANDROID_VIEW_VIEW_GROUP == clazz.qualifiedName() -> TYPE_LAYOUT + ANDROID_VIEW_VIEW == clazz.qualifiedName() -> TYPE_WIDGET + ANDROID_VIEW_VIEW_GROUP_LAYOUT_PARAMS == clazz.qualifiedName() -> TYPE_LAYOUT_PARAM + else -> { + val parent = clazz.superClass() + if (parent != null) { + checkInheritance(parent) + } else TYPE_NONE + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/StubWriter.kt b/src/main/java/com/android/tools/metalava/StubWriter.kt index a1a9534..6c21923 100644 --- a/src/main/java/com/android/tools/metalava/StubWriter.kt +++ b/src/main/java/com/android/tools/metalava/StubWriter.kt @@ -24,6 +24,7 @@ import com.android.tools.metalava.model.Codebase import com.android.tools.metalava.model.ConstructorItem import com.android.tools.metalava.model.FieldItem import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.MemberItem import com.android.tools.metalava.model.MethodItem import com.android.tools.metalava.model.ModifierList import com.android.tools.metalava.model.PackageItem @@ -133,7 +134,8 @@ class StubWriter( ModifierList.writeAnnotations( list = pkg.modifiers, separateLines = true, - writer = writer) + writer = writer + ) writer.println("package ${pkg.qualifiedName()};") @@ -194,8 +196,13 @@ class StubWriter( } compilationUnit?.getImportStatements(filterReference)?.let { - for (importedClass in it) { - writer.println("import $importedClass;") + for (item in it) { + when (item) { + is ClassItem -> + writer.println("import ${item.qualifiedName()};") + is MemberItem -> + writer.println("import static ${item.containingClass().qualifiedName()}.${item.name()};") + } } writer.println() } @@ -248,7 +255,7 @@ class StubWriter( } private fun appendDocumentation(item: Item, writer: PrintWriter) { - val documentation = item.fullyQualifiedDocumentation() + val documentation = item.documentation if (documentation.isNotBlank()) { val trimmed = trimDocIndent(documentation) writer.println(trimmed) @@ -282,7 +289,7 @@ class StubWriter( removeFinal: Boolean = false, addPublic: Boolean = false ) { - if (item.deprecated) { + if (item.deprecated && generateAnnotations) { writer.write("@Deprecated ") } @@ -463,7 +470,7 @@ class StubWriter( if (isEnum && (method.name() == "values" || method.name() == "valueOf" && method.parameters().size == 1 && - method.parameters()[0].type().toTypeString() == "java.lang.String") + method.parameters()[0].type().toTypeString() == JAVA_LANG_STRING) ) { // Skip the values() and valueOf(String) methods in enums: these are added by // the compiler for enums anyway, but was part of the doclava1 signature files @@ -482,7 +489,7 @@ class StubWriter( generateTypeParameterList(typeList = method.typeParameterList(), addSpace = true) val returnType = method.returnType() - writer.print(returnType?.toTypeString(outerAnnotations = false, innerAnnotations = true)) + writer.print(returnType?.toTypeString(outerAnnotations = false, innerAnnotations = generateAnnotations)) writer.print(' ') writer.print(method.name()) @@ -508,7 +515,7 @@ class StubWriter( appendDocumentation(field, writer) appendModifiers(field, false, false) - writer.print(field.type().toTypeString(outerAnnotations = false, innerAnnotations = true)) + writer.print(field.type().toTypeString(outerAnnotations = false, innerAnnotations = generateAnnotations)) writer.print(' ') writer.print(field.name()) val needsInitialization = @@ -539,7 +546,12 @@ class StubWriter( writer.print(", ") } appendModifiers(parameter, false) - writer.print(parameter.type().toTypeString(outerAnnotations = false, innerAnnotations = true)) + writer.print( + parameter.type().toTypeString( + outerAnnotations = false, + innerAnnotations = generateAnnotations + ) + ) writer.print(' ') val name = parameter.publicName() ?: parameter.name() writer.print(name) diff --git a/src/main/java/com/android/tools/metalava/doclava1/ApiFile.java b/src/main/java/com/android/tools/metalava/doclava1/ApiFile.java index d4b03f9..2bc9cc0 100644 --- a/src/main/java/com/android/tools/metalava/doclava1/ApiFile.java +++ b/src/main/java/com/android/tools/metalava/doclava1/ApiFile.java @@ -36,6 +36,9 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import static com.android.tools.metalava.ConstantsKt.JAVA_LANG_ANNOTATION; +import static com.android.tools.metalava.ConstantsKt.JAVA_LANG_ENUM; +import static com.android.tools.metalava.ConstantsKt.JAVA_LANG_STRING; import static com.android.tools.metalava.model.FieldItemKt.javaUnescapeString; // @@ -192,7 +195,7 @@ public class ApiFile { } else if ("enum".equals(token)) { isEnum = true; fin = true; - ext = "java.lang.Enum"; + ext = JAVA_LANG_ENUM; token = tokenizer.requireToken(); } else { throw new ApiParseException("missing class or interface. got: " + token, tokenizer.getLine()); @@ -233,11 +236,11 @@ public class ApiFile { } } } - if ("java.lang.Enum".equals(ext)) { + if (JAVA_LANG_ENUM.equals(ext)) { cl.setIsEnum(true); } else if (isAnnotation) { - api.mapClassToInterface(cl, "java.lang.annotation.Annotation"); - } else if (api.implementsInterface(cl, "java.lang.annotation.Annotation")) { + api.mapClassToInterface(cl, JAVA_LANG_ANNOTATION); + } else if (api.implementsInterface(cl, JAVA_LANG_ANNOTATION)) { cl.setIsAnnotationType(true); } if (!"{".equals(token)) { @@ -674,7 +677,7 @@ public class ApiFile { } } else if ("char".equals(type)) { return (char) Integer.parseInt(val); - } else if ("java.lang.String".equals(type)) { + } else if (JAVA_LANG_STRING.equals(type)) { if ("null".equals(val)) { return null; } else { @@ -751,7 +754,7 @@ public class ApiFile { if ("=".equals(token)) { token = tokenizer.requireToken(false); try { - defaultValue = (String) parseValue("java.lang.String", token); + defaultValue = (String) parseValue(JAVA_LANG_STRING, token); } catch (ApiParseException ex) { ex.line = tokenizer.getLine(); throw ex; diff --git a/src/main/java/com/android/tools/metalava/doclava1/ApiInfo.kt b/src/main/java/com/android/tools/metalava/doclava1/ApiInfo.kt index 1993aa5..c9b042a 100644 --- a/src/main/java/com/android/tools/metalava/doclava1/ApiInfo.kt +++ b/src/main/java/com/android/tools/metalava/doclava1/ApiInfo.kt @@ -19,6 +19,7 @@ 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_OBJECT import com.android.tools.metalava.model.AnnotationItem import com.android.tools.metalava.model.ClassItem import com.android.tools.metalava.model.Codebase @@ -118,7 +119,7 @@ class ApiInfo : DefaultCodebase() { } var scName: String? = mClassToSuper[cl] if (scName == null) { - scName = "java.lang.Object" + scName = JAVA_LANG_OBJECT } var superclass: TextClassItem? = mAllClasses[scName] if (superclass == null) { @@ -153,7 +154,7 @@ class ApiInfo : DefaultCodebase() { // java.lang.Object has no superclass var scName: String? = mClassToSuper[cl] if (scName == null) { - scName = "java.lang.Object" + scName = JAVA_LANG_OBJECT } var superclass: TextClassItem? = mAllClasses[scName] if (superclass == null) { diff --git a/src/main/java/com/android/tools/metalava/doclava1/ApiPredicate.kt b/src/main/java/com/android/tools/metalava/doclava1/ApiPredicate.kt index 4b5af4c..dfbcaee 100644 --- a/src/main/java/com/android/tools/metalava/doclava1/ApiPredicate.kt +++ b/src/main/java/com/android/tools/metalava/doclava1/ApiPredicate.kt @@ -56,7 +56,7 @@ class ApiPredicate( return false } - var visible = member.isPublic || member.isProtected + var visible = member.isPublic || member.isProtected // TODO: Should this use checkLevel instead? var hidden = member.hidden if (!visible || hidden) { return false 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 9e50d2d..9f1bbd8 100644 --- a/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt +++ b/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt @@ -18,6 +18,8 @@ package com.android.tools.metalava.model import com.android.SdkConstants import com.android.SdkConstants.ATTR_VALUE +import com.android.tools.metalava.ANDROID_SUPPORT_ANNOTATION_PREFIX +import com.android.tools.metalava.JAVA_LANG_PREFIX import com.android.tools.metalava.NEWLY_NONNULL import com.android.tools.metalava.NEWLY_NULLABLE import com.android.tools.metalava.Options @@ -98,7 +100,7 @@ interface AnnotationItem { companion object { /** Whether the given annotation name is "significant", e.g. should be included in signature files */ fun isSignificantAnnotation(qualifiedName: String?): Boolean { - return qualifiedName?.startsWith("android.support.annotation.") ?: false + return qualifiedName?.startsWith(ANDROID_SUPPORT_ANNOTATION_PREFIX) ?: false } /** The simple name of an annotation, which is the annotation name (not qualified name) prefixed by @ */ @@ -184,7 +186,6 @@ interface AnnotationItem { // These aren't support annotations "android.annotation.AppIdInt", "android.annotation.BroadcastBehavior", - "android.annotation.SdkConstant", "android.annotation.SuppressAutoDoc", "android.annotation.SystemApi", "android.annotation.TestApi", @@ -208,6 +209,8 @@ interface AnnotationItem { } // Included for analysis, but should not be exported: + "android.annotation.SdkConstant", + "android.annotation.RequiresFeature", "android.annotation.SystemService" -> return qualifiedName // Should not be mapped to a different package name: @@ -230,8 +233,8 @@ interface AnnotationItem { isNonNullAnnotation(qualifiedName) -> "android.support.annotation.NonNull" // Support library annotations are all included, as is the built-in stuff like @Retention - qualifiedName.startsWith("android.support.annotation.") -> return qualifiedName - qualifiedName.startsWith("java.lang.") -> return qualifiedName + qualifiedName.startsWith(ANDROID_SUPPORT_ANNOTATION_PREFIX) -> return qualifiedName + qualifiedName.startsWith(JAVA_LANG_PREFIX) -> return qualifiedName // Unknown Android platform annotations qualifiedName.startsWith("android.annotation.") -> { @@ -276,7 +279,7 @@ interface AnnotationItem { source.startsWith("android.annotation.", 1) -> { "@" + source.substring("@android.annotation.".length) } - source.startsWith("android.support.annotation.", 1) -> { + source.startsWith(ANDROID_SUPPORT_ANNOTATION_PREFIX, 1) -> { "@" + source.substring("@android.support.annotation.".length) } else -> source 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 ecfa8cd..ae74cc1 100644 --- a/src/main/java/com/android/tools/metalava/model/ClassItem.kt +++ b/src/main/java/com/android/tools/metalava/model/ClassItem.kt @@ -17,6 +17,7 @@ package com.android.tools.metalava.model import com.android.tools.metalava.ApiAnalyzer +import com.android.tools.metalava.JAVA_LANG_OBJECT import com.android.tools.metalava.compatibility import com.android.tools.metalava.model.visitors.ApiVisitor import com.android.tools.metalava.model.visitors.ItemVisitor @@ -169,7 +170,7 @@ interface ClassItem : Item { fun typeArgumentClasses(): List<ClassItem> = TODO("Not yet implemented") fun isJavaLangObject(): Boolean { - return qualifiedName() == "java.lang.Object" + return qualifiedName() == JAVA_LANG_OBJECT } // Mutation APIs: Used to "fix up" the API hierarchy (in [ApiAnalyzer]) to only expose @@ -296,7 +297,7 @@ interface ClassItem : Item { } companion object { - // Same as doclava1 (modulo the new handling when class names match + // Same as doclava1 (modulo the new handling when class names match) val comparator: Comparator<in ClassItem> = Comparator { o1, o2 -> val delta = o1.fullName().compareTo(o2.fullName()) if (delta == 0) { @@ -653,7 +654,6 @@ class VisitCandidate(private val cls: ClassItem, private val visitor: ApiVisitor fields } - for (constructor in sortedConstructors) { constructor.accept(visitor) } diff --git a/src/main/java/com/android/tools/metalava/model/CompilationUnit.kt b/src/main/java/com/android/tools/metalava/model/CompilationUnit.kt index 6f75493..37529de 100644 --- a/src/main/java/com/android/tools/metalava/model/CompilationUnit.kt +++ b/src/main/java/com/android/tools/metalava/model/CompilationUnit.kt @@ -31,5 +31,5 @@ open class CompilationUnit( override fun toString(): String = "compilation unit ${file.virtualFile?.path}" - open fun getImportStatements(predicate: Predicate<Item>): Collection<String>? = null + open fun getImportStatements(predicate: Predicate<Item>): Collection<Item> = emptyList() } \ No newline at end of file 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 1dc8fd1..028960f 100644 --- a/src/main/java/com/android/tools/metalava/model/Item.kt +++ b/src/main/java/com/android/tools/metalava/model/Item.kt @@ -175,12 +175,8 @@ interface Item { fun hasShowAnnotation(): Boolean = modifiers.hasShowAnnotation() fun hasHideAnnotation(): Boolean = modifiers.hasHideAnnotations() - // TODO: Cache? fun checkLevel(): Boolean { - if (isHiddenOrRemoved()) { - return false - } - return modifiers.isPublic() || modifiers.isProtected() + return modifiers.checkLevel() } fun compilationUnit(): CompilationUnit? { 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 6195682..edb325a 100644 --- a/src/main/java/com/android/tools/metalava/model/ModifierList.kt +++ b/src/main/java/com/android/tools/metalava/model/ModifierList.kt @@ -16,6 +16,12 @@ package com.android.tools.metalava.model +import com.android.tools.metalava.DocLevel +import com.android.tools.metalava.DocLevel.HIDDEN +import com.android.tools.metalava.DocLevel.PACKAGE +import com.android.tools.metalava.DocLevel.PRIVATE +import com.android.tools.metalava.DocLevel.PROTECTED +import com.android.tools.metalava.DocLevel.PUBLIC import com.android.tools.metalava.Options import com.android.tools.metalava.compatibility import com.android.tools.metalava.options @@ -118,6 +124,27 @@ interface ModifierList { } } + /** Returns true if this modifier list has adequate access */ + fun checkLevel() = checkLevel(options.docLevel) + + /** + * Returns true if this modifier list has access modifiers that + * are adequate for the given documentation level + */ + fun checkLevel(level: DocLevel): Boolean { + if (level == HIDDEN) { + return true + } else if (owner().isHiddenOrRemoved()) { + return false + } + return when (level) { + PUBLIC -> isPublic() + PROTECTED -> isPublic() || isProtected() + PACKAGE -> !isPrivate() + PRIVATE, HIDDEN -> true + } + } + companion object { fun write( writer: Writer, 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 937b5ec..072e4e8 100644 --- a/src/main/java/com/android/tools/metalava/model/TypeItem.kt +++ b/src/main/java/com/android/tools/metalava/model/TypeItem.kt @@ -17,6 +17,8 @@ package com.android.tools.metalava.model import com.android.tools.lint.detector.api.ClassContext +import com.android.tools.metalava.JAVA_LANG_OBJECT +import com.android.tools.metalava.JAVA_LANG_PREFIX import com.android.tools.metalava.compatibility import com.android.tools.metalava.options import java.util.function.Predicate @@ -53,7 +55,7 @@ interface TypeItem { fun asClass(): ClassItem? fun toSimpleType(): String { - return toTypeString().replace("java.lang.", "") + return stripJavaLangPrefix(toTypeString()) } val primitive: Boolean @@ -76,7 +78,7 @@ interface TypeItem { } fun isJavaLangObject(): Boolean { - return toTypeString() == "java.lang.Object" + return toTypeString() == JAVA_LANG_OBJECT } fun defaultValue(): Any? { @@ -115,21 +117,28 @@ interface TypeItem { fun isTypeParameter(): Boolean = toTypeString().length == 1 // heuristic; accurate implementation in PSI subclass companion object { - private const val JAVA_LANG_PREFIX = "java.lang." - private const val ANDROID_SUPPORT_ANNOTATION_PREFIX = "@android.support.annotation." - + /** Shortens types, if configured */ fun shortenTypes(type: String): String { - if (options.omitCommonPackages && - (type.contains("java.lang.") || - type.contains("@android.support.annotation.")) - ) { + if (options.omitCommonPackages) { var cleaned = type - if (options.omitCommonPackages) { - if (cleaned.contains(ANDROID_SUPPORT_ANNOTATION_PREFIX)) { - cleaned = cleaned.replace(ANDROID_SUPPORT_ANNOTATION_PREFIX, "@") - } + if (cleaned.contains("@android.support.annotation.")) { + cleaned = cleaned.replace("@android.support.annotation.", "@") } + return stripJavaLangPrefix(cleaned) + } + + return type + } + + /** + * Removes java.lang. prefixes from types, unless it's in a subpackage such + * as java.lang.reflect + */ + fun stripJavaLangPrefix(type: String): String { + if (type.contains(JAVA_LANG_PREFIX)) { + var cleaned = type + // Replacing java.lang is harder, since we don't want to operate in sub packages, // e.g. java.lang.String -> String, but java.lang.reflect.Method -> unchanged var index = cleaned.indexOf(JAVA_LANG_PREFIX) 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 83a90df..02fe52d 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 @@ -558,6 +558,10 @@ open class PsiBasedCodebase(override var description: String = "Unknown") : Defa val name = top.name val fullName = top.qualifiedName ?: return "" + if (name == fullName) { + return "" + } + return fullName.substring(0, fullName.length - 1 - name!!.length) } diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt index c7524a0..7b949d3 100644 --- a/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt @@ -28,20 +28,13 @@ import com.google.common.base.Splitter import com.intellij.lang.jvm.types.JvmReferenceType import com.intellij.psi.PsiClass import com.intellij.psi.PsiClassType -import com.intellij.psi.PsiComment import com.intellij.psi.PsiCompiledFile -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiJavaFile import com.intellij.psi.PsiModifier import com.intellij.psi.PsiModifierListOwner import com.intellij.psi.PsiType import com.intellij.psi.PsiTypeParameter -import com.intellij.psi.PsiWhiteSpace import com.intellij.psi.impl.source.PsiClassReferenceType import com.intellij.psi.util.PsiUtil -import org.jetbrains.kotlin.kdoc.psi.api.KDoc -import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.psi.psiUtil.startOffset class PsiClassItem( override val codebase: PsiBasedCodebase, @@ -206,34 +199,7 @@ class PsiClassItem( return null } - return object : CompilationUnit(containingFile) { - override fun getHeaderComments(): String? { - // https://youtrack.jetbrains.com/issue/KT-22135 - if (file is PsiJavaFile) { - val pkg = file.packageStatement ?: return null - return file.text.substring(0, pkg.startOffset) - } else if (file is KtFile) { - var curr: PsiElement? = file.firstChild - var comment: String? = null - while (curr != null) { - if (curr is PsiComment || curr is KDoc) { - val text = curr.text - comment = if (comment != null) { - comment + "\n" + text - } else { - text - } - } else if (curr !is PsiWhiteSpace) { - break - } - curr = curr.nextSibling - } - return comment - } - - return super.getHeaderComments() - } - } + return PsiCompilationUnit(codebase, containingFile) } fun findMethod(template: MethodItem): MethodItem? { @@ -475,7 +441,7 @@ class PsiClassItem( } } - if (hasImplicitDefaultConstructor ) { + if (hasImplicitDefaultConstructor) { assert(constructors.isEmpty()) constructors.add(PsiConstructorItem.createDefaultConstructor(codebase, item, psiClass)) } @@ -553,8 +519,8 @@ class PsiClassItem( newClass.fields = classFilter.fields.asSequence() // Preserve sorting order for enums .sortedBy { it.sortingRank }.map { - PsiFieldItem.create(codebase, newClass, it as PsiFieldItem) - }.toMutableList() + PsiFieldItem.create(codebase, newClass, it as PsiFieldItem) + }.toMutableList() newClass.innerClasses = classFilter.innerClasses.map { diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiCompilationUnit.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiCompilationUnit.kt new file mode 100644 index 0000000..f70d21c --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiCompilationUnit.kt @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2018 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.psi + +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.CompilationUnit +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.MemberItem +import com.android.tools.metalava.model.visitors.ItemVisitor +import com.google.common.collect.ArrayListMultimap +import com.google.common.collect.Multimap +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiClassOwner +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiJavaFile +import com.intellij.psi.PsiWhiteSpace +import org.jetbrains.kotlin.kdoc.psi.api.KDoc +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.psiUtil.startOffset +import java.util.function.Predicate + +class PsiCompilationUnit(val codebase: PsiBasedCodebase, containingFile: PsiFile) : CompilationUnit(containingFile) { + override fun getHeaderComments(): String? { + // https://youtrack.jetbrains.com/issue/KT-22135 + if (file is PsiJavaFile) { + val pkg = file.packageStatement ?: return null + return file.text.substring(0, pkg.startOffset) + } else if (file is KtFile) { + var curr: PsiElement? = file.firstChild + var comment: String? = null + while (curr != null) { + if (curr is PsiComment || curr is KDoc) { + val text = curr.text + comment = if (comment != null) { + comment + "\n" + text + } else { + text + } + } else if (curr !is PsiWhiteSpace) { + break + } + curr = curr.nextSibling + } + return comment + } + + return super.getHeaderComments() + } + + override fun getImportStatements(predicate: Predicate<Item>): Collection<Item> { + val imports = mutableListOf<Item>() + + if (file is PsiJavaFile) { + // TODO: Do we need to deal with static imports? + val importList = file.importList + if (importList != null) { + for (importStatement in importList.importStatements) { + val resolved = importStatement.resolve() ?: continue + if (resolved is PsiClass) { + val classItem = codebase.findClass(resolved) ?: continue + if (predicate.test(classItem)) { + imports.add(classItem) + } + } + } + } + + for (psiClass in file.classes) { + val classItem = codebase.findClass(psiClass) ?: continue + if (predicate.test(classItem)) { + + } + + } + } else if (file is KtFile) { + for (importDirective in file.importDirectives) { + val resolved = importDirective.reference?.resolve() ?: continue + if (resolved is PsiClass) { + val classItem = codebase.findClass(resolved) ?: continue + if (predicate.test(classItem)) { + imports.add(classItem) + } + } + } + } + + // Next only keep those that are present in any docs; those are the only ones + // we need to import + if (!imports.isEmpty()) { + val map: Multimap<String, Item> = ArrayListMultimap.create() + for (item in imports) { + if (item is ClassItem) { + map.put(item.simpleName(), item) + } else if (item is MemberItem) { + map.put(item.name(), item) + } + } + + // Compute set of import statements that are actually referenced + // from the documentation (we do inexact matching here; we don't + // need to have an exact set of imports since it's okay to have + // some extras) + val result = mutableListOf<Item>() + for (cls in classes(predicate)) { + cls.accept(object : ItemVisitor() { + override fun visitItem(item: Item) { + val doc = item.documentation + if (doc.isNotBlank()) { + var found: MutableList<String>? = null + for (name in map.keys()) { + if (doc.contains(name)) { + if (found == null) { + found = mutableListOf() + } + found.add(name) + } + } + found?.let { + for (name in found) { + val all = map.get(name) ?: continue + for (referenced in all) { + if (!result.contains(referenced)) { + result.add(referenced) + } + } + map.removeAll(name) + } + } + } + } + }) + } + + return result + } + + return emptyList() + } + + private fun classes(predicate: Predicate<Item>): List<ClassItem> { + val topLevel = mutableListOf<ClassItem>() + if (file is PsiClassOwner) { + for (psiClass in file.classes) { + val classItem = codebase.findClass(psiClass) ?: continue + if (predicate.test(classItem)) { + topLevel.add(classItem) + } + } + } + + return topLevel + } +} \ No newline at end of file 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 068d44b..d8621a7 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 @@ -215,7 +215,6 @@ abstract class PsiItem( } when (resolved) { - // TODO: If same package, do nothing // TODO: If not absolute, preserve syntax is PsiClass -> { if (samePackage(resolved)) { 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 89826e4..d711446 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 @@ -291,6 +291,13 @@ open class PsiMethodItem( val name = psiMethod.name val commentText = javadoc(psiMethod) val modifiers = modifiers(codebase, psiMethod, commentText) + if (modifiers.isFinal() && containingClass.modifiers.isFinal()) { + // The containing class is final, so it is implied that every method is final as well. + // No need to apply 'final' to each method. (We do it here rather than just in the + // signature emit code since we want to make sure that the signature comparison + // methods with super methods also consider this method non-final.) + modifiers.setFinal(false) + } val parameters = psiMethod.parameterList.parameters.mapIndexed { index, parameter -> PsiParameterItem.create(codebase, parameter, index) } diff --git a/src/test/java/com/android/tools/metalava/AnnotationStatisticsTest.kt b/src/test/java/com/android/tools/metalava/AnnotationStatisticsTest.kt index c9ba501..dc749c3 100644 --- a/src/test/java/com/android/tools/metalava/AnnotationStatisticsTest.kt +++ b/src/test/java/com/android/tools/metalava/AnnotationStatisticsTest.kt @@ -138,15 +138,12 @@ class AnnotationStatisticsTest : DriverTest() { 6 methods and fields were missing nullness annotations out of 7 total API references. API nullness coverage is 14% - |--------------------------------------------------------------|------------------| | Qualified Class Name | Usage Count | |--------------------------------------------------------------|-----------------:| | test.pkg.ApiSurface | 7 | - |--------------------------------------------------------------|------------------| Top referenced un-annotated members: - |--------------------------------------------------------------|------------------| | Member | Usage Count | |--------------------------------------------------------------|-----------------:| | ApiSurface.missing1(Object) | 2 | @@ -155,7 +152,6 @@ class AnnotationStatisticsTest : DriverTest() { | ApiSurface.missing3(int) | 1 | | ApiSurface.missing4(Object) | 1 | | ApiSurface.missing5(Object) | 1 | - |--------------------------------------------------------------|------------------| """, compatibilityMode = false ) diff --git a/src/test/java/com/android/tools/metalava/ApiFileTest.kt b/src/test/java/com/android/tools/metalava/ApiFileTest.kt index e0eb443..92a5a0c 100644 --- a/src/test/java/com/android/tools/metalava/ApiFileTest.kt +++ b/src/test/java/com/android/tools/metalava/ApiFileTest.kt @@ -168,7 +168,7 @@ class ApiFileTest : DriverTest() { package test.pkg { public final class Foo { ctor public Foo(); - method public final void error(int p = "42", Integer? int2 = "null"); + method public void error(int p = "42", Integer? int2 = "null"); } } """, @@ -216,10 +216,10 @@ class ApiFileTest : DriverTest() { package test.pkg { public final class Kotlin extends test.pkg.Parent { ctor public Kotlin(java.lang.String property1, int arg2); - method public final java.lang.String getProperty1(); - method public final java.lang.String getProperty2(); - method public final void otherMethod(boolean ok, int times); - method public final void setProperty2(java.lang.String p); + method public java.lang.String getProperty1(); + method public java.lang.String getProperty2(); + method public void otherMethod(boolean ok, int times); + method public void setProperty2(java.lang.String p); field public static final test.pkg.Kotlin.Companion Companion; field public static final int MY_CONST = 42; // 0x2a field public int someField2; @@ -237,9 +237,9 @@ class ApiFileTest : DriverTest() { privateApi = """ package test.pkg { public final class Kotlin extends test.pkg.Parent { - method internal final boolean getMyHiddenVar${"$"}lintWithKotlin(); - method internal final void myHiddenMethod${"$"}lintWithKotlin(); - method internal final void setMyHiddenVar${"$"}lintWithKotlin(boolean p); + method internal boolean getMyHiddenVar${"$"}lintWithKotlin(); + method internal void myHiddenMethod${"$"}lintWithKotlin(); + method internal void setMyHiddenVar${"$"}lintWithKotlin(boolean p); field internal boolean myHiddenVar; field private final java.lang.String property1; field private java.lang.String property2; @@ -250,7 +250,7 @@ class ApiFileTest : DriverTest() { } internal static final class Kotlin.myHiddenClass extends kotlin.Unit { ctor public Kotlin.myHiddenClass(); - method internal final test.pkg.Kotlin.myHiddenClass copy(); + method internal test.pkg.Kotlin.myHiddenClass copy(); } } """, @@ -396,8 +396,8 @@ class ApiFileTest : DriverTest() { } public final class NonNullableKotlinPair<F, S> { ctor public NonNullableKotlinPair(F first, S second); - method public final F getFirst(); - method public final S getSecond(); + method public F getFirst(); + method public S getSecond(); } public class NullableJavaPair<F, S> { ctor public NullableJavaPair(F?, S?); @@ -406,8 +406,8 @@ class ApiFileTest : DriverTest() { } public final class NullableKotlinPair<F, S> { ctor public NullableKotlinPair(F? first, S? second); - method public final F? getFirst(); - method public final S? getSecond(); + method public F? getFirst(); + method public S? getSecond(); } public class PlatformJavaPair<F, S> { ctor public PlatformJavaPair(F!, S!); @@ -416,7 +416,7 @@ class ApiFileTest : DriverTest() { } public final class TestKt { ctor public TestKt(); - method public static final operator <F, S> F! component1(androidx.util.PlatformJavaPair<F,S>); + method public static operator <F, S> F! component1(androidx.util.PlatformJavaPair<F,S>); } } """, @@ -704,6 +704,75 @@ class ApiFileTest : DriverTest() { ) } + @Test + fun `Do not include inherited public methods from private parents in compat mode`() { + // Real life example: StringBuilder.setLength, in compat mode + check( + compatibilityMode = true, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + public class MyStringBuilder extends AbstractMyStringBuilder { + } + """ + ), + java( + """ + package test.pkg; + class AbstractMyStringBuilder { + public void setLength(int length) { + } + } + """ + ) + ), + api = """ + package test.pkg { + public class MyStringBuilder { + ctor public MyStringBuilder(); + } + } + """ + ) + } + + @Test + fun `Include inherited public methods from private parents`() { + // In non-compat mode, include public methods from hidden parents too. + // Real life example: StringBuilder.setLength + // This is just like the above test, but with compat mode disabled. + check( + compatibilityMode = false, + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + public class MyStringBuilder extends AbstractMyStringBuilder { + } + """ + ), + java( + """ + package test.pkg; + class AbstractMyStringBuilder { + public void setLength(int length) { + } + } + """ + ) + ), + api = """ + package test.pkg { + public class MyStringBuilder { + ctor public MyStringBuilder(); + method public void setLength(int); + } + } + """ + ) + } + @Test fun `Annotation class extraction, non-compat mode`() { @Suppress("ConstantConditionIf") @@ -1368,7 +1437,53 @@ class ApiFileTest : DriverTest() { @Test fun `Inheriting from package private classes, package private class should be included`() { check( - checkDoclava1 = true, + checkDoclava1 = false, // doclava1 does not include method2, which it should + compatibilityMode = false, + sourceFiles = + *arrayOf( + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + public class MyClass extends HiddenParent { + public void method1() { } + } + """ + ), + java( + """ + package test.pkg; + @SuppressWarnings("ALL") + class HiddenParent { + public static final String CONSTANT = "MyConstant"; + protected int mContext; + public void method2() { } + } + """ + ) + ), + warnings = "", + api = """ + package test.pkg { + public class MyClass { + ctor public MyClass(); + method public void method1(); + method public void method2(); + } + } + """ + ) + } + + @Test + fun `Using compatibility flag manually`() { + // Like previous test, but using compatibility mode and explicitly turning on + // the hidden super class compatibility flag. This test is mostly intended + // to test the flag handling for individual compatibility flags. + check( + checkDoclava1 = false, // doclava1 does not include method2, which it should + compatibilityMode = true, + extraArguments = arrayOf("--include-public-methods-from-hidden-super-classes=true"), sourceFiles = *arrayOf( java( @@ -1398,6 +1513,7 @@ class ApiFileTest : DriverTest() { public class MyClass { ctor public MyClass(); method public void method1(); + method public void method2(); } } """ @@ -1877,7 +1993,7 @@ class ApiFileTest : DriverTest() { api = """ package test.pkg { public final class -Foo { - method public static final void printHelloWorld(java.lang.String); + method public static void printHelloWorld(java.lang.String); } } """ diff --git a/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt index efa6646..4a1a86a 100644 --- a/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt +++ b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt @@ -184,6 +184,7 @@ class DocAnalyzerTest : DriverTest() { stubs = arrayOf( """ package test.pkg; + import android.Manifest; @SuppressWarnings({"unchecked", "deprecation", "all"}) public class PermissionTest { public PermissionTest() { throw new RuntimeException("Stub!"); } @@ -917,7 +918,7 @@ class DocAnalyzerTest : DriverTest() { } @Test - fun `Merge API levels)`() { + fun `Merge API levels`() { check( sourceFiles = *arrayOf( java( @@ -977,7 +978,7 @@ class DocAnalyzerTest : DriverTest() { } @Test - fun `Merge deprecation levels)`() { + fun `Merge deprecation levels`() { check( sourceFiles = *arrayOf( java( @@ -1041,7 +1042,7 @@ class DocAnalyzerTest : DriverTest() { @Test - fun `Generate overview html docs)`() { + fun `Generate overview html docs`() { // If a codebase provides overview.html files in the a public package, // make sure that we include this in the exported stubs folder as well! check( @@ -1071,4 +1072,50 @@ class DocAnalyzerTest : DriverTest() { ) ) } + + @Test + fun `Check RequiresFeature handling`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + import android.annotation.RequiresFeature; + import android.content.pm.PackageManager; + @SuppressWarnings("WeakerAccess") + @RequiresFeature(PackageManager.FEATURE_LOCATION) + public class LocationManager { + } + """ + ), + java( + """ + package android.content.pm; + public abstract class PackageManager { + public static final String FEATURE_LOCATION = "android.hardware.location"; + } + """ + ), + + requiresFeatureSource + ), + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + import android.content.pm.PackageManager; + /** + * Requires the {@link android.content.pm.PackageManager#FEATURE_LOCATION PackageManager#FEATURE_LOCATION} feature which can be detected using {@link android.content.pm.PackageManager#hasSystemFeature(String) PackageManager.hasSystemFeature(String)}. + */ + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class LocationManager { + public LocationManager() { throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } + + } \ 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 2a5d0e7..e888878 100644 --- a/src/test/java/com/android/tools/metalava/DriverTest.kt +++ b/src/test/java/com/android/tools/metalava/DriverTest.kt @@ -187,7 +187,19 @@ abstract class DriverTest { /** Whether we should warn about super classes that are stripped because they are hidden */ includeStrippedSuperclassWarnings: Boolean = false, /** Apply level to XML */ - applyApiLevelsXml: String? = null + applyApiLevelsXml: String? = null, + /** Corresponds to SDK constants file broadcast_actions.txt */ + sdk_broadcast_actions: String? = null, + /** Corresponds to SDK constants file activity_actions.txt */ + sdk_activity_actions: String? = null, + /** Corresponds to SDK constants file service_actions.txt */ + sdk_service_actions: String? = null, + /** Corresponds to SDK constants file categories.txt */ + sdk_categories: String? = null, + /** Corresponds to SDK constants file features.txt */ + sdk_features: String? = null, + /** Corresponds to SDK constants file widgets.txt */ + sdk_widgets: String? = null ) { System.setProperty("METALAVA_TESTS_RUNNING", VALUE_TRUE) @@ -425,6 +437,23 @@ abstract class DriverTest { emptyArray() } + val sdkFilesDir: File? + val sdkFilesArgs: Array<String> + if (sdk_broadcast_actions != null || + sdk_activity_actions != null || + sdk_service_actions != null || + sdk_categories != null || + sdk_features != null || + sdk_widgets != null + ) { + val dir = File(project, "sdk-files") + sdkFilesArgs = arrayOf("--sdk-values", dir.path) + sdkFilesDir = dir + } else { + sdkFilesArgs = emptyArray() + sdkFilesDir = null + } + val actualOutput = runDriver( "--no-color", "--no-banner", @@ -435,6 +464,10 @@ abstract class DriverTest { //"--unhide-classpath-classes", "--allow-referencing-unknown-classes", + // Annotation generation temporarily turned off by default while integrating with + // SDK builds; tests need these + "--include-annotations", + "--sourcepath", sourcePath, "--classpath", @@ -462,6 +495,7 @@ abstract class DriverTest { *applyApiLevelsXmlArgs, *showAnnotationArguments, *showUnannotatedArgs, + *sdkFilesArgs, *importedPackageArgs.toTypedArray(), *skipEmitPackagesArgs.toTypedArray(), *extraArguments, @@ -524,6 +558,36 @@ abstract class DriverTest { assertEquals(stripComments(proguard, stripLineComments = false).trimIndent(), expectedProguard.trim()) } + if (sdk_broadcast_actions != null) { + val actual = readFile(File(sdkFilesDir, "broadcast_actions.txt"), stripBlankLines, trim) + assertEquals(sdk_broadcast_actions.trimIndent().trim(), actual.trim()) + } + + if (sdk_activity_actions != null) { + val actual = readFile(File(sdkFilesDir, "activity_actions.txt"), stripBlankLines, trim) + assertEquals(sdk_activity_actions.trimIndent().trim(), actual.trim()) + } + + if (sdk_service_actions != null) { + val actual = readFile(File(sdkFilesDir, "service_actions.txt"), stripBlankLines, trim) + assertEquals(sdk_service_actions.trimIndent().trim(), actual.trim()) + } + + if (sdk_categories != null) { + val actual = readFile(File(sdkFilesDir, "categories.txt"), stripBlankLines, trim) + assertEquals(sdk_categories.trimIndent().trim(), actual.trim()) + } + + if (sdk_features != null) { + val actual = readFile(File(sdkFilesDir, "features.txt"), stripBlankLines, trim) + assertEquals(sdk_features.trimIndent().trim(), actual.trim()) + } + + if (sdk_widgets != null) { + val actual = readFile(File(sdkFilesDir, "widgets.txt"), stripBlankLines, trim) + assertEquals(sdk_widgets.trimIndent().trim(), actual.trim()) + } + if (warnings != null) { assertEquals( warnings.trimIndent().trim(), @@ -960,102 +1024,129 @@ val intRangeAnnotationSource: TestFile = java( long from() default Long.MIN_VALUE; long to() default Long.MAX_VALUE; } - """ + """ ).indented() val intDefAnnotationSource: TestFile = java( """ -package android.annotation; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; -import static java.lang.annotation.RetentionPolicy.SOURCE; -@Retention(SOURCE) -@Target({ANNOTATION_TYPE}) -public @interface IntDef { - long[] value() default {}; - boolean flag() default false; -} -""" -) + package android.annotation; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + import static java.lang.annotation.ElementType.*; + import static java.lang.annotation.RetentionPolicy.SOURCE; + @Retention(SOURCE) + @Target({ANNOTATION_TYPE}) + public @interface IntDef { + long[] value() default {}; + boolean flag() default false; + } + """ +).indented() val nonNullSource: TestFile = java( """ -package android.annotation; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; + package android.annotation; + import java.lang.annotation.Retention; + import java.lang.annotation.Target; + + import static java.lang.annotation.ElementType.FIELD; + import static java.lang.annotation.ElementType.METHOD; + import static java.lang.annotation.ElementType.PARAMETER; + import static java.lang.annotation.RetentionPolicy.SOURCE; + /** + * Denotes that a parameter, field or method return value can never be null. + * @paramDoc This value must never be {@code null}. + * @returnDoc This value will never be {@code null}. + * @hide + */ + @SuppressWarnings({"WeakerAccess", "JavaDoc"}) + @Retention(SOURCE) + @Target({METHOD, PARAMETER, FIELD, TYPE_USE}) + public @interface NonNull { + } + """ +).indented() -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.RetentionPolicy.SOURCE; -/** - * Denotes that a parameter, field or method return value can never be null. - * @paramDoc This value must never be {@code null}. - * @returnDoc This value will never be {@code null}. - * @hide - */ -@SuppressWarnings({"WeakerAccess", "JavaDoc"}) -@Retention(SOURCE) -@Target({METHOD, PARAMETER, FIELD, TYPE_USE}) -public @interface NonNull { -} +val requiresPermissionSource: TestFile = java( + """ + package android.annotation; + import java.lang.annotation.*; + import static java.lang.annotation.ElementType.*; + import static java.lang.annotation.RetentionPolicy.SOURCE; + @Retention(SOURCE) + @Target({ANNOTATION_TYPE,METHOD,CONSTRUCTOR,FIELD,PARAMETER}) + public @interface RequiresPermission { + String value() default ""; + String[] allOf() default {}; + String[] anyOf() default {}; + boolean conditional() default false; + } + """ +).indented() -""" -) +val requiresFeatureSource: TestFile = java( + """ + package android.annotation; + import java.lang.annotation.*; + import static java.lang.annotation.ElementType.*; + import static java.lang.annotation.RetentionPolicy.SOURCE; + @Retention(SOURCE) + @Target({TYPE,FIELD,METHOD,CONSTRUCTOR}) + public @interface RequiresFeature { + String value(); + } + """ +).indented() -val requiresPermissionSource: TestFile = java( +val sdkConstantSource: TestFile = java( """ -package android.annotation; -import java.lang.annotation.*; -import static java.lang.annotation.ElementType.*; -import static java.lang.annotation.RetentionPolicy.SOURCE; -@Retention(SOURCE) -@Target({ANNOTATION_TYPE,METHOD,CONSTRUCTOR,FIELD,PARAMETER}) -public @interface RequiresPermission { - String value() default ""; - String[] allOf() default {}; - String[] anyOf() default {}; - boolean conditional() default false; -} - """ -) + package android.annotation; + import java.lang.annotation.*; + @Target({ ElementType.FIELD }) + @Retention(RetentionPolicy.SOURCE) + public @interface SdkConstant { + enum SdkConstantType { + ACTIVITY_INTENT_ACTION, BROADCAST_INTENT_ACTION, SERVICE_ACTION, INTENT_CATEGORY, FEATURE; + } + SdkConstantType value(); + } + """ +).indented() val nullableSource: TestFile = java( """ -package android.annotation; -import java.lang.annotation.*; -import static java.lang.annotation.ElementType.*; -import static java.lang.annotation.RetentionPolicy.SOURCE; -/** - * Denotes that a parameter, field or method return value can be null. - * @paramDoc This value may be {@code null}. - * @returnDoc This value may be {@code null}. - * @hide - */ -@SuppressWarnings({"WeakerAccess", "JavaDoc"}) -@Retention(SOURCE) -@Target({METHOD, PARAMETER, FIELD, TYPE_USE}) -public @interface Nullable { -} - """ -) + package android.annotation; + import java.lang.annotation.*; + import static java.lang.annotation.ElementType.*; + import static java.lang.annotation.RetentionPolicy.SOURCE; + /** + * Denotes that a parameter, field or method return value can be null. + * @paramDoc This value may be {@code null}. + * @returnDoc This value may be {@code null}. + * @hide + */ + @SuppressWarnings({"WeakerAccess", "JavaDoc"}) + @Retention(SOURCE) + @Target({METHOD, PARAMETER, FIELD, TYPE_USE}) + public @interface Nullable { + } + """ +).indented() val supportNonNullSource: TestFile = java( """ -package android.support.annotation; -import java.lang.annotation.*; -import static java.lang.annotation.ElementType.*; -import static java.lang.annotation.RetentionPolicy.SOURCE; -@SuppressWarnings("WeakerAccess") -@Retention(SOURCE) -@Target({METHOD, PARAMETER, FIELD, TYPE_USE}) -public @interface NonNull { -} - -""" -) + package android.support.annotation; + import java.lang.annotation.*; + import static java.lang.annotation.ElementType.*; + import static java.lang.annotation.RetentionPolicy.SOURCE; + @SuppressWarnings("WeakerAccess") + @Retention(SOURCE) + @Target({METHOD, PARAMETER, FIELD, TYPE_USE}) + public @interface NonNull { + } + """ +).indented() val supportNullableSource: TestFile = java( """ @@ -1073,80 +1164,78 @@ public @interface Nullable { val supportParameterName: TestFile = java( """ -package android.support.annotation; -import java.lang.annotation.*; -import static java.lang.annotation.ElementType.*; -import static java.lang.annotation.RetentionPolicy.SOURCE; -@SuppressWarnings("WeakerAccess") -@Retention(SOURCE) -@Target({METHOD, PARAMETER, FIELD}) -public @interface ParameterName { - String value(); -} - -""" -) + package android.support.annotation; + import java.lang.annotation.*; + import static java.lang.annotation.ElementType.*; + import static java.lang.annotation.RetentionPolicy.SOURCE; + @SuppressWarnings("WeakerAccess") + @Retention(SOURCE) + @Target({METHOD, PARAMETER, FIELD}) + public @interface ParameterName { + String value(); + } + """ +).indented() val supportDefaultValue: TestFile = java( """ -package android.support.annotation; -import java.lang.annotation.*; -import static java.lang.annotation.ElementType.*; -import static java.lang.annotation.RetentionPolicy.SOURCE; -@SuppressWarnings("WeakerAccess") -@Retention(SOURCE) -@Target({METHOD, PARAMETER, FIELD}) -public @interface DefaultValue { - String value(); -} - -""" -) + package android.support.annotation; + import java.lang.annotation.*; + import static java.lang.annotation.ElementType.*; + import static java.lang.annotation.RetentionPolicy.SOURCE; + @SuppressWarnings("WeakerAccess") + @Retention(SOURCE) + @Target({METHOD, PARAMETER, FIELD}) + public @interface DefaultValue { + String value(); + } + """ +).indented() val uiThreadSource: TestFile = java( """ -package android.support.annotation; -import java.lang.annotation.*; -import static java.lang.annotation.ElementType.*; -import static java.lang.annotation.RetentionPolicy.SOURCE; -/** - * Denotes that the annotated method or constructor should only be called on the - * UI thread. If the annotated element is a class, then all methods in the class - * should be called on the UI thread. - * @memberDoc This method must be called on the thread that originally created - * this UI element. This is typically the main thread of your app. - * @classDoc Methods in this class must be called on the thread that originally created - * this UI element, unless otherwise noted. This is typically the - * main thread of your app. * @hide - */ -@SuppressWarnings({"WeakerAccess", "JavaDoc"}) -@Retention(SOURCE) -@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER}) -public @interface UiThread { -} - """ -) + package android.support.annotation; + import java.lang.annotation.*; + import static java.lang.annotation.ElementType.*; + import static java.lang.annotation.RetentionPolicy.SOURCE; + /** + * Denotes that the annotated method or constructor should only be called on the + * UI thread. If the annotated element is a class, then all methods in the class + * should be called on the UI thread. + * @memberDoc This method must be called on the thread that originally created + * this UI element. This is typically the main thread of your app. + * @classDoc Methods in this class must be called on the thread that originally created + * this UI element, unless otherwise noted. This is typically the + * main thread of your app. * @hide + */ + @SuppressWarnings({"WeakerAccess", "JavaDoc"}) + @Retention(SOURCE) + @Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER}) + public @interface UiThread { + } + """ +).indented() val workerThreadSource: TestFile = java( """ -package android.support.annotation; -import java.lang.annotation.*; -import static java.lang.annotation.ElementType.*; -import static java.lang.annotation.RetentionPolicy.SOURCE; -/** - * @memberDoc This method may take several seconds to complete, so it should - * only be called from a worker thread. - * @classDoc Methods in this class may take several seconds to complete, so it should - * only be called from a worker thread unless otherwise noted. - * @hide - */ -@SuppressWarnings({"WeakerAccess", "JavaDoc"}) -@Retention(SOURCE) -@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER}) -public @interface WorkerThread { -} - """ -) + package android.support.annotation; + import java.lang.annotation.*; + import static java.lang.annotation.ElementType.*; + import static java.lang.annotation.RetentionPolicy.SOURCE; + /** + * @memberDoc This method may take several seconds to complete, so it should + * only be called from a worker thread. + * @classDoc Methods in this class may take several seconds to complete, so it should + * only be called from a worker thread unless otherwise noted. + * @hide + */ + @SuppressWarnings({"WeakerAccess", "JavaDoc"}) + @Retention(SOURCE) + @Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER}) + public @interface WorkerThread { + } + """ +).indented() val suppressLintSource: TestFile = java( """ @@ -1164,27 +1253,38 @@ public @interface SuppressLint { val systemServiceSource: TestFile = java( """ -package android.annotation; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.SOURCE; -import java.lang.annotation.*; -@Retention(SOURCE) -@Target(TYPE) -public @interface SystemService { - String value(); -} - """ -) + package android.annotation; + import static java.lang.annotation.ElementType.TYPE; + import static java.lang.annotation.RetentionPolicy.SOURCE; + import java.lang.annotation.*; + @Retention(SOURCE) + @Target(TYPE) + public @interface SystemService { + String value(); + } + """ +).indented() val systemApiSource: TestFile = java( """ -package android.annotation; -import static java.lang.annotation.ElementType.*; -import java.lang.annotation.*; -@Target({TYPE, FIELD, METHOD, CONSTRUCTOR, ANNOTATION_TYPE, PACKAGE}) -@Retention(RetentionPolicy.SOURCE) -public @interface SystemApi { -} -""" -) + package android.annotation; + import static java.lang.annotation.ElementType.*; + import java.lang.annotation.*; + @Target({TYPE, FIELD, METHOD, CONSTRUCTOR, ANNOTATION_TYPE, PACKAGE}) + @Retention(RetentionPolicy.SOURCE) + public @interface SystemApi { + } + """ +).indented() + +val widgetSource: TestFile = java( + """ + package android.annotation; + import java.lang.annotation.*; + @Target({ ElementType.TYPE }) + @Retention(RetentionPolicy.SOURCE) + public @interface Widget { + } + """ +).indented() diff --git a/src/test/java/com/android/tools/metalava/OptionsTest.kt b/src/test/java/com/android/tools/metalava/OptionsTest.kt index 6740f44..8e47973 100644 --- a/src/test/java/com/android/tools/metalava/OptionsTest.kt +++ b/src/test/java/com/android/tools/metalava/OptionsTest.kt @@ -62,6 +62,15 @@ API sources: --show-unannotated Include un-annotated public APIs in the signature file as well +Documentation: +--public Only include elements that are public +--protected Only include elements that are public or protected +--package Only include elements that are public, protected or + package protected +--private Include all elements except those that are marked + hidden +--hidden INclude all elements, including hidden + Extracting Signature Files: --api <file> Generate a signature descriptor file --private-api <file> Generate a signature descriptor file listing the @@ -86,6 +95,7 @@ Extracting Signature Files: for well known annotations like @Nullable and @NonNull. --proguard <file> Write a ProGuard keep file for the API +--sdk-values <dir> Write SDK values files to the given directory Generating Stubs: --stubs <dir> Generate stub source files for the API diff --git a/src/test/java/com/android/tools/metalava/SdkFileWriterTest.kt b/src/test/java/com/android/tools/metalava/SdkFileWriterTest.kt new file mode 100644 index 0000000..e28f65f --- /dev/null +++ b/src/test/java/com/android/tools/metalava/SdkFileWriterTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2018 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 SdkFileWriterTest : DriverTest() { + @Test + fun `Test generating broadcast actions`() { + check( + sourceFiles = *arrayOf( + java( + """ + package android.telephony; + + import android.annotation.SdkConstant; + import android.annotation.SdkConstant.SdkConstantType; + + public class SubscriptionManager { + /** + * Broadcast Action: The default subscription has changed. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_DEFAULT_SUBSCRIPTION_CHANGED + = "android.telephony.action.DEFAULT_SUBSCRIPTION_CHANGED"; + } + """ + ), + sdkConstantSource + ), + sdk_broadcast_actions = """ + android.telephony.action.DEFAULT_SUBSCRIPTION_CHANGED + """ + ) + } + + @Test + fun `Test generating widgets`() { + check( + sourceFiles = *arrayOf( + java( + """ + package android.widget; + + import android.content.Context; + import android.annotation.Widget; + + @Widget + public class MyButton extends android.view.View { + public MyButton(Context context) { + super(context, null); + } + } + """ + ), + widgetSource + ), + sdk_widgets = """ + Wandroid.view.View java.lang.Object + Wandroid.widget.MyButton android.view.View java.lang.Object + """ + ) + } +} \ No newline at end of file diff --git a/src/test/java/com/android/tools/metalava/StubsTest.kt b/src/test/java/com/android/tools/metalava/StubsTest.kt index d5f0df4..924373b 100644 --- a/src/test/java/com/android/tools/metalava/StubsTest.kt +++ b/src/test/java/com/android/tools/metalava/StubsTest.kt @@ -709,9 +709,10 @@ class StubsTest : DriverTest() { @Test fun `Check inheriting from package private class`() { checkStubs( - // Disabled because doclava1 includes fields here that it doesn't include in the - // signature file; not sure if it's a bug or intentional but it seems suspicious. + // Note that doclava1 includes fields here that it doesn't include in the + // signature file. //checkDoclava1 = true, + compatibilityMode = false, sourceFiles = *arrayOf( java( @@ -739,6 +740,7 @@ class StubsTest : DriverTest() { public class MyClass { public MyClass() { throw new RuntimeException("Stub!"); } public void method1() { throw new RuntimeException("Stub!"); } + public void method2() { throw new RuntimeException("Stub!"); } } """ ) @@ -1261,12 +1263,12 @@ class StubsTest : DriverTest() { public Kotlin(@android.support.annotation.NonNull java.lang.String property1, int arg2) { throw new RuntimeException("Stub!"); } @android.support.annotation.NonNull public java.lang.String method() { throw new RuntimeException("Stub!"); } /** My method doc */ - public final void otherMethod(boolean ok, int times) { throw new RuntimeException("Stub!"); } + public void otherMethod(boolean ok, int times) { throw new RuntimeException("Stub!"); } /** property doc */ - @android.support.annotation.Nullable public final java.lang.String getProperty2() { throw new RuntimeException("Stub!"); } + @android.support.annotation.Nullable public java.lang.String getProperty2() { throw new RuntimeException("Stub!"); } /** property doc */ - public final void setProperty2(@android.support.annotation.Nullable java.lang.String p) { throw new RuntimeException("Stub!"); } - @android.support.annotation.NonNull public final java.lang.String getProperty1() { throw new RuntimeException("Stub!"); } + public void setProperty2(@android.support.annotation.Nullable java.lang.String p) { throw new RuntimeException("Stub!"); } + @android.support.annotation.NonNull public java.lang.String getProperty1() { throw new RuntimeException("Stub!"); } public int someField2; } """, @@ -1553,6 +1555,7 @@ class StubsTest : DriverTest() { fun `Check generating required stubs from hidden super classes and interfaces`() { checkStubs( checkDoclava1 = false, + compatibilityMode = false, sourceFiles = *arrayOf( java( @@ -1617,14 +1620,17 @@ class StubsTest : DriverTest() { public class MyClass extends test.pkg.PublicSuperParent implements test.pkg.PublicInterface test.pkg.PublicInterface2 { ctor public MyClass(); method public void myMethod(); + method public void publicInterfaceMethod(); method public void publicInterfaceMethod2(); + method public void publicMethod(); + method public void publicMethod2(); field public static final int MY_CONSTANT = 5; // 0x5 } - public abstract interface PublicInterface { - method public abstract void publicInterfaceMethod(); + public interface PublicInterface { + method public void publicInterfaceMethod(); } - public abstract interface PublicInterface2 { - method public abstract void publicInterfaceMethod2(); + public interface PublicInterface2 { + method public void publicInterfaceMethod2(); } public abstract class PublicSuperParent { ctor public PublicSuperParent(); @@ -1641,10 +1647,10 @@ class StubsTest : DriverTest() { public MyClass() { throw new RuntimeException("Stub!"); } public void myMethod() { throw new RuntimeException("Stub!"); } public void publicInterfaceMethod2() { throw new RuntimeException("Stub!"); } - // Inlined stub from hidden parent class test.pkg.HiddenSuperClass public void publicMethod() { throw new RuntimeException("Stub!"); } - // Inlined stub from hidden parent class test.pkg.HiddenSuperClass + public void publicMethod2() { throw new RuntimeException("Stub!"); } public void publicInterfaceMethod() { throw new RuntimeException("Stub!"); } + public void inheritedMethod2() { throw new RuntimeException("Stub!"); } public static final int MY_CONSTANT = 5; // 0x5 } """ @@ -1835,19 +1841,16 @@ class StubsTest : DriverTest() { @SuppressWarnings({"unchecked", "deprecation", "all"}) public class MyClass<X extends java.lang.Number, Y> implements test.pkg.Generics.PublicParent<X,Y> { public MyClass() { throw new RuntimeException("Stub!"); } - // Inlined stub from hidden parent class test.pkg.Generics.HiddenParent2 public java.util.Map<X,java.util.Map<Y,java.lang.String>> createMap(java.util.List<X> list) { throw new RuntimeException("Stub!"); } } @SuppressWarnings({"unchecked", "deprecation", "all"}) public class MyClass2<W> implements test.pkg.Generics.PublicParent<java.lang.Float,W> { public MyClass2() { throw new RuntimeException("Stub!"); } - // Inlined stub from hidden parent class test.pkg.Generics.HiddenParent2 public java.util.Map<java.lang.Float,java.util.Map<W,java.lang.String>> createMap(java.util.List<java.lang.Float> list) { throw new RuntimeException("Stub!"); } } @SuppressWarnings({"unchecked", "deprecation", "all"}) public class MyClass3 implements test.pkg.Generics.PublicParent<java.lang.Float,java.lang.Double> { public MyClass3() { throw new RuntimeException("Stub!"); } - // Inlined stub from hidden parent class test.pkg.Generics.HiddenParent2 public java.util.Map<java.lang.Float,java.util.Map<java.lang.Double,java.lang.String>> createMap(java.util.List<java.lang.Float> list) { throw new RuntimeException("Stub!"); } } @SuppressWarnings({"unchecked", "deprecation", "all"}) @@ -1911,6 +1914,8 @@ class StubsTest : DriverTest() { } public class Generics.MyClass<X, Y extends java.lang.Number> extends test.pkg.Generics.PublicParent implements test.pkg.Generics.PublicInterface { ctor public Generics.MyClass(); + method public java.util.Map<X, java.util.Map<Y, java.lang.String>> createMap(java.util.List<X>); + method public java.util.List<X> foo(); } public static abstract interface Generics.PublicInterface<A, B> { method public abstract java.util.Map<A, java.util.Map<B, java.lang.String>> createMap(java.util.List<A>) throws java.io.IOException; @@ -1929,9 +1934,7 @@ class StubsTest : DriverTest() { @SuppressWarnings({"unchecked", "deprecation", "all"}) public class MyClass<X, Y extends java.lang.Number> extends test.pkg.Generics.PublicParent<X,Y> implements test.pkg.Generics.PublicInterface<X,Y> { public MyClass() { throw new RuntimeException("Stub!"); } - // Inlined stub from hidden parent class test.pkg.Generics.HiddenParent public java.util.List<X> foo() { throw new RuntimeException("Stub!"); } - // Inlined stub from hidden parent class test.pkg.Generics.HiddenParent public java.util.Map<X,java.util.Map<Y,java.lang.String>> createMap(java.util.List<X> list) { throw new RuntimeException("Stub!"); } } @SuppressWarnings({"unchecked", "deprecation", "all"}) @@ -1993,6 +1996,9 @@ class StubsTest : DriverTest() { } public static abstract class ConcurrentHashMap.KeySetView<K, V> implements java.util.Collection java.io.Serializable java.util.Set { ctor public ConcurrentHashMap.KeySetView(); + method public int size(); + method public final java.lang.Object[] toArray(); + method public final <T> T[] toArray(T[]); } } """, @@ -2004,11 +2010,8 @@ class StubsTest : DriverTest() { @SuppressWarnings({"unchecked", "deprecation", "all"}) public abstract static class KeySetView<K, V> implements java.util.Collection<K>, java.io.Serializable, java.util.Set<K> { public KeySetView() { throw new RuntimeException("Stub!"); } - // Inlined stub from hidden parent class test.pkg.ConcurrentHashMap.CollectionView public int size() { throw new RuntimeException("Stub!"); } - // Inlined stub from hidden parent class test.pkg.ConcurrentHashMap.CollectionView public final java.lang.Object[] toArray() { throw new RuntimeException("Stub!"); } - // Inlined stub from hidden parent class test.pkg.ConcurrentHashMap.CollectionView public final <T> T[] toArray(T[] a) { throw new RuntimeException("Stub!"); } } } @@ -2612,7 +2615,6 @@ class StubsTest : DriverTest() { @SuppressWarnings({"unchecked", "deprecation", "all"}) public class SpannableString implements test.pkg.SpanTest.CharSequence, test.pkg.SpanTest.Spannable { public SpannableString() { throw new RuntimeException("Stub!"); } - // Inlined stub from hidden parent class test.pkg.SpanTest.SpannableStringInternal public int nextSpanTransition(int start, int limit, java.lang.Class kind) { throw new RuntimeException("Stub!"); } } @SuppressWarnings({"unchecked", "deprecation", "all"}) @@ -2705,10 +2707,12 @@ class StubsTest : DriverTest() { public class SomeClass { /** * My method. + * @param focus The focus to find. One of {@link OtherClass#FOCUS_INPUT} or + * {@link OtherClass#FOCUS_ACCESSIBILITY}. * @throws IOException when blah blah blah * @throws {@link RuntimeException} when blah blah blah */ - public void baz() throws IOException; + public void baz(int focus) throws IOException; public boolean importance; } """ @@ -2719,6 +2723,8 @@ class StubsTest : DriverTest() { @SuppressWarnings("all") public class OtherClass { + public static final int FOCUS_INPUT = 1; + public static final int FOCUS_ACCESSIBILITY = 2; public int foo; public void bar(int baz, boolean bar); } @@ -2737,11 +2743,12 @@ class StubsTest : DriverTest() { warnings = "", source = """ package test.pkg1; + import test.pkg2.OtherClass; + import java.io.IOException; /** - * Blah blah {@link test.pkg2.OtherClass OtherClass} blah blah. - * Referencing <b>field</b> {@link test.pkg2.OtherClass#foo OtherClass#foo}, - * and referencing method {@link test.pkg2.OtherClass#bar(int, - * boolean) OtherClass#bar(int, + * Blah blah {@link OtherClass} blah blah. + * Referencing <b>field</b> {@link OtherClass#foo}, + * and referencing method {@link OtherClass#bar(int, * boolean)}. * And relative method reference {@link #baz()}. * And relative field reference {@link #importance}. @@ -2749,7 +2756,7 @@ class StubsTest : DriverTest() { * And here's one in the same package: {@link LocalClass}. * * @deprecated For some reason - * @see test.pkg2.OtherClass + * @see OtherClass * @see OtherClass#bar(int, boolean) */ @SuppressWarnings({"unchecked", "deprecation", "all"}) @@ -2757,10 +2764,12 @@ class StubsTest : DriverTest() { public SomeClass() { throw new RuntimeException("Stub!"); } /** * My method. - * @throws java.io.IOException when blah blah blah - * @throws {java.lang.RuntimeExceptionk RuntimeException} when blah blah blah + * @param focus The focus to find. One of {@link OtherClass#FOCUS_INPUT} or + * {@link OtherClass#FOCUS_ACCESSIBILITY}. + * @throws IOException when blah blah blah + * @throws {@link RuntimeException} when blah blah blah */ - public void baz() throws java.io.IOException { throw new RuntimeException("Stub!"); } + public void baz(int focus) throws java.io.IOException { throw new RuntimeException("Stub!"); } public boolean importance; } """ diff --git a/src/test/java/com/android/tools/metalava/model/TypeItemTest.kt b/src/test/java/com/android/tools/metalava/model/TypeItemTest.kt index 29c919f..b7b586c 100644 --- a/src/test/java/com/android/tools/metalava/model/TypeItemTest.kt +++ b/src/test/java/com/android/tools/metalava/model/TypeItemTest.kt @@ -16,6 +16,7 @@ package com.android.tools.metalava.model +import com.android.tools.metalava.JAVA_LANG_STRING import com.android.tools.metalava.options import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -25,7 +26,7 @@ class TypeItemTest { fun test() { options.omitCommonPackages = true assertThat(TypeItem.shortenTypes("@android.support.annotation.Nullable")).isEqualTo("@Nullable") - assertThat(TypeItem.shortenTypes("java.lang.String")).isEqualTo("String") + assertThat(TypeItem.shortenTypes(JAVA_LANG_STRING)).isEqualTo("String") assertThat(TypeItem.shortenTypes("java.lang.reflect.Method")).isEqualTo("java.lang.reflect.Method") assertThat(TypeItem.shortenTypes("java.util.List<java.lang.String>")).isEqualTo("java.util.List<String>") assertThat(TypeItem.shortenTypes("java.util.List<@android.support.annotation.NonNull java.lang.String>")).isEqualTo( -- GitLab