diff --git a/README.md b/README.md index 5a46b2c202c2530d73332c3568dd134e75dcfcb5..58ac765f4408ad6da07cdb8ef3fcc37a303db0de 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 01e847a4ae09ed1013d8db015e91c8a596f4a191..5a8b5cb3182070c78cf6d51a5448020425ff87ff 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 f82e67c326ed5e6291b6a848ed66cadd278c9050..1f2e4aa661e95deccb4f2dc8a498df22b43beb7e 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 bf36ddaeba4c3c2b69f7e70fb1b6017f64a07cc6..3819c1ee043f771e8786afff7f8f5263a5b4fdf6 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 7abc4ded965e141137856725758a1704330a20b9..a4cced958e053447629a955b42880ee972680d5b 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 0000000000000000000000000000000000000000..7badba36ef57f8da227aaf339a55903a797b621a --- /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 4fe3b2fac2a7517ddb0129fff40d6cbdf2890afc..872531b0ca59476aa10c84bdb3b39e6259e6804c 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 0000000000000000000000000000000000000000..633aa6a7660db80dab3d49846289aba3b70892ca --- /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 4b79517f3c2873bb77a4937d6aa4f8149c767e31..1342e22078229906afe940b8f875541b8c566b1b 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 06fbbda3ab4a0c7e8ec9e97c463603c031a9e57a..5d72e52db7c0ed5b3befd7b9e02adc5c325a573d 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 0000000000000000000000000000000000000000..c6980c5da2660010cc694e7f2697fefd2c7636d4 --- /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 a1a9534df289bfdb21ad1679ee04a5d7ab0534ae..6c2192350f95f1e68d995b6192dea5b70ee5adac 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 d4b03f9a5df244ddd7acd53c87fd4f4b6b59fff2..2bc9cc0c849e2a3c247906661d28fdd6b54ecd76 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 1993aa541cb2486441040ec29715684ce4377643..c9b042a3af6f5d386da2e9bfe3842f2c584aa46f 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 4b5af4c9ba825d62ede1d382e5f39f4711e3ccfb..dfbcaeeda6ddba1153602f82f7d2269164fddd7f 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 9e50d2d861982ff89d6273b37c6202d4a1b2061a..9f1bbd8cd7d0f79d0e23003b94b6f282d6ea5231 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 ecfa8cda85ea05601616e30faef616cb5f3cba5c..ae74cc15560b52106fdfc90c0ee3ac97ea82be68 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 6f7549322b69ba71a7766b806d194f1daf4b5f9b..37529de78d85bc2b245f3abf3f69b031c0395853 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 1dc8fd1907512654ec43a1d54b674e62198833c2..028960f41c5cb13613a0cc15db94b93fe7af4cac 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 6195682cef5df44c810028f0e8a04dbfbb864e44..edb325a0e216c2f124255758012488d74374b283 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 937b5ec915a21af288b4663972cc827f65e8d012..072e4e871bdef3a3ba1b135ad625fade53234e18 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 83a90df1f4d9a09ca0d742b45da562a8ae0af6b6..02fe52defb3c76726db96d4a6e7b427ed388ad00 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 c7524a0d7438585426c49dd20253c109c248ffde..7b949d3bb89a9ce7061a6e2b4755f4d434070f3e 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 0000000000000000000000000000000000000000..f70d21cb73b07134448b7fc2973ef306cfc3a50f --- /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 068d44ba6cdb0542daf8979d970ff8d3e79e213a..d8621a7a5ef3aa1b610f893d179664a771cd9161 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 89826e4da852964eb51bc8bf12407d514d7ca56e..d711446ed7df3662fc8025e9198e93c12d3ce0ed 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 c9ba5018d614ece5c5f009e2ff4f6e5b50177a7e..dc749c31bc2a75dfd25d5495dcff98687c8427dc 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 e0eb443457d4185eadb510e51ff393691dafd1b8..92a5a0c5bd046871002aa5c48ff343e19d23f213 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 efa6646f332c4c788fe801f89becfa0669a9e980..4a1a86a6f32ec5a2fb4b3df984eaab7064b27edb 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 2a5d0e74660c1da1885338fbc0f974dc8d0908af..e8888781fae2f54f74119814d4f547d7694dc34c 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 6740f449bbbb36b1d8bf35eaf50fa7e55c449b2c..8e47973bb58565cbb5b95e7f924e0e55d4b5218a 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 0000000000000000000000000000000000000000..e28f65fb1a8d6bb467f515fdc9f11c65627cd5b3 --- /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 d5f0df455be575cb35135b974b7b4605384785fd..924373b0e772d1adc661626876f36181c1ce3688 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 29c919f043c71057fde959b04beff66d8b57ec1a..b7b586c8e1f1eed88a72c290875ce2b786351a40 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(